//컬렉션 순회하기
for(Iterator<Integer> i = numberList.iterator();i.hasNext();){
Integer number = i.next();
...
}
//배열 순회하기
for(int i = 0; i < numberArrays.length; i++{
Integer number = numberArrays[i];
...
}
단순히 순회를 목적으로 한다면 필요한 것은 원소일 뿐인데 위와 같은 for문을 사용해야할까? 이 for문은 다음과 같은 단점을 가진다.
- 쓰이는 요소 종류가 늘어나면 헷갈려서 오류가 생길 가능성 증가한다.
- 컬렉션 예시 코드는 반복자 i가 세 번 쓰이고, 배열 예시 코드는 인덱스 i가 네 번 쓰이는데 이는 잘못된 변수 사용 가능성이 커진다.
- 변수를 잘못 사용할 경우, 컴파일 타임에 에러로 잡히지 않는다.
- 컬렉션이냐 배열이냐에 따라 코드 형태가 상당히 달라진다.
for-each 문( 향상된 for 문; enhanced for statement )
반복자와 인덱스 변수를 사용하지 않은 향상된 for 문은 코드가 깔끔해지고 오류가 날 일도 없다.
for (Integer number : numbers) {
...
}
콜론(:)은 안(in)과 같은 의미이고, numbers 안의 원소 number라고 읽을 수 있다. 하나의 관용구로 컬렉션과 배열을 모두 처리해 어떤 컨테이너를 다루든 신경쓰지 않아도 되고 속도도 그대로다. for-each 문이 만들어내는 코드는 사람이 최적화 한것과 사실상 같다.
그러면 컴파일 타임에 for-each문은 어떤식으로 최적화 할지 알아보자.
컴파일 전 for-each 문으로 통일했는데, 컴파일러는 List의 경우에는 iterator로, 배열의 경우에는 가장 보편적인 length를 통해 인덱스를 접근하는 방식으로 변형시켜주었다. 즉, for-each 문은 사용자가 사용하기 편하게 제공하면서 내부적으로는 최적화를 시켜주는 것이다.
여기서 이상한 점이 있다. for-each 문 내부를 전부 비웠긴 하지만, list의 for-each 문이 최적화된 내용은 var4 라는 변수를 증감식에서 next를 호출했다. 그러면 제일 처음 for 문에서는 var4 라는 변수를 사용할 수 없는 것 아닌가? 라고 생각했다.
그래서 다음과 같이 두 경우로 나눠서 어떤 식으로 컴파일 되는지를 확인했다.
// for-each 내부에 println 메소드 넣기 - java
List<Integer> numberList = List.of(1, 2, 3);
for (Integer number : numberList) {
System.out.println("number = " + number);
}
// for-each 내부에 println 메소드 넣기 - class
List<Integer> numberList = List.of(1, 2, 3);
Iterator var2 = numberList.iterator();
while(var2.hasNext()) {
Integer number = (Integer)var2.next();
System.out.println("number = " + number);
}
// for-each 내부에 새로운 변수를 선언하고, 거기에 현재 원소를 넣기 - java
List<Integer> numberList = List.of(1, 2, 3);
for (Integer number : numberList) {
int a = number+2;
}
// for-each 내부에 새로운 변수를 선언하고, 거기에 현재 원소를 넣기 - class
List<Integer> numberList = List.of(1, 2, 3);
Integer number;
int var4;
for(Iterator var2 = numberList.iterator(); var2.hasNext(); var4 = number + 2) {
number = (Integer)var2.next();
}
매번 동일한 형식의 정적인 for 문을 형성하는게 아닌 가장 최적의 방법으로 컴파일 되고 필요하다면 while 문도 사용했다.
즉, 사람이 만들어낸 코드보다 더 안전하게 최적화 해준다는 것을 알 수 있다.
컬렉션 중첩 순회
이 경우 for-each 문의 이점이 더 커진다. for 문의 반복문을 중첩할 때 저지르는 실수를 예방할 수도 있다.
다음과 같이 모양을 나타내는 Suit 열거타입과 숫자를 나타내는 Rank를 통해 카드를 만든다고 가정해보자.
public class Card {
private final Suit suit;
private final Rank rank;
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING }
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());
Card(Suit suit, Rank rank) {
this.suit = suit;
this.rank = rank;
}
}
이 때 일반적인 iterator로 for 문을 구성하면 버그가 발생할 수 있다.
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
13개의 숫자(Rank) * 4개의 모양(Suit) = 52개의 카드로 덱을 설정해야 하지만 Suit의 next 메소드가 내부 Rank의 반복자에서 같이 호출된다. 이렇게 되면 (CLUB, ACE), (DIAMOND, DEUCE), (HEART, THREE), (SPADE, FOUR) 까지 호출하고, Rank가 FIVE를 호출할 때 Suit는 순회가 끝났는데 next() 메소드를 호출하면서 NoSuchElementException을 던진다.
만약 숫자도 4개까지 있었으면, 4*4 = 16개의 카드로 덱을 생성했던 것을 기대했던 것과 달리 네 번의 반복만 하고 예외도 던지지 않고 종료하게 된다. 주사위를 두 번 굴렸을 때 나오는 경우의 수 6*6을 기대했다면, 위 코드처럼 코딩할 경우 (1,1)...(6.6)이 되며 예외도 던지지 않고 종료된다는 의미다. [주사위 예시 코드]
하지만 for-each 문을 사용한다면 이 문제는 굉장히 단순해지고 간결해진다.
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));
for-each 문을 사용할 수 없는 상황
파괴적인 필터링(destructive filtering)
컬렉션을 순회하며 선택된 원소를 제거해야 한다면 반복자의 remove 메소드를 호출해야한다. 자바 8부터는 Collection의 removeIf 메소드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
변형(transforming)
리스트나 배열을 순회하며 그 원소의 값 일부나 전체를 교체해야 하면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
병렬 반복(parallel iteration)
여러 컬렉션을 병렬로 순회해야 하면, 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야한다.
[테스트 코드 확인] - 위 카드의 iterator 실수 예시와 같은 사례
3가지 상황에 속한다면 일반적인 for문을 사용하되, 설명한 예외 사례들을 주의하며 사용하자.
for-each문의 사용 조건
컬렉션과 배열은 물론 Iterable 인터페이스를 구현한 객체라면 무엇이든 순회할 수 있다.
// Iterable 인터페이스는 이 객체의 원소들을 순회하는 반복자를 반환해주고
// splicator와 forEach 메소드가 default로 제공된다.
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
따라서 원소들의 묶음을 표현하는 타입을 작성해야하면 Collection 인터페이스를 구현하진 않더라도 Iterable을 구현해두면 원소들의 순회에 사용할 수 있어서 좋아진다. for-each 문은 명료하고, 유연하고 버그를 예방해주며 성능저하도 없으니 가능한 모든 곳에서 사용하자.
* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.
'Reading > Effective Java' 카테고리의 다른 글
[Effective-Java] Item 60.정확한 답이 필요하다면 float와 double은 피하라 (0) | 2021.11.25 |
---|---|
[Effective-Java] Item 59.라이브러리를 익히고 사용하라 (0) | 2021.11.25 |
[Effective-Java] Item 57.지역변수의 범위를 최소화하라 (0) | 2021.11.14 |
[Effective-Java] Item 55.옵셔널 반환은 신중히 하라 (0) | 2021.11.14 |
[Effective-Java] Item 54.null이 아닌, 빈 컬렉션이나 배열을 반환하라 (0) | 2021.11.08 |