item58. 전통적인 for문보다는 for-each문을 사용하라.
item58. 전통적인 for문보다는 for-each문을 사용하라.
✅ 핵심 정리
- 전통적인 for문 - 인덱스 변수를 사용하여 객체의 각 요소에 접근한다. 하지만 인덱스 변수는 코드를 장황하게 만들기 때문에, 읽기 어렵고 버그가 발생하기 쉬울 수 있다.
- for-each문 - 인덱스 변수 없이 반복 가능한 객체의 각 요소에 순차적으로 접근할 수 있다. 이를 통해 코드를 더 간결하고 읽기 쉽게 만들 수 있다. 또한, for-each문은 컴파일러가 컴파일 타임에 요소의 타입을 검사하기 때문에 타입 안정성(type safety)을 보장해줍니다.
⚓️서론
이 아이템장에서는 전통적인 for문을 사용해보고 문제점을 파악하고, 그 문제점을 해결하는 for-each문을 배울 것이다.
1️⃣전통적인 for 문으로 컬렉션을 순회하는 코드.
➡️컬렉션 순회하기 - for loop
for(iterator<Element> i = c.iterator(); i.hasNext(); ){
Element e = i.next();
//do something
}
➡️배열 순회하기
String[] str = new String[]{"a","b","c"};
for(int i = 0; i < str.length; i++){
String element = arr[i];
//do something
}
➡️컬렉션 순회하기 - while
Iterator<Element> i = c.iterator();
while(iterator.hasNext()){
Element e = i.next();
//do something
}
for loop와 배열 순회하기는 반복자와 인덱스 변수 모두 코드를 지저분하게 한다. 또한 이렇게 쓴다면 요소 종류가 늘어나면 오류가 생길 가능성이 높아진다.
혹시라도 잘못된 변수를 사용했을 때 컴파일러가 잡아준다는 보장도 없다.
컬렉션이냐 배열이냐에 따라 코드 형태가 상당히 달라지므로 주의해야한다.
우리에게 필요한 것은 원소들뿐이다.
해결책은 for-each문
을 사용하면 위의 주의사항을 피하면서 사용할 수 있다.
2️⃣for-each를 알아보자.
for-each를 사용하면
- 반복자와 인덱스 변수를 사용하지 않아도된다.
- 코드가 깔끔해진다.
- 오류가 생길 가능성이 적어진다.
- 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어서 어떤 컨테이너를 다루던지 신경쓰지 않아도된다.
➡️컬렉션과 배열을 순회하는 올바른 관용구
for(Element e : elements){
... // do something
}
- 위에서 콜론(:)은 “안의(in)”라고 읽으면 된다.
- 반복문은 “element 안의 각 원소 e에 대해” 라고 읽는다.
- 반복 대상이 컬렉션이든 배열이든, for-each 문으로 사용해도 속도는 그대로이다.
- 컬렉션을 중첩해 순회해야 한다면 for-each 문의 이점이 더욱 커진다.
컬렉션 중첩 사용
🔡카드 예)
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;
}
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()));
}
}
- 위 코드를 실행하면
NoSuchElementException
을 던진다.
➡️왜 NoSuchElementException을 던질까?
바깥 컬렉션(suits)의 반복자에서 next메서드가 너무 많이 불린다. → 마지막 줄의 i.next()를 주목
next()는 숫자(suit) 하나당 한 번씩만 불러야하는데, 안쪽 반복문에서 호출되기 때문에 카드(Rank) 하나당 한번 씩 불리고 있다.
따라서 숫자가 바닥나면 반복문에서 NoSuchElementException
을 던진다.
➡️위 코드를 for-each로 만들면
for (Suit suit : suits) {
for (Rank rank : ranks) {
deck.add(new Card(suit, rank));
}
}
코드가 정말 간결해지고 가독성이 좋아진다. 머리가 덜 아프다.
- 결과를 찍어보면 .
ACE-----CLUB
DEUCE-----CLUB
THREE-----CLUB
FOUR-----CLUB
FIVE-----CLUB
SIX-----CLUB
SEVEN-----CLUB
EIGHT-----CLUB
NINE-----CLUB
TEN-----CLUB
JACK-----CLUB
QUEEN-----CLUB
KING-----CLUB
ACE-----DIAMOND
DEUCE-----DIAMOND
THREE-----DIAMOND
FOUR-----DIAMOND
FIVE-----DIAMOND
SIX-----DIAMOND
SEVEN-----DIAMOND
EIGHT-----DIAMOND
NINE-----DIAMOND
TEN-----DIAMOND
JACK-----DIAMOND
QUEEN-----DIAMOND
KING-----DIAMOND
ACE-----HEART
DEUCE-----HEART
THREE-----HEART
FOUR-----HEART
FIVE-----HEART
SIX-----HEART
SEVEN-----HEART
EIGHT-----HEART
NINE-----HEART
TEN-----HEART
JACK-----HEART
QUEEN-----HEART
KING-----HEART
ACE-----SPADE
DEUCE-----SPADE
THREE-----SPADE
FOUR-----SPADE
FIVE-----SPADE
SIX-----SPADE
SEVEN-----SPADE
EIGHT-----SPADE
NINE-----SPADE
TEN-----SPADE
JACK-----SPADE
QUEEN-----SPADE
KING-----SPADE
🔡주사위 예시)
- 주사위를 두 번 굴렸을 때 나올 수 있는 모든 경우의 수를 출력하는 코드
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
***************************
위 코드는 예외는 반환하지 않는다.
하지만
가능한 조합을 단 여섯 쌍만 출력하고 끝나버린다. 원래는 36개 조합이 나와야 한다.
이 문제를 해결하려면 바깥 반복문에 바깥 원소를 저장하는 변수를 하나 추가해야 한다.
🔡문제 해결 코드
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);
}
}
}
}
- 결과
ONE ONE
ONE TWO
ONE THREE
ONE FOUR
ONE FIVE
ONE SIX
TWO ONE
TWO TWO
TWO THREE
TWO FOUR
TWO FIVE
TWO SIX
THREE ONE
THREE TWO
THREE THREE
THREE FOUR
THREE FIVE
THREE SIX
FOUR ONE
FOUR TWO
FOUR THREE
FOUR FOUR
FOUR FIVE
FOUR SIX
FIVE ONE
FIVE TWO
FIVE THREE
FIVE FOUR
FIVE FIVE
FIVE SIX
SIX ONE
SIX TWO
SIX THREE
SIX FOUR
SIX FIVE
SIX SIX
for-each는 코드를 완전히 깔끔히 만들면서 간단히 해결했다.
하지만 for-each문을 사용할 수 없는 상황이 있다.
➡️for-each문을 사용할 수 없는 상황
1️⃣ 파괴적인 필터링(destructive filtering)
- 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove 메소드를 호출해야 한다. 자바 8부터는 Collecion의 removeIf 메소드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
2️⃣ 변형(transforming)
- 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야한다.
3️⃣병렬 반복(parallel iteration)
- 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.
세 가지 상황 중 하나에 속할 때는 일반적으로 for문을 사용한다.