본문 바로가기

회고/이펙티브 자바 3판

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

반응형

이펙티브 자바3판 게시글은 문단별로 핵심 문장만 추려서 정리합니다

내용을 곱씹고 다시 한번 생각해보기 위해서 기록을 남깁니다

 

일단 전통적인 for문 사용을 먼저 살펴본다.

 

1. 컬렉션 순회하기 -> 더 나은 방법이 있다.

for (Iterator<Element> i = c.iterator(); i.hasNext();){
	Element e = i.next();
}

 

2,. 배열 순회하기 -> 더 나은 방법이 있다.

for ( int i = 0 ; i < a.length ; i++ ) {
	//Todo : 해야 할 일
}

 

전통적인 for문은 while문보다는 좋은 방법이지만, 인덱스 변수는 코드를 지저분하게 하고 있으며 우리가 필요한 것은 결국 원소이다. 반복자 i의 많은 등장은 변수를 잘못 사용할 틈새를 주는 것이다. 또한 잘못된 변수를 사용하더라도 컴파일러가 잡아주리라는 보장이 없다.

 

 이 문제는 for-each문을 사용하여 해결된다. for-each의 정식 이름은 '향상된 for문(enhanced for statement)'이다. 반복자와 인덱스 변수를 사용하지 않아 코드가 깔끔해지고 오류가 날 일이 없다.

 

컬렉션과 배열을 순회하는 올바른 관용구

for ( Element e : elements ) {
	//Todo : e로 무엇인가를 한다.
}

 

여기서 콜론(:) 은 "안의(in)"라고 읽으면 된다. "elements 안의 각 원소 e에 대해" 라고 읽는다. 컬렉션이든, 배열이든 for-each문을 사용해도 속도는 그대로이다. 특히 중첩된 컬렉션에서는 더욱 이점이 커진다.

 

버그를 찾아보자

enum Suit {CLUB, DIAMOND, HEART, SPADE}
enum Rank {ACE, DEUCE, THREE, FOUR, FIVE .... }
...

static Collection<Suit> suits = Arrays.asList(Suit.values());
static 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());

 여기서 문제는 바깥 쪽 Suit에 의해서 i.next()가 너무 많이 불린다는 것이 문제이다. 이 next()는 '숫자(Suit) 하나당' 한 번씩만 불려야 하는데, 안쪽 반복문에서 호출되는 바람에 '카드(Rank) 하나당' 한 번씩 불리고 있다. 따라서 Suit가 바닥나면 NoSuchElemnentException을 던지고 말 것이다.

 운이 나빠서 바깥 컬렉션의 크기가 안쪽 컬렉션 크기의 배수라면 이 반복문은 예외를 던지지 않고 종료한다. 물론 우리가 원하는 일을 수행하지 않은 채 말이다.

 

같은 버그, 다른 증상!

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

 이 프로그램은 예외를 던지지는 않지만 가능한 조합을 "ONE ONE" 부터 "SIX SIX" 까지 단 6쌍만 출력하고 끝이 난다. 수정하기 위해서 바깥 반복문의 i.next()를 분리해본다.

 

문제는 고쳤지만 보기 좋진 않다. 더 나은 방법이 있다.

enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }
...
Collection<Face> faces = EnumSet.allOf(Face.class);

for(Iterator<Face> i = faces.iterator(); i.hasNext(); )
	Face face = i.next();
	for(Iterator<Face> j = faces.iterator(); j.hasNext(); )
    	System.out.println(face + " " + j.next());

이것을 완전한 for-each문으로 바꿔보자

 

컬렉션이나 배열의 중첩 반복을 위한 관용구

for (Suit suit : suits)
	for (Rank rank : ranks)
    	deck.add(new Card(suit,rank));

 

하지만 다음과 같은 상황에서는 for-each를 사용하지 않고 전통적인 for문을 사용한다.

 

1. 파괴적인 필터링

 컬렉션을 순회하면서 선택된 원소를 제거한다면 반복자의 remove를 호출해야 한다. 자바 8부터는 Collection의 removeIf 메서드를 사용해서 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.

 

2. 변형

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

 

3. 병렬 반복

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

 

 for-each문은 컬렉션과 배열은 물론이고 Iterable 인터페이스를 구현한 객체라면 무엇이든지 순회할 수 있다. 따라서, 원소들의 묶음을 표현하는 타입을 작성해야 한다면 Iterable을 구현하는 쪽으로 고민해보면 된다. 비록, 해당 타입에서 Collection 인터페이스를 구현하지 않아도 말이다.

public interface Iterable<E> {
	Iterator<E> iterator();
}

 

* 느낀점

 순회하면서 인덱스를 이용해 변형을 하지 않는 이상 for-each문이 얼마나 강력한지 알 수 있었다. 단순히 컬렉션과 배열을 순회할 때는 무조건 for-each문을 사용하도록 염두해 두어야 겠다. 또한, Iterable을 구현하면 이것을 인식해서 순회가 가능하므로 이 점도 염두하면 좋다. 하지만 다시한번, 모든 경우에 for-each가 적용되는 것이 아님은 인지하자

반응형