Java Collection Framework 계층을 보면, Iterable은 Collection 인터페이스(List, Set, Queue)의 상위 인터페이스입니다.
Collection 인터페이스 메소드로는 add(), contains(), size() 등이 있는데 그러면 왜 굳이 Iterable이 최상위 인터페이스로 있는 걸까요?
Iterable은 반복 가능한 객체를 의미합니다. 즉, Collection이 아니어도 반복 가능한 객체의 역할을 부여하고 싶다면 Iterable 인터페이스를 구현하면 됩니다.
Iterable을 구성하는 메소드는 3개가 있습니다.
메소드 | 설명 |
---|---|
default void forEach(Consumer<? super T> action) | 람다 전용 메소드 |
Iterator<T> iterator() | Iterator |
default Spliterator<T> spliterator() | 파이프라이닝 관련 메소드 |
자바8부터 디폴트 메소드로 추가된 forEach()를 살펴보면, Consumer<? super T>
인터페이스 타입의 action 파라미터를 받아서 for (T t : this)
: 각 요소를 순회하며 action.accept(t);
를 호출합니다. 즉, 리턴 타입이 없는 수행 작업을 람다로 받아서 순회 요소에 대한 작업을 수행합니다. (물론, 람다 대신 익명 클래스도 가능하겠지만요.. 😅)
신기하게도 스트림을 생성하지 않고도 곧바로 forEach를 사용할 수 있습니다.
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.forEach(System.out::println);
당연히, 스트림을 생성한 후 forEach()를 사용은 가능하지만 굳이 단순 반복 목적이라면 오버헤드를 감당하면서까지 스트림으로 변환할 일은 없겠지만요.
iterator()는 추상 메소드로 선언이 되어 있습니다. 따라서 Iterable 인터페이스를 구현하려면 반드시 iterator()를 오버라이딩해서 Iterator 인터페이스를 반환해야 하는데요. 반복 가능한 객체를 의미하는 Iterable 인터페이스는 추상적인 개념이고, iterator()를 통해 실제 반복을 수행할 Iterator 객체를 제공해서 추상화를 구현하는 것입니다.
Iterable이 반복 가능한 인터페이스를 의미한다면, Iterator는 실제 반복 작업을 수행하는 인터페이스입니다.
Iterable의 요소를 안전하고 일관된 방식으로 접근할 수 있는 역할을 하는데, hasNext()로 확인을 하고 next()로 순회하는 방식을 의미합니다.
또한, remove()를 사용하려면 next()가 선행 호출 되어야 합니다. remove() 메소드는 "마지막으로 반환된 요소"를 제거하도록 설계되었기 때문입니다.
메소드 | 설명 |
---|---|
boolean hasNext() | 다음 요소가 있으면 true, 없으면 false 반환 |
E next() | 다음 요소를 반환하고 커서를 전진 |
default void remove() | 마지막으로 반환된 요소를 제거 (선택적 작업) |
default void forEachRemaining(Consumer<? super E> action) | 남은 모든 요소에 대해 지정된 작업 수행 |
또한, 단순 for문을 통해서는 요소들을 삭제하는 것이 까다롭기 때문에 순회 요소에 접근 후에 제거해야 한다면 Iterator를 적극 활용하는 것이 좋아 보입니다. (역순으로 조회하면서 제거하면 되긴 하지만 가독성이 떨어진다고 생각합니다. 아니면 자바8의 removeIf()
도 좋은 방법이겠죠)
굳이 index가 필요한 게 아니라면 for (int i=0; i<100; i++)
문을 잘 안쓰고 for (String str : list)
혹은 list.forEach(str -> ..)
이런 식으로 향상된 for문이나 forEach()를 자주 사용하는데요. (이펙티브
왜 Iterator 파트에서 for-each 루프에 대해 이야기를 했을까요? (공식적으로는 "enhanced for statement"라고 한다고 합니다.)
컴파일러가 for-each 루프를 Iterator를 사용하는 코드로 변환시켜주기 때문입니다.
예를 들어, 다음과 같은 for-each 루프는
for (String item : list) {
System.out.println(item);
}
컴파일러에 의해 대략 아래와 같이 변환됩니다.
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
System.out.println(item);
}