[Java] Iterable
정의
java.lang.Iterable<T> 는 for-each 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, iterator() 를 정의한다.
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) { ... }
default Spliterator<T> spliterator() { ... }
}
Collection 인터페이스가 Iterable 을 extends 하므로 ArrayList, LinkedList, HashSet, LinkedList 등 모든 컬렉션이 자동으로 for-each 대상이 된다. 컬렉션이 아닌 사용자 정의 타입 도 Iterable 만 구현하면 같은 문법으로 순회 가능.
for-each 문의 desugaring
for (String s : myIterable) {
System.out.println(s);
}
위 코드는 컴파일 시 아래로 변환된다.
Iterator<String> $it = myIterable.iterator();
while ($it.hasNext()) {
String s = $it.next();
System.out.println(s);
}
즉 for-each 는 단순히 iterator() 호출 + hasNext() / next() 패턴의 문법 설탕. 배열의 경우는 컴파일러가 인덱스 기반 for 로 변환 (배열은 Iterable 이 아님, 특별 케이스).
Iterator 인터페이스
Iterable.iterator() 가 반환하는 객체는 Iterator<E> 를 구현해야 한다.
public interface Iterator<E> {
boolean hasNext(); // 다음 원소가 있는가
E next(); // 다음 원소를 반환, 커서 전진
default void remove() { ... } // 마지막에 반환한 원소를 삭제 (선택)
default void forEachRemaining(Consumer<? super E> action) { ... }
}
3가지 약속:
hasNext()가 true 인 동안에만next()호출 가능, 아니면NoSuchElementExceptionremove()는 직전next()호출이 있어야 호출 가능, 아니면IllegalStateExceptionremove()두 번 연속 호출 금지, 다음next()가 있어야 다시 가능
사용자 정의 Iterable
Iterable 구현은 매우 간단하다. 예: 1 부터 N 까지 정수를 순회하는 타입.
public class Range implements Iterable<Integer> {
private final int end;
public Range(int end) { this.end = end; }
@Override
public Iterator<Integer> iterator() {
return new Iterator<>() {
int i = 1;
@Override public boolean hasNext() { return i <= end; }
@Override public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
return i++;
}
};
}
}
// 사용
for (int x : new Range(5)) {
System.out.println(x); // 1, 2, 3, 4, 5
}
기본 메서드 두 가지
forEach(Consumer)
Java 8 부터 추가. 람다와 함께 더 간결.
list.forEach(System.out::println);
list.forEach(x -> log.info("item: {}", x));
내부적으로 iterator() 사용. 따라서 fail-fast iterator 의 영향을 받는다 (수정 중이면 CME).
spliterator()
Java 8 부터 추가. Stream / 병렬 처리를 위해 컬렉션을 분할 가능한 형태로 노출.
list.spliterator(); // 단일 spliterator
list.stream(); // Stream API 의 입구
list.parallelStream(); // 병렬 Stream
대부분의 사용자가 직접 spliterator() 를 호출할 일은 없다. Stream API 가 알아서 사용.
Iterable vs Collection vs Stream
| Iterable | Collection | Stream | |
|---|---|---|---|
| 크기 알 수 있나 | 보장 안 됨 | size() 있음 | 보장 안 됨 |
| 재사용 가능한가 | ✓ (여러 번 iterator 생성 가능) | ✓ | ✗ (1회 소비) |
| 수정 가능한가 | 정의 안 함 | add/remove 정의 | ✗ (불변 파이프라인) |
| 무한 가능한가 | ✓ | ✗ | ✓ |
| 상위 관계 | 최상위 | extends Iterable | 별도 인터페이스 |
IMPORTANT
Iterable 은 크기와 무한성에 대한 약속이 없다. 무한 시퀀스를 Iterable 로 모델링해도 합법. 다만 for-each 가 영원히 안 끝나니 break 가 필요. 대안으로 Stream 의 limit(n) / takeWhile 이 더 명확.
무한 Iterable 의 예
피보나치 수열을 무한 시퀀스로:
public class Fibonacci implements Iterable<Long> {
@Override
public Iterator<Long> iterator() {
return new Iterator<>() {
long a = 0, b = 1;
@Override public boolean hasNext() { return true; } // 무한
@Override public Long next() {
long r = a;
long next = a + b;
a = b; b = next;
return r;
}
};
}
}
// 사용
for (long x : new Fibonacci()) {
if (x > 1000) break;
System.out.println(x);
}
Iterable 의 강점은 이런 lazy / 무한 시퀀스도 자연스럽게 표현되는 점.
Stream 으로 변환
Iterable 을 Stream 으로 바꾸는 표준 방법:
Iterable<String> it = ...;
Stream<String> stream = StreamSupport.stream(it.spliterator(), false);
false 는 순차 스트림, true 는 병렬. Iterable 이 spliterator() 의 기본 구현을 갖고 있어 가능.
자주 만나는 함정
1. iterator() 가 매번 새 인스턴스를 반환해야 함
// ❌ 잘못된 구현 - iterator 를 캐시
public class BadIterable<T> implements Iterable<T> {
private final Iterator<T> it; // 단 하나 보관
@Override public Iterator<T> iterator() { return it; }
}
// → for-each 두 번 돌리면 두 번째에서 hasNext() == false
매 iterator() 호출이 새 인스턴스를 반환해야 다회 순회가 가능.
2. for-each 안에서 수정 → CME
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
for (Integer x : list) {
list.add(4); // ❌ ConcurrentModificationException
}
ConcurrentModificationException 참조.
3. next() 의 결과를 두 번 부르면 두 원소가 진행됨
Iterator<Integer> it = list.iterator();
if (it.next() > 0 || it.next() > 0) { ... }
// ^^^^^^^^^^ ^^^^^^^^^^
// 첫 원소 두 번째 원소 (의도와 다를 수 있음)
it.next() 는 부수효과 (커서 전진) 가 있다. 한 번만 부르고 변수에 저장하라.
참고
이 글의 용어 (7개)
- [Java] ArrayListjava
- 정의 는 동적 배열 (dynamic array) 로 구현된 . 내부적으로 를 들고 다니며, 용량이 부족해지면 새 배열로 옮긴다. Java 의 가장 흔한 컬렉션 구현체. 인덱스 기…
- [Java] Collectionjava
- 정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
- [Java] ConcurrentModificationExceptionjava
- 정의 (CME) 는 가 컬렉션의 구조 변경을 탐지했을 때 던지는 unchecked 예외 ( ). 이름과 달리 반드시 동시성 (멀티스레드) 문제일 때만 발생하는 것이 아니다. 단일…
- [Java] Fail-Fast Iteratorjava
- 정의 Fail-Fast Iterator 는 순회 도중 컬렉션의 구조가 변경되면 즉시 을 던지는 iterator. , , 등 대부분의 컬렉션의 iterator 가 이 방식. 목적은…
- [Java] LinkedListjava
- 정의 는 양방향 연결 리스트 (doubly linked list) 로 구현된 이자 . 각 원소는 와 포인터를 가진 노드로 보관된다. 와 같은 List 이지만, 메모리 레이아웃과 …
- [Java] Listjava
- 정의 는 순서가 있고 중복이 허용 되는 컬렉션을 표현하는 인터페이스. 각 원소는 0 부터 시작하는 정수 인덱스로 접근 가능. 인터페이스를 확장하며, 의 모든 성질을 물려받는다. …
- [Java] Objectjava
- 정의 는 Java 의 모든 클래스의 최상위 부모 (root) 클래스. 가 명시되지 않은 클래스는 컴파일러가 자동으로 를 붙인다. 인터페이스는 클래스가 아니라 를 직접 상속하지는 …
이 개념을 다룬 위키 페이지 (26)
- wiki[Java] ArrayDeque
- wiki[Java] ArrayList
- wiki[Java] BlockingQueue
- wiki[Java] Collection
- wiki[Java] ConcurrentHashMap
- wiki[Java] ConcurrentLinkedDeque
- wiki[Java] ConcurrentLinkedQueue
- wiki[Java] ConcurrentSkipListMap
- wiki[Java] ConcurrentSkipListSet
- wiki[Java] CopyOnWriteArrayList
- wiki[Java] CopyOnWriteArraySet
- wiki[Java] Deque
- wiki[Java] HashMap
- wiki[Java] HashSet
- wiki[Java] LinkedHashMap
- wiki[Java] LinkedList
- wiki[Java] List
- wiki[Java] Map
- wiki[Java] Object
- wiki[Java] PriorityQueue
- wiki[Java] Queue
- wiki[Java] Set
- wiki[Java] TreeMap
- wiki[Java] TreeSet
- wiki[Java] Vector
- wiki[Spring] CompositeMap & CompositeIterator
💬 댓글