Effective Java : 아이템 58. 전통적인 for 문보다는 for-each 문을 사용하라.

    Effective Java : 아이템 58. 전통적인 for 문보다는 for-each 문을 사용하라.

    • 전통적인 for문은 다음 단점이 있음.
      • 불필요한 반복자가 많이 생성됨 → 코드 실수 범위가 넓어짐. (잘못된 변수를 사용해도 컴파일러가 잡아주지 않음) 
      • 컬렉션 / 배열이냐에 따라 for문의 코드가 달라짐.
    • for-each를 사용하면 위의 단점을 해결해줌.
      • for-each의 콜론(:)은 "안의(in)"으로 읽으면 됨. 
    • for-each를 사용할 수 없는 경우
      • 순회하며 원소를 삭제해야 하는 경우.
      • 순회하며 원소를 교체해야하는 경우.
      • 여러 컬렉션을 병렬로 돌아야 하는 경우 → 이 경우 여러 번의 for-each로 분리해서 가능할 수도 있음. 

    전통적인 for문을 단점이 존재함.

    While문 보다는 for문을 사용하는 것이 좋다. 코드가 좀 더 깔끔하기 때문인데, 그럼에도 불구하고 for문 자체는 두 가지 단점이 존재한다.

    • 반복자 / 반복변수가 반복문 내에 계속 사용됨.
    • 배열 / 컬렉션이냐에 따라 사용하는 방법이 달라짐. 

    첫번째 단점을 아래에서 살펴볼 수 있다. 여기서 반복변수로 사용되는 i는 두 줄짜리 코드에서 총 4번 사용되었다. 사용되는 곳이 많아지면 실수할 여지도 많아진다. 즉, for문 자체만을 사용했을 때 반복자 / 반복변수에 의해서 코드에서 실수할 여지가 많아지는 것이다. 

    for (int i = 0; i < list.size(); i++) {
        Integer integer = list.get(i);
    }

    두번째 단점은 아래에서 살펴볼 수 있다. 아래에서 볼 수 있듯이 for 문에서 컬렉션과 배열의 사용 방법이 다르다. 예를 들어 List는 size()로 끝지점을 찾고, get() 메서드를 호출해 필요한 원소를 호출한다. 배열은 각각 length를 호출하고, 인덱스로 직접 접근한다. for문은 이런 단점이 존재한다.

    List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    for (int i = 0; i < list.size(); i++) {
        Integer integer = list.get(i);
    }
    
    int[] intArray = new int[10];
    for (int i = 0; i < intArray.length; i++) {
        int i1 = intArray[i];
    }

     


    전통적인 for문의 단점은 for-each로 해결됨

    for-each문은 콜론(:)을 이용해서 컬렉션 / 배열을 순회하는 방법이다. 여기서 콜론(:)은 "in"이라는 의미로 사용되는데 풀어서 설명하면, "intArray 안의 i" 같은 느낌으로 이해할 수 있다. 아래 코드에서 두 가지 단점이 모두 해결된 것을 볼 수 있다. 

    • 배열 / 컬렉션을 순회하는 방법이 동일함. 
    • 불필요한 반복자 / 반복변수가 없음. 
    for (int i : list) {
        System.out.println(i);
    }
    
    for (int i : intArray) {
        System.out.println(i);
    }

    따라서 전통적인 For문을 사용하는 것보다 for-each를 사용하는 것이 더 깔끔하다. 

     


    전통적인 For문의 실수할만한 부분

    전통적인 For문에서는 원소를 사용하기 위해서 반복자 / 반복인수를 이용해서 직접 원소를 가져와야한다. 그 과정에서 주의하지 못하면 놓칠만한 부분이 있다. 아래 코드에서는 i.next() 하는 부분 때문에 원하는만큼의 카드가 만들어지지 않을 것이다. iterator()는 한번 next()를 호출하면 그 다음 값을 호출하기 때문이다. 

    enum Suit {CLUB, DIAMOND, HEART, SPADE}
    enum Rank {ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING}
    public record Card(Suit suit, Rank rank) {}
    
    public void badCase() {
    
        List<Suit> suits = Arrays.asList(Suit.values());
        List<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()));
            }
        }
    }

    for문에서 발생하는 이런 종류의 실수들은 컴파일 시점에 발견되지 않는다. 위 경우는 런타임에서 발견되긴 하지만, 만약 Suit / Rank의 Enum 변수 길이가 같았다면 런타임 시점에도 이런 에러는 발견되지 않았을 것이다.

    전통적인 for문에서는 원소를 사용하기 위해 반복자 / 반복변수에 직접 접근해야 하기 때문에 이런 어처구니 없는 실수가 나타날 수 있다. for문 대신 for-each문을 사용하면 위의 문제는 깔끔하게 해결된다. for-each는 원소를 직접 전달해주기 때문에 위와 같은 실수도 하지 않으며, 코드 역시 굉장히 깔끔해지는 것을 알 수 있다. 

    public void goodCase() {
    
        List<Suit> suits = Arrays.asList(Suit.values());
        List<Rank> ranks = Arrays.asList(Rank.values());
    
        List<Card> deck = new ArrayList<>();
        for (Suit suit : suits)
            for (Rank rank : ranks)
                deck.add(new Card(suit, rank));
    }

     


    for-each를 사용할 수 없는 경우.

    for문보다 for-each를 사용하는 것이 더 좋다는 것을 알 수 있었다. 그렇지만 for문 대신 항상 for-each를 사용할 수 있는 것은 아니다. 어떤 경우에 for-each를 사용할 수 없을까? 아래 세 가지 경우다.

    • 배열 / 컬렉션을 순회하며 원소를 지워야 할 때 
    • 배열 / 컬렉션을 순회하며 원소를 바꾸어야 할 때
    • 여러 배열 / 컬렉션을 동시에 순회해야 할 때 → 오히려 for-each를 분리하는게 좋을지도.

    가장 마지막 부분의 예시는 아래에서 확인해 볼 수 있다. Suit를 순회하면서 세 개의 컬렉션(ranks1, ranks2, ranks3)을 함께 병렬로 순회해야 한다면 for-each를 사용해서는 한번에 해결할 수는 없다. 하지만 아래 코드에는 예상되는 문제가 있는데 ranks1, ranks2, ranks3이 같은 크기를 가지는 컬렉션이라는 것을 보장할 수 없다. 자칫 잘못하면 런타임에서 IndexBoundOut 같은 에러가 발생할 것이다. 

    public void isItGood() {
        List<Suit> suits = Arrays.asList(Suit.values());
        List<Rank> ranks1 = Arrays.asList(Rank.values());
        List<Rank> ranks2 = Arrays.asList(Rank.values());
        List<Rank> ranks3 = Arrays.asList(Rank.values());
    
        List<Card> deck = new ArrayList<>();
        for (int i = 0; i < suits.size(); i++) {
            for (int j = 0; j < ranks3.size(); j++) {
                deck.add(new Card(suits.get(i), ranks1.get(j)));
                deck.add(new Card(suits.get(i), ranks2.get(j)));
                deck.add(new Card(suits.get(i), ranks3.get(j)));
            }
        }
        
    }

    오히려 병렬로 순회해야하는 상황이라면 아래와 같이 For-each문을 여러 번 분리해서 사용하는 것이 좋은 방법이 될 수도 있을 것이다.

    public void thisIsBetter() {
        List<Suit> suits = Arrays.asList(Suit.values());
        List<Rank> ranks1 = Arrays.asList(Rank.values());
        List<Rank> ranks2 = Arrays.asList(Rank.values());
        List<Rank> ranks3 = Arrays.asList(Rank.values());
    
        List<Card> deck = new ArrayList<>();
        for (Suit suit : suits) 
            for (Rank rank : ranks1)
                deck.add(new Card(suit, rank));
    
        for (Suit suit : suits)
            for (Rank rank : ranks2)
                deck.add(new Card(suit, rank));
    
        for (Suit suit : suits)
            for (Rank rank : ranks3)
                deck.add(new Card(suit, rank));
    }

     

    댓글

    Designed by JB FACTORY