peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

Item 58. 전통적인 for문보다는 for-each문을 사용하라 #62

Open jseok0917 opened 2 months ago

jseok0917 commented 2 months ago

Chapter : 9. 일반적인 프로그래밍 원칙

Item : 58. 전통적인 for문보다는 for-each문을 사용하라

Assignee : jseok0917


🍑 서론

전통적인 for문

List<String> fruits = Arrays.asList("Blackberry", "Strawberry", "Raspberry");
int[] primeNumbers = {2, 3, 5, 7, 11};

//컬렉션의 전통적인 for문
for (Iterator<String> iter = fruits.iterator(); iter.hasNext();) {
            //...
}

//배열의 전통적인 for문
for (int i = 0; i < primeNumbers.length; i++) {
    //...
}




🍑 본론

for-each문(향상된 for문)

// 콜론(:)은 안의(in)으로 해석하면 된다.
for (String fruit : fruits) {
    //...
}

for (int prime : primeNumbers) {
    //...
}


전통적인 for문에서 버그를 찾아보자

enum Suit {CLUB, DIAMOND, HEART, SPADE}
enum Rank {ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
    NINE, TEN, JACK, QUEEN, KING}

static class Card {
    //필드
    Suit suit;
    Rank rank;
    //생성자
    public Card (Suit suit, Rank rank) {
        this.suit = suit;
        this.rank = rank;
    }
}

public static void main(String[] args) {

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

    //트럼프카드 deck 컬렉션을 생성하는 전통적인 for문
    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()));
        }
    }

    System.out.println(deck); //NoSuchElementException 오류 발생, 
    //코드 작성에 따라, 바깥쪽 컬렉션의 크기가 안쪽 컬렉션 크기의의 배수라면 오류가 생기지 않을 수도 있음(책 주사위 코드 참고)

}


for-each문을 사용한다면?

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



for-each문을 사용할 수 없는 상황


  1. 파괴적인 필터링(destructive filtering)
    • 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove 메서드를 호출해야 한다.
    • 자바8부터는 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다
//rank가 ACE인 카드들은 모두 제거하는 코드
for (Card card : deck) {
    if (card.rank == Rank.ACE) {

        //ConcurrentModificationException 발생
        deck.remove(card);
    }
}

//그렇다면 올바른 방법은?
//1. 인덱스를 이용한 제거
for (int i = 0; i < deck.size(); i++) {
    Card card = deck.get(i);
    if (card.rank == Rank.ACE) {
        deck.remove(i);
        i--; // 삭제 후 리스트의 인덱스가 하나씩 땡겨지므로 i를 하나 감소시킴
    }
}

//2. Iterator를 이용한 제거
for (Iterator<Card> iter = deck.iterator(); iter.hasNext();) {
    Card card = iter.next();
    if (card.rank == Rank.ACE) {
        iter.remove();
    }
}

//3. removeIf를 사용하여 제거(전통적인 for문과 명시적인 인덱스나 Iterator를 쓰지않고 제거 가능)
deck.removeIf(card -> card.rank == Rank.ACE);


  1. 변형(transforming)
    • 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야한다.
//rank가 ACE인 카드들을 모두 DIAMOND ACE카드로 변경하는 코드

for (Card card : deck) {
    if (card.rank == Rank.ACE) {
        card = new Card(Suit.DIAMOND, Rank.ACE);
    }
}

//오류가 발생하진 않지만, deck의 내부 객체들은 전혀 변경되지 않는다.
System.out.println(deck); //기존 deck과 동일


  1. 병렬 반복(parallel iteration)
    • 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야한다.
      • 말이 이해가 안돼서 많이 헷갈렸다...
      • 여기서 병렬은 병행적(In Concurrently)이라기보다는 한번에(In Parallel)의 의미(원서 참고, https://stackoverflow.com/questions/33208013/parallel-iteration-on-multiple-collections 참고)
      • 병렬 순회라는 말보다, 엄격하고 명시적으로 제어해야한다는 부분에 주목
      • 예시 : 하나의 인덱스로 2개 이상의 컬렉션(혹은 배열)을 순회할 때(애초에 for-each문 자체가 불가능)
//58.4(Iterator를 명확하게 명시하고, 반복조건을 명확히 적어줌)
for (Iterator<Suit> i = suits.iterator(); i.hasNext();) {
    for (Iterator<Rank> j = ranks.iterator(); j.hasNext();) {
        deck.add(new Card(i.next(), j.next()));
    }
}

//엄격하고 명시적인 제어가 필요한 이유
Integer[] evens = {2, 4, 6, 8};
Integer[] odds = {1, 3, 5, 7, 9};
List<Integer> evenNumbers = Arrays.asList(evens);
List<Integer> oddNumbers = Arrays.asList(odds);

//1. 배열의 경우
//인덱스를 이용해 evens의 원소들과 odds의 원소들을 순차적으로 더한 값을 출력하는 코드
for (int i = 0; i < odds.length; i++){
    System.out.println(evens[i] + odds[i]); // OutofBound 인덱스 오류 발생
}

//다음과 같이 조건을 엄격하게 명시
for (int i = 0; i < evens.length && i < odds.length; i++){
    System.out.println(evens[i] + odds[i]);
}

//2. 리스트의 경우
//다음처럼 Iterator로 반복가능한 조건을 명확하게 명시하고, 
//다루는 원소 또한 명확하게 명시
for (Iterator evenIter = evenNumbers.iterator(), oddIter = oddNumbers.iterator(); evenIter.hasNext() && oddIter.hasNext();){
    Integer even = (Integer) evenIter.next();
    Integer odd = (Integer) oddIter.next();
    System.out.println(even+odd);
}

//3. for-each문의 경우
for(int i : evens){
    //???? 어케 even과 odd를 함께 더하죠?
    System.out.println(i + ?);
}

//for-each는 다음처럼 내부적으로 Iterator 하나만 이용하므로 병렬적으로 두 컬렉션의 원소를 다룰 수 없다
for (Iterator evenIter = evenNumbers.iterator(); evenIter.hasNext()){
    Integer even = (Integer) evenIter.next();
    Integer odd = (Integer) oddIter.next(); //oddIter가 없어용
    System.out.println(even+odd); 

}



Iterable 구현하기

//직접 구현하긴 까다롭지만
//원소들의 묶음을 표현하는 타입을 작성해야 한다면,
//해당 타입에서 Collection 인터페이스는 구현하지 않기로 했더라도 Iterable은 구현하는게 좋을 수 있다.

//Iterable 인터페이스, 단 하나의 메서드만 구현하면 된다.
public interface Iterable<T> {
    //이 객체의 원소들을 순회하는 반복자를 반환한다.
    Iterator<T> iterator();
}

//Iterator 인터페이스, 
//hasNext()는 탐색할 것이 남아있는지 boolean은 반환하는 메서드
//next()는 다음 값을 넘겨주는 메서드
public interface Iterator<E> {
    boolean hasNext();
    E next();

}



🍑 결론