JAVA/Effective Java

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

꾸준함. 2024. 3. 16. 03:51

아이템 45에서 다루었다시피 stream이 제격인 작업이 있고 반복이 제격인 작업이 있습니다.

 

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (Iterator<Integer> i = list.iterator(); i.hasNext(); ) {
Integer num = i.next();
// num을 이용한 비즈니스 로직
}
for (int i = 0; i < list.size(); i++) {
// list[i]를 이용한 비즈니스 로직
}
}
view raw .java hosted with ❤ by GitHub

 

 

앞서 아이템 57에서도 언급했듯이 for문과 같은 광용구들은 while문보다는 낫지만 다음 이유 때문에 최선의 방법은 아닙니다.

  • 진짜 필요한 것은 원소들 뿐이지만 위 코드처럼 작성할 경우 반복자와 인덱스 변수 모두 선언해야 하기 때문에 코드가 지저분해짐
  • 컬렉션이나 배열이여냐에 따라 코드 형태가 달라짐

 

for-each문

  • 정식 이름은 `향상된 for문(enhanced for statement)`
  • for-each 문은 컬렉션과 배열은 물론 Iterable 인터페이스를 구현한 객체라면 무엇이든 순회 가능
  • 반복자와 인덱스 변수를 사용하지 않기 때문에 코드가 깔끔해지고 오류가 날 일이 없음
  • 하나의 관용구로 되어있어 컬렉션과 배열 모두 같은 코드 형태이며 성능도 동일
  • 콜론(:)은 안의(in) 라고 읽으면 됨
    • 따라서 아래 코드의 반복문은 `elements 안의 각 원소 e에 대해`라고 해석하면 됨

 

for (Element e : elements) {
... //e로 무언가를 한다
}
view raw .java hosted with ❤ by GitHub

 

1. 중첩 컬렉션에서 for-each문의 이점 극대화


public class Card {
private final Suit suit;
private final Rank rank;
// Can you spot the bug?
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;
}
public static void main(String[] args) {
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()));
}
}
}
}
view raw .java hosted with ❤ by GitHub

 

코드 부연 설명

  • 위 코드에서 Iterator를 사용할 경우 `deck.add(new Card(i.next(), j.next()));`에서 NoSuchElementException 예외 발생
    • next()가 Suit 하나당 한 번식만 불려야 하는데
    • 안쪽 반복문에서 호출되는 바람에 Rank 하나당 한 번씩 불렸기 때문
    • 위 코드를 고친다고 해도 코드가 지저분해지는 단점 존재


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()));
}
}
view raw .java hosted with ❤ by GitHub

  • 반면 for-each문을 사용하면 깔끔하고 간단하게 오류를 유발하지 않는 코드를 작성할 수 있음

 

for (Suit suit : suits) {
for (Rank rank : ranks) {
deck.add(new Card(suit, rank));
}
}
view raw .java hosted with ❤ by GitHub

 

2. for-each문을 사용할 수 없는 케이스

 

2.1 파괴적인 필터링(destructive filtering)

  • 리스트를 순회하면서 원소를 제거할 때, 리스트의 구조가 변경되면서 ConcurrentModificationException이 발생
  • 컬렉션을 순회하면서 선택된 원소를 제거해야 할 경우 반복자의 remove 메서드를 호출해야 함
  • 자바 8버전부터 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적으로 순회하지 않아도 됨

 

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
for (Integer number : numbers) {
if (number % 2 == 0) {
numbers.remove(number); // ConcurrentModificationException 발생!
}
}
System.out.println(numbers);
view raw .java hosted with ❤ by GitHub

 

Iterator<Integer> iterator = numbers.iterator();
while (iterator.hasNext()) {
Integer number = iterator.next();
if (number % 2 == 0) {
iterator.remove(); // 반복자의 remove 메서드를 호출하여 안전하게 제거
}
}
System.out.println(numbers);
view raw .java hosted with ❤ by GitHub

 

2.2 변형(transforming)

  • 리스트나 배열을 순회하면서 원소 값 일부 혹은 전체를 교체하는 경우에는 인덱스를 사용해야 함
  • ex) arr[i] = 1;

 

2.3 병렬 반복(parallel iteration)

  • 여러 컬렉션을 병렬로 순회해야 할 경우 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 함
  • for-each문은 각각의 요소에 대해 반복할 때마다 내부적으로 반복자(iterator)를 사용하여 요소를 가져오는데, 이러한 반복자는 병렬 처리를 지원하지 않음

 

참고

이펙티브 자바

 

반응형