본문 바로가기

Reading Record/이펙티브자바

[아이템 57, 58] 지역변수의 범위를 최소화하라, 전통적인 for 문보다는 for-each 문을 사용하라

[아이템 57] 지역변수의 범위를 최소화하라

지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다.

지역변수의 범위를 줄이는 몇 가지 방법

1. 가장 처음에 쓰일 때 선언한다

지역변수를 사용하려면 멀었는데 미리 선언 부터 해두면 가독성이 떨어진다.
또한 지역변수의 범위는 선언된 지점부터 그 지점을 포함한 블록이 끝날 때까지이므로, 실제 사용되는 블록 바깥에 선언된 지역변수는 그 블록이 끝나더라도 살아남게 된다. 실수로 해당 변수를 사용하게 된다면 예기치 못한 상황이 발생할 가능성이 있다.

2. 거의 모든 지역변수는 선언과 동시에 초기화한다

만약 초기화에 필요한 정보가 충분하지 않다면 충분해질때까지 선언을 미뤄두자.

그런데 이 규칙의 예외로 초기화에 검사 예외를 던질 가능성이 있는 경우는 try블록 안에서 초기화해야한다. 만약 변수를 try블록이 끝난 뒤에도 사용해야 한다면 선언은 try블록 바로 앞에서 선언하도록 하자.

반복문은 독특한 방식으로 변수 범위를 최소화 해준다. 반복 변수의 범위는 for문의 반복문의 몸체, for 키워드와 몸체 사이의 괄호 안으로 제한된다. 따라서 for 문안에서 선언된 반복 변수는 해당 for 문 밖에선 사용할 수 없게된다.

for문의 경우 for문에서 선언된 반복 변수를 for 문 바깥에서 사용할 경우 컴파일 타임에 잡아주기도 한다.

사용할 수 없다

또한 for문의 변수 유효 범위가 for문 범위와 일치하기 때문에 똑같은 이름의 변수를 여러 반복문에서 사용하여도 서로 영향을 주지않는다.

영향을 주지않는다

또 for문 관용구의 반복 변수를 여러개 선언하여 사용할 수 있다.

반복 변수 선언

반복 여부를 결정짓는 변수 i 의 한계값을 변수 k 에 저장하여, 반복 때마다 다시 계산하는 비용을 없앴다. 같은 값을 반환하는 메소드를 매번 호출한다면 이런 관용구를 사용하면 좋을 것이다.

3. 메소드를 작게 유지하고 한 가지 기능에 집중한다.

한 메소드에서 여러 가지 기능을 처리한다면 그중 한기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있을 것이다.

단순히 메소드를 기능별로 쪼갠다면 이런 것을 해결할 수 있을 것이다.

 

[아이템 58] 전통적인 for 문 보다는 for-each 문을 사용하라 

컬렉션을 순회할 때 전통적으로 다음과 같이 for문을 사용한다.

전통적인 방법

실질적으로 우리가 사용할 것은 컬렉션의 원소인데 전통적인 for 문에서는 반복자와 인덱스 변수가 많이 등장해서 코드를 지저분하게 한다.
첫 번째 for문에선 i 라는 반복자가 총 3번 등장하고, 두 번째 for문에선 i라는 인덱스 변수가 총 4번 등장한다.
또 컬렉션인지 배열인지에 따라 코드 형태가 상당히 달라지기 때문에 주의해야한다.

이런 경우 for-each 구문을 사용하면 깔끔하게 해결된다.

for-each 구문

for-each는 'enhanced-for statement' 로 '향상된 for 문' 을 의미한다. 반복자나 인덱스 배열을 사용하지 않아 코드가 깔끔해지고 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어서 어떤 컨테이너를 다루는지 신경쓰지 않아도 된다.

for-each 문은 컬렉션을 중첩해서 사용해야 할 때 그 이점이 더욱 커진다.

public class Test {
    public static void main(String[] args) {
        Collection<Suit> suits = Arrays.asList(Suit.values());
        Collection<Rank> ranks = Arrays.asList(Rank.values());

        List<Card> deck = new ArrayList<>();
        for (Iterator<Suit> i = suits.iterator() ; i.hasNext() ;) {
            for (Iterator<Rank> j = ranks.iterator() ; j.hasNext() ;) {
                deck.add(new Card(i.next(), j.next()));
            }
        }
    }

    static class Card {
        Suit suit;
        Rank rank;

        public Card(final Suit suit, final Rank rank) {
            this.suit = suit;
            this.rank = rank;
        }
    }

    enum Suit {
        CLUB, DIAMOND, HEART, SPADE
    }

    enum Rank {
        ACE, DEUCE, THREE, FOUR, FIVE,
        SIX, SEVEN, EIGHT, NINE, TEN,
        JACK, QUEEN, KING
    }
}

위 와 같이 트럼프 카드를 만든는 중첩 for 문이 있다고 하자. 각각의 반복자를 가져와 중첩 for문을 통해 원소들을 순회하면서 카드를 만든다.
여기서 문제점은 무엇일까? 카드 문양의 반복자 i가 문양 하나당 호출되어야 하는데 중첩 for문의 제일 안쪽에서 숫자 하나당 호출 되고 있기때문에 Suit의 모든 원소를 순회하고 나면 NoSuchElementException을 던질 것이다.

Exception 발생

for-each 문을 사용하여 해결해 보자.

for-each

코드가 훨씬 간결해졌다.

하지만 for-each 문을 사용할 수 없는 상황이 세 가지 존재한다.

1. 파괴적인 필터링 - 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 remove 메소드를 호출해야 한다.

2. 변형 - 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.

3. 병렬 반복 - 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.

위의 세 가지 상황의 간단한 예제는 다음 링크에 구현해 두었다. - for-each를 사용할 수 없는 상황 세 가지
 

[아이템 58] for-each를 사용할 수 없는 상황 세가지 · Issue #128 · Java-Bom/ReadingRecord

p.349 하지만 안타깝게도 for-each를 사용할 수 없는 상황이 세가지 존재한다. 파괴적인 필터링 변형 병렬 반복 위 세가지 경우 예시 코드 있으면 좋을 것 같아

github.com

마지막으로 for-each 문은 컬렉션과 배열은 물론 Iterable 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다.

따라서 원소들의 묶음을 표현하는 타입을 구현해야 한다면 Iterable을 구현하는 쪽으로 고민해보자