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

반응형
728x90
반응형

전통적인 for문

각 경우에 따라 스트림이 제격인 작업이 있고 반복이 제격인 작업이 있다.

 

컬렉션 순회하기 - 더 나은 방법
package com.java.effective.item58;

public class Main {
    public static void main(String[] args) {
        for (Iterator<Element> i = c.iterator(); i.hasNext();) {
            element e = i.next();
            ... // e 로 무언가를 한다.
        }
    }
}

 

배열 순회하기
package com.java.effective.item58;

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < a.length; i++) {
            ... // a[i] 로 무언가를 한다.
        }
    }
}

이 관용구들은 while 문보다는 낫지만 가장 좋은 방법은 아니다. 반복자와 인덱스 변수는 모두 코드를 지저분하게 할 뿐 우리가 사용할건 바로 원소다. 이처럼 쓰이는 요소 종류가 늘어나면 오류가 생길 가능성이 높아진다. 우리는 이를 for-each를 사용하여 해결할 수 있다.

 

 

for-each문

for-each 문의 정식 이름은 '향상된 for문 (enhanced for statement)'이다. 반복자와 인덱스 변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일도 없다. 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어서 어떤 컨테이너로 다루는지는 신경쓰지 않아도 된다.

package com.java.effective.item58;

public class Main {
    public static void main(String[] args) {
        for (Element e : elements) {
            ... // e 로 무언가를 한다.
        }
    }
}

: 을 "안의(in)"이라고 읽으면 된다. "elements 안의 각 원소 e에 대해"라고 읽는다. 반복대상이 컬렉션이든 배열이든, for-each문을 사용해도 속도는 그대로다. 컬렉션을 중첩해 순회해야한다면 for-each문의 이점이 더욱 커진다.

 

반복문을 중첩하는 코드를 보자.
package com.java.effective.item58;

import java.util.*;

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}

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

    static Collection<Suit> suits = Arrays.asList(Suit.values());
    static Collection<Rank> ranks = Arrays.asList(Rank.values());

    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()));
            }
        }
    }
}

문제가 있다. 바깥 컬렉션(suits)의 반복자에서 next 메서드가 너무 많이 호출된다. 마지막 줄의 i.next()를 보자. 이 next()는 '숫자(Suit) 하나당' 한번씩만 불려야 하는데, 안쪽 반복문에서 호출되는 바람에 '카드(Rank) 하나당' 한번씩 불리고 있다. 이렇게되면 숫자가 바닥나서 반복문에서 NoSuchElementException을 던진다. 

 

바깥 컬렉션의 크기가 안쪽 컬렉션의 크기의 배수라면 예외를 던지지 않고 종료될 수 있다.

 

좀더 나은 방법 - 해결방안
package com.java.effective.item58;

import java.util.*;

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}

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

    static Collection<Suit> suits = Arrays.asList(Suit.values());
    static Collection<Rank> ranks = Arrays.asList(Rank.values());

    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()));
//            }
//        }

        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()));
            }
        }

    }
}

추가로 예를 들어보자.

 

주사위를 두번 굴렸을때 나올 수 있는 모든 경우의수를 출력하는 코드
package com.java.effective.item58;

import java.util.*;

public class DiceRolls {
    enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }

    public static void main(String[] args) {
        Collection<Face> faces = EnumSet.allOf(Face.class);

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

 

▶ 결과

ONE ONE
TWO TWO
THREE THREE
FOUR FOUR
FIVE FIVE
SIX SIX

모든 경우의수를 출력하지않고 6쌍만 출력하고 종료된다. 이 문제를 해결하려면 바깥 반복문에 바깥 원소를 저장하는 변수를 하나 추가해야한다.

 

for-each문으로 해결
package com.java.effective.item58;

import java.util.*;

public class DiceRolls {
    enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }

    public static void main(String[] args) {
        Collection<Face> faces = EnumSet.allOf(Face.class);

        for (Face f1 : faces) {
            for (Face f2 : faces) {
                System.out.println(f1 + " " + f2);
            }
        }
    }
}

 

 

for-each문이 사용 불가능한 상황

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

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

 

2) 변형 (transforming)

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

 

3) 병렬 반복 (parallel iteration)

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

 

위 3가지 상황 중 하나에 속할 경우 일반적으로 for 문을 사용하되, 앞서 언급되었던 문제들에 주의해야한다. 

 

 

 

 

반응형

Designed by JB FACTORY