Iterable과 Iterator
이 예제에서 hasNext의 커서 확인과 next의 전진을 분리하고 배열·연결 구현을 향상된 for문으로 같은 방식으로 순회합니다.
순회는 저장 구조와 소비 코드를 분리합니다.
배열은 index를 증가시키고 연결 목록은 next 참조를 따라가지만, 호출자는 hasNext()와 next()만 사용합니다.
이 장의 Iterable 구현은 순회마다 새 Iterator를 만들고, 각 Iterator는 한 번의 순회 위치를 소유합니다.
Iterable은 “새 순회를 시작할 수 있는 대상”이고 Iterator는 “지금 어디까지 읽었는지를 기억하는 순회 도구”입니다. 목록을 두 번 읽을 때는 같은 Iterable에서 Iterator를 두 개 얻습니다. 첫 Iterator가 끝났다고 목록의 원소를 지운 것은 아닙니다. 다음 원소가 있는지 묻는 hasNext와 그 원소를 꺼내 위치를 옮기는 next의 역할을 분리해서 읽으세요.
hasNext의 커서 변경 위험
hasNext는 다음 원소의 존재를 확인하며, 이 예제의 커서를 전진시켜 원소를 건너뛰면 안 됩니다.
다음 잘못된 반복자는 hasNext에서 커서를 증가시키고 next에서도 증가시켜, 이 main에서는 20과 40만 출력한 뒤 정상 종료합니다.
import java.util.Iterator;
public final class AdvancingHasNextFailure {
public static void main(String[] args) {
Iterator<Integer> iterator = new Broken(new int[] {10, 20, 30, 40});
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
private static final class Broken implements Iterator<Integer> {
private final int[] values;
private int cursor;
Broken(int[] values) {
this.values = values;
}
public boolean hasNext() {
return ++cursor < values.length;
}
public Integer next() {
return values[cursor++];
}
}
}잘못된 hasNext가 10과 30을 건너뛴다
hasNext가 커서를 잘못 전진시켜 10·30을 건너뛰고 20·40만 반환합니다. 다섯 호출의 cursor 변화가 계약 위반의 직접 원인입니다.
원문 main의 다섯 호출을 따라가면 hasNext와 next가 모두 커서를 증가시켜 20과 40을 출력하고 정상 종료합니다. 각 호출 전후 cursor는 코드에서 추적한 상태입니다.
ITERATOR · 잘못된 커서 이동
잘못된 hasNext가 10과 30을 건너뛴다
hasNext와 next가 모두 커서를 증가시키면, 출력은 20·40만 남습니다.
실선: 원 다섯 호출 순서질문과 반환이 모두 전진20·40만 stdout에 출력
| 실제 호출 순서 | cursor 전 · 코드 추적 | 반환·결과 | cursor 후 · 코드 추적 |
|---|---|---|---|
hasNext() 1 | 0 | true | 1 |
next() 1 | 1 | 20 출력 | 2 |
hasNext() 2 | 2 | true | 3 |
next() 2 | 3 | 40 출력 | 4 |
hasNext() 3 | 4 | false · 정상 종료 | 5 |
hasNext()1- cursor 전 · 코드 추적
0- 반환·결과
true- cursor 후 · 코드 추적
1
next()1- cursor 전 · 코드 추적
1- 반환·결과
20출력- cursor 후 · 코드 추적
2
hasNext()2- cursor 전 · 코드 추적
2- 반환·결과
true- cursor 후 · 코드 추적
3
next()2- cursor 전 · 코드 추적
3- 반환·결과
40출력- cursor 후 · 코드 추적
4
hasNext()3- cursor 전 · 코드 추적
4- 반환·결과
false· 정상 종료- cursor 후 · 코드 추적
5
이 예의 조건
- 잘못된 hasNext가 커서를 전진시키는 원 호출 순서이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원 Broken의 다섯 호출을 실제 순서로 연결합니다. hasNext가 0→1과 2→3으로 전진하여 10과 30을 건너뛰고 next는 20·40을 반환합니다. 마지막 cursor 5와 false는 정상 종료입니다. |
| 4 | cursor 0→1 |
| 8 | index 1의 20 반환 |
| 11 | cursor 2→3 |
| 15 | index 3의 40 반환 |
| 18 | cursor 4→5 |
| 구분 | 내용 |
|---|---|
| 설명 | 원 Broken의 다섯 호출을 실제 순서로 연결합니다. hasNext가 0→1과 2→3으로 전진하여 10과 30을 건너뛰고 next는 20·40을 반환합니다. 마지막 cursor 5와 false는 정상 종료입니다. |
| 전체 읽기 정보 | 잘못된 hasNext가 10과 30을 건너뛴다 잘못된 hasNext가 10과 30을 건너뛴다 원문 main의 다섯 호출을 따라가면 hasNext와 next가 모두 커서를 증가시켜 20과 40을 출력하고 정상 종료합니다. 각 호출 전후 cursor는 코드에서 추적한 상태입니다. ITERATOR · 잘못된 커서 이동 잘못된 hasNext가 10과 30을 건너뛴다 hasNext와 next가 모두 커서를 증가시키면, 출력은 20·40만 남습니다. 잘못된 hasNext가 10과 30을 건너뛴다 실제 호출 순서 cursor 전 · 코드 추적 반환·결과 cursor 후 · 코드 추적 hasNext() 1 0 true 1 next() 1 1 20 출력 2 hasNext() 2 2 true 3 next() 2 3 40 출력 4 hasNext() 3 4 false · 정상 종료 5 hasNext() 1 cursor 전 · 코드 추적 0 반환·결과 true cursor 후 · 코드 추적 1 next() 1 cursor 전 · 코드 추적 1 반환·결과 20 출력 cursor 후 · 코드 추적 2 hasNext() 2 cursor 전 · 코드 추적 2 반환·결과 true cursor 후 · 코드 추적 3 next() 2 cursor 전 · 코드 추적 3 반환·결과 40 출력 cursor 후 · 코드 추적 4 hasNext() 3 cursor 전 · 코드 추적 4 반환·결과 false · 정상 종료 cursor 후 · 코드 추적 5 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 질문/읽기 hasNext가 10을 건너뛰고 next가 20을 반환합니다. |
|
|
| 둘째 질문/읽기 다시 질문이 30을 건너뛰고 next가 40을 반환합니다. |
|
|
| 마지막 질문 커서 4→5로 움직이고 false가 됩니다. |
|
|
잘못된 hasNext가 10과 30을 건너뛴다
hasNext가 커서를 잘못 전진시켜 10·30을 건너뛰고 20·40만 반환합니다. 다섯 호출의 cursor 변화가 계약 위반의 직접 원인입니다.
원문 main의 다섯 호출을 따라가면 hasNext와 next가 모두 커서를 증가시켜 20과 40을 출력하고 정상 종료합니다. 각 호출 전후 cursor는 코드에서 추적한 상태입니다.
ITERATOR · 잘못된 커서 이동
잘못된 hasNext가 10과 30을 건너뛴다
hasNext와 next가 모두 커서를 증가시키면, 출력은 20·40만 남습니다.
실선: 원 다섯 호출 순서질문과 반환이 모두 전진20·40만 stdout에 출력
| 실제 호출 순서 | cursor 전 · 코드 추적 | 반환·결과 | cursor 후 · 코드 추적 |
|---|---|---|---|
hasNext() 1 | 0 | true | 1 |
next() 1 | 1 | 20 출력 | 2 |
hasNext() 2 | 2 | true | 3 |
next() 2 | 3 | 40 출력 | 4 |
hasNext() 3 | 4 | false · 정상 종료 | 5 |
hasNext()1- cursor 전 · 코드 추적
0- 반환·결과
true- cursor 후 · 코드 추적
1
next()1- cursor 전 · 코드 추적
1- 반환·결과
20출력- cursor 후 · 코드 추적
2
hasNext()2- cursor 전 · 코드 추적
2- 반환·결과
true- cursor 후 · 코드 추적
3
next()2- cursor 전 · 코드 추적
3- 반환·결과
40출력- cursor 후 · 코드 추적
4
hasNext()3- cursor 전 · 코드 추적
4- 반환·결과
false· 정상 종료- cursor 후 · 코드 추적
5
이 예의 조건
- 잘못된 hasNext가 커서를 전진시키는 원 호출 순서이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원 Broken의 다섯 호출을 실제 순서로 연결합니다. hasNext가 0→1과 2→3으로 전진하여 10과 30을 건너뛰고 next는 20·40을 반환합니다. 마지막 cursor 5와 false는 정상 종료입니다. |
| 4 | cursor 0→1 |
| 8 | index 1의 20 반환 |
| 11 | cursor 2→3 |
| 15 | index 3의 40 반환 |
| 18 | cursor 4→5 |
| 구분 | 내용 |
|---|---|
| 설명 | 원 Broken의 다섯 호출을 실제 순서로 연결합니다. hasNext가 0→1과 2→3으로 전진하여 10과 30을 건너뛰고 next는 20·40을 반환합니다. 마지막 cursor 5와 false는 정상 종료입니다. |
| 전체 읽기 정보 | 잘못된 hasNext가 10과 30을 건너뛴다 잘못된 hasNext가 10과 30을 건너뛴다 원문 main의 다섯 호출을 따라가면 hasNext와 next가 모두 커서를 증가시켜 20과 40을 출력하고 정상 종료합니다. 각 호출 전후 cursor는 코드에서 추적한 상태입니다. ITERATOR · 잘못된 커서 이동 잘못된 hasNext가 10과 30을 건너뛴다 hasNext와 next가 모두 커서를 증가시키면, 출력은 20·40만 남습니다. 잘못된 hasNext가 10과 30을 건너뛴다 실제 호출 순서 cursor 전 · 코드 추적 반환·결과 cursor 후 · 코드 추적 hasNext() 1 0 true 1 next() 1 1 20 출력 2 hasNext() 2 2 true 3 next() 2 3 40 출력 4 hasNext() 3 4 false · 정상 종료 5 hasNext() 1 cursor 전 · 코드 추적 0 반환·결과 true cursor 후 · 코드 추적 1 next() 1 cursor 전 · 코드 추적 1 반환·결과 20 출력 cursor 후 · 코드 추적 2 hasNext() 2 cursor 전 · 코드 추적 2 반환·결과 true cursor 후 · 코드 추적 3 next() 2 cursor 전 · 코드 추적 3 반환·결과 40 출력 cursor 후 · 코드 추적 4 hasNext() 3 cursor 전 · 코드 추적 4 반환·결과 false · 정상 종료 cursor 후 · 코드 추적 5 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 질문/읽기 hasNext가 10을 건너뛰고 next가 20을 반환합니다. |
|
|
| 둘째 질문/읽기 다시 질문이 30을 건너뛰고 next가 40을 반환합니다. |
|
|
| 마지막 질문 커서 4→5로 움직이고 false가 됩니다. |
|
|
첫 hasNext가 커서를 1로 만들고 next는 index 1의 20을 반환한 뒤 2가 됩니다.
질문 횟수에 따라 결과가 달라지는 것은 Iterator 규칙 위반입니다.
hasNext를 두 번 연속 호출해도 다음 값은 같아야 합니다.
원 Broken에 10·20·30·40을 넣은 새 반복자에서 hasNext를 두 번 연속 부르면 둘 다 true이지만 cursor는 0→1→2가 됩니다. 그 뒤 next는 10이 아니라 30을 반환합니다. 질문만 추가했는데 소비 결과가 달라집니다. 올바른 반복자라면 두 질문 뒤에도 첫 next가 10이어야 합니다. 원 main의 20·40 출력과 이 별도 호출 순서를 섞지 않고 검산하세요.
cursor는 다음에 반환할 원소의 위치
초기 커서 0은 아직 아무 값도 반환하지 않았다는 뜻입니다.
hasNext는 cursor < size만 계산합니다.
next는 먼저 hasNext를 확인하고 현재 값을 반환한 뒤 커서를 하나 늘립니다.
끝에서 next를 직접 부르면 NoSuchElementException을 던집니다.
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
public final class IterableArrayList {
public static void main(String[] args) {
MyArrayList<String> titles = new MyArrayList<>();
titles.add("iterator");
titles.add("iterable");
titles.add("for-each");
for (String title : titles) {
System.out.println(title);
}
Iterator<String> first = titles.iterator(), second = titles.iterator();
System.out.println("independent=" + first.next() + "/" + second.next());
}
private static final class MyArrayList<E> implements Iterable<E> {
private Object[] values = new Object[2];
private int size;
void add(E value) {
if (size == values.length) values = Arrays.copyOf(values, size * 2);
values[size++] = value;
}
@SuppressWarnings("unchecked")
E get(int index) {
if (index < 0 || index >= size) throw new IndexOutOfBoundsException(index);
return (E) values[index];
}
public Iterator<E> iterator() {
return new Iterator<>() {
private int cursor;
public boolean hasNext() {
return cursor < size;
}
public E next() {
if (!hasNext()) throw new NoSuchElementException();
return get(cursor++);
}
};
}
}
}세 값 순회 뒤 first와 second는 각각 처음에서 시작한다
같은 목록에서 for-each와 별도 두 Iterator의 커서가 독립 전진합니다. 도구를 고정하고 각 호출의 cursor 변화만 보여 주면 저장값·커서 분리가 선명합니다.
향상된 for문은 iterator, iterable, for-each를 순서대로 출력합니다. 그 뒤 새로 만든 first와 second는 각각 cursor 0에서 iterator를 반환합니다. 커서 상태는 코드 추적입니다.
ITERABLE · 독립 커서
세 값 순회 뒤 first와 second는 각각 처음에서 시작한다
for-each의 커서가 끝나도 새 반복자 first와 second는 각각 첫 원소에서 시작합니다.
| 반복자·실제 next 호출 | cursor 전 · 코드 추적 | 반환값 | cursor 후 · 코드 추적 |
|---|---|---|---|
| for-each · 1 | 0 | iterator | 1 |
| for-each · 2 | 1 | iterable | 2 |
| for-each · 3 | 2 | for-each | 3 |
first.next() | 0 | iterator | 1 |
second.next() | 0 | iterator | 1 |
- for-each · 1
- cursor 전 · 코드 추적
0- 반환값
iterator- cursor 후 · 코드 추적
1
- for-each · 2
- cursor 전 · 코드 추적
1- 반환값
iterable- cursor 후 · 코드 추적
2
- for-each · 3
- cursor 전 · 코드 추적
2- 반환값
for-each- cursor 후 · 코드 추적
3
first.next()- cursor 전 · 코드 추적
0- 반환값
iterator- cursor 후 · 코드 추적
1
second.next()- cursor 전 · 코드 추적
0- 반환값
iterator- cursor 후 · 코드 추적
1
이 예의 조건
- 세 순회 도구는 같은 세 값을 별도 커서로 읽는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 세 영역은 for-each, first, second의 별도 Iterator입니다. 각각 cursor 0에서 시작하며 first.next와 second.next가 모두 문자열 iterator를 반환합니다. 소진된 순회는 목록을 비우거나 다른 커서를 옮기지 않습니다. |
| 구분 | 내용 |
|---|---|
| 설명 | 세 영역은 for-each, first, second의 별도 Iterator입니다. 각각 cursor 0에서 시작하며 first.next와 second.next가 모두 문자열 iterator를 반환합니다. 소진된 순회는 목록을 비우거나 다른 커서를 옮기지 않습니다. |
| 전체 읽기 정보 | 세 값 순회 뒤 first와 second는 각각 처음에서 시작한다 세 값 순회 뒤 first와 second는 각각 처음에서 시작한다 향상된 for문은 iterator, iterable, for-each를 순서대로 출력합니다. 그 뒤 새로 만든 first와 second는 각각 cursor 0에서 iterator를 반환합니다. 커서 상태는 코드 추적입니다. ITERABLE · 독립 커서 세 값 순회 뒤 first와 second는 각각 처음에서 시작한다 for-each의 커서가 끝나도 새 반복자 first와 second는 각각 첫 원소에서 시작합니다. 세 값 순회 뒤 first와 second는 각각 처음에서 시작한다 반복자·실제 next 호출 cursor 전 · 코드 추적 반환값 cursor 후 · 코드 추적 for-each · 1 0 iterator 1 for-each · 2 1 iterable 2 for-each · 3 2 for-each 3 first.next() 0 iterator 1 second.next() 0 iterator 1 for-each · 1 cursor 전 · 코드 추적 0 반환값 iterator cursor 후 · 코드 추적 1 for-each · 2 cursor 전 · 코드 추적 1 반환값 iterable cursor 후 · 코드 추적 2 for-each · 3 cursor 전 · 코드 추적 2 반환값 for-each cursor 후 · 코드 추적 3 first.next() cursor 전 · 코드 추적 0 반환값 iterator cursor 후 · 코드 추적 1 second.next() cursor 전 · 코드 추적 0 반환값 iterator cursor 후 · 코드 추적 1 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 독립 시작 for-each/first/second가 각각 cursor 0을 만듭니다. |
|
|
| 각 소비 for-each는 세 값을 읽고 first/second는 각각 첫 값을 읽습니다. |
|
|
| 저장 보존 목록과 독립 두 커서 상태를 확인합니다. |
|
|
세 값 순회 뒤 first와 second는 각각 처음에서 시작한다
같은 목록에서 for-each와 별도 두 Iterator의 커서가 독립 전진합니다. 도구를 고정하고 각 호출의 cursor 변화만 보여 주면 저장값·커서 분리가 선명합니다.
향상된 for문은 iterator, iterable, for-each를 순서대로 출력합니다. 그 뒤 새로 만든 first와 second는 각각 cursor 0에서 iterator를 반환합니다. 커서 상태는 코드 추적입니다.
ITERABLE · 독립 커서
세 값 순회 뒤 first와 second는 각각 처음에서 시작한다
for-each의 커서가 끝나도 새 반복자 first와 second는 각각 첫 원소에서 시작합니다.
| 반복자·실제 next 호출 | cursor 전 · 코드 추적 | 반환값 | cursor 후 · 코드 추적 |
|---|---|---|---|
| for-each · 1 | 0 | iterator | 1 |
| for-each · 2 | 1 | iterable | 2 |
| for-each · 3 | 2 | for-each | 3 |
first.next() | 0 | iterator | 1 |
second.next() | 0 | iterator | 1 |
- for-each · 1
- cursor 전 · 코드 추적
0- 반환값
iterator- cursor 후 · 코드 추적
1
- for-each · 2
- cursor 전 · 코드 추적
1- 반환값
iterable- cursor 후 · 코드 추적
2
- for-each · 3
- cursor 전 · 코드 추적
2- 반환값
for-each- cursor 후 · 코드 추적
3
first.next()- cursor 전 · 코드 추적
0- 반환값
iterator- cursor 후 · 코드 추적
1
second.next()- cursor 전 · 코드 추적
0- 반환값
iterator- cursor 후 · 코드 추적
1
이 예의 조건
- 세 순회 도구는 같은 세 값을 별도 커서로 읽는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 세 영역은 for-each, first, second의 별도 Iterator입니다. 각각 cursor 0에서 시작하며 first.next와 second.next가 모두 문자열 iterator를 반환합니다. 소진된 순회는 목록을 비우거나 다른 커서를 옮기지 않습니다. |
| 구분 | 내용 |
|---|---|
| 설명 | 세 영역은 for-each, first, second의 별도 Iterator입니다. 각각 cursor 0에서 시작하며 first.next와 second.next가 모두 문자열 iterator를 반환합니다. 소진된 순회는 목록을 비우거나 다른 커서를 옮기지 않습니다. |
| 전체 읽기 정보 | 세 값 순회 뒤 first와 second는 각각 처음에서 시작한다 세 값 순회 뒤 first와 second는 각각 처음에서 시작한다 향상된 for문은 iterator, iterable, for-each를 순서대로 출력합니다. 그 뒤 새로 만든 first와 second는 각각 cursor 0에서 iterator를 반환합니다. 커서 상태는 코드 추적입니다. ITERABLE · 독립 커서 세 값 순회 뒤 first와 second는 각각 처음에서 시작한다 for-each의 커서가 끝나도 새 반복자 first와 second는 각각 첫 원소에서 시작합니다. 세 값 순회 뒤 first와 second는 각각 처음에서 시작한다 반복자·실제 next 호출 cursor 전 · 코드 추적 반환값 cursor 후 · 코드 추적 for-each · 1 0 iterator 1 for-each · 2 1 iterable 2 for-each · 3 2 for-each 3 first.next() 0 iterator 1 second.next() 0 iterator 1 for-each · 1 cursor 전 · 코드 추적 0 반환값 iterator cursor 후 · 코드 추적 1 for-each · 2 cursor 전 · 코드 추적 1 반환값 iterable cursor 후 · 코드 추적 2 for-each · 3 cursor 전 · 코드 추적 2 반환값 for-each cursor 후 · 코드 추적 3 first.next() cursor 전 · 코드 추적 0 반환값 iterator cursor 후 · 코드 추적 1 second.next() cursor 전 · 코드 추적 0 반환값 iterator cursor 후 · 코드 추적 1 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 독립 시작 for-each/first/second가 각각 cursor 0을 만듭니다. |
|
|
| 각 소비 for-each는 세 값을 읽고 first/second는 각각 첫 값을 읽습니다. |
|
|
| 저장 보존 목록과 독립 두 커서 상태를 확인합니다. |
|
|
위 Iterable을 대상으로 한 향상된 for문은 컴파일 과정에서 반복자 획득, hasNext 검사, next 반환 구조로 바뀝니다.
배열 목록의 get을 사용하므로 전체 순회는 O(n)입니다.
연결 목록에서 index get으로 반복자를 만들면 매 원소마다 head부터 걸어 O(n²)이 될 수 있으므로 Iterator가 current Node를 직접 가져야 합니다.
원소 n개를 get(0), get(1), …, get(n−1)로 읽으면 연결 이동 수는 0+1+…+(n−1)=n(n−1)/2입니다. 세 원소에서 0+1+2=3번이지만 1,000개에서는 499,500번입니다. current를 한 번씩 next로 옮기면 각 노드의 다음 참조만 한 번 따라갑니다. 이 수는 소스에서 센 이동 수이며 실행시간 측정값은 아닙니다.
연결 목록 반복자의 Node 방문
current는 다음에 반환할 Node를 가리킵니다.
hasNext는 current가 null인지 보고, next는 값을 보존한 뒤 current = current.next로 이동합니다.
목록의 size나 index를 몰라도 null 종단까지 선형 순회합니다.
import java.util.Iterator;
import java.util.NoSuchElementException;
public final class LinkedNodeIterator {
public static void main(String[] args) {
Chain<Integer> chain = new Chain<>();
chain.addLast(30);
chain.addLast(40);
chain.addLast(50);
int total = 0;
for (int value : chain) {
total += value;
}
System.out.println("total=" + total);
}
private static final class Chain<E> implements Iterable<E> {
private Node<E> head, tail;
void addLast(E value) {
Node<E> n = new Node<>(value);
if (tail == null) head = tail = n;
else {
tail.next = n;
tail = n;
}
}
public Iterator<E> iterator() {
return new Iterator<>() {
private Node<E> current = head;
public boolean hasNext() {
return current != null;
}
public E next() {
if (current == null) throw new NoSuchElementException();
E value = current.value;
current = current.next;
return value;
}
};
}
}
private static final class Node<E> {
final E value;
Node<E> next;
Node(E value) {
this.value = value;
}
}
}current는 30·40·50을 지나 null에 도달한다
next마다 current가 30→40→50→null로 이동하며 total 120을 만듭니다. 고정 next 구조에서 커서·반환값을 단계로 추적합니다.
연결 목록의 next는 현재 노드의 값을 반환하고 반복자의 current를 다음 노드로 옮깁니다. 마지막 값 50 뒤에는 current가 null이 되어 순회가 끝납니다. 노드 상태는 코드 추적이며 실제 출력은 total=120입니다.
ITERATOR · 연결 노드
current는 30·40·50을 지나 null에 도달한다
next는 현재 노드의 값을 반환한 뒤 다음 노드로 이동합니다. 50 이후에는 current가 null입니다.
실선: Node.next 참조점선: 초기 current 참조next마다 current를 다음으로반환30+40+50=total120
next()1- current 전 · 코드 추적
- 값
30의 노드 - 반환·판정
30- current 후 · 코드 추적
- 값
40의 노드
next()2- current 전 · 코드 추적
- 값
40의 노드 - 반환·판정
40- current 후 · 코드 추적
- 값
50의 노드
next()3- current 전 · 코드 추적
- 값
50의 노드 - 반환·판정
50- current 후 · 코드 추적
null
마지막
hasNext()- current 전 · 코드 추적
null- 반환·판정
false· 순회 종료- current 후 · 코드 추적
null
이 예의 조건
- null은 노드 객체가 아닌 종단 참조이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 실선은 30→40→50→null의 Node.next 참조이며 점선은 처음 current가 30의 Node를 가리키는 참조입니다. next마다 current를 다음 Node로 옮겨 세 값을 반환하고 total 120입니다. null은 노드가 아닙니다. |
| 18 | 반환 30+40+50=total 120 |
| 구분 | 내용 |
|---|---|
| 설명 | 실선은 30→40→50→null의 Node.next 참조이며 점선은 처음 current가 30의 Node를 가리키는 참조입니다. next마다 current를 다음 Node로 옮겨 세 값을 반환하고 total 120입니다. null은 노드가 아닙니다. |
| 전체 읽기 정보 | current는 30·40·50을 지나 null에 도달한다 current는 30·40·50을 지나 null에 도달한다 연결 목록의 next는 현재 노드의 값을 반환하고 반복자의 current를 다음 노드로 옮깁니다. 마지막 값 50 뒤에는 current가 null이 되어 순회가 끝납니다. 노드 상태는 코드 추적이며 실제 출력은 total=120입니다. ITERATOR · 연결 노드 current는 30·40·50을 지나 null에 도달한다 next는 현재 노드의 값을 반환한 뒤 다음 노드로 이동합니다. 50 이후에는 current가 null입니다. next() 1 current 전 · 코드 추적 값 30 의 노드 반환·판정 30 current 후 · 코드 추적 값 40 의 노드 next() 2 current 전 · 코드 추적 값 40 의 노드 반환·판정 40 current 후 · 코드 추적 값 50 의 노드 next() 3 current 전 · 코드 추적 값 50 의 노드 반환·판정 50 current 후 · 코드 추적 null 마지막 hasNext() current 전 · 코드 추적 null 반환·판정 false · 순회 종료 current 후 · 코드 추적 null |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 next current 30에서 값을 반환하고 40으로 이동합니다. |
|
|
| 둘째 next 40을 반환하고 50으로 이동합니다. |
|
|
| 끝 소비 50을 반환한 뒤 null에 도달합니다. |
|
|
current는 30·40·50을 지나 null에 도달한다
next마다 current가 30→40→50→null로 이동하며 total 120을 만듭니다. 고정 next 구조에서 커서·반환값을 단계로 추적합니다.
연결 목록의 next는 현재 노드의 값을 반환하고 반복자의 current를 다음 노드로 옮깁니다. 마지막 값 50 뒤에는 current가 null이 되어 순회가 끝납니다. 노드 상태는 코드 추적이며 실제 출력은 total=120입니다.
ITERATOR · 연결 노드
current는 30·40·50을 지나 null에 도달한다
next는 현재 노드의 값을 반환한 뒤 다음 노드로 이동합니다. 50 이후에는 current가 null입니다.
실선: Node.next 참조점선: 초기 current 참조next마다 current를 다음으로반환30+40+50=total120
next()1- current 전 · 코드 추적
- 값
30의 노드 - 반환·판정
30- current 후 · 코드 추적
- 값
40의 노드
next()2- current 전 · 코드 추적
- 값
40의 노드 - 반환·판정
40- current 후 · 코드 추적
- 값
50의 노드
next()3- current 전 · 코드 추적
- 값
50의 노드 - 반환·판정
50- current 후 · 코드 추적
null
마지막
hasNext()- current 전 · 코드 추적
null- 반환·판정
false· 순회 종료- current 후 · 코드 추적
null
이 예의 조건
- null은 노드 객체가 아닌 종단 참조이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 실선은 30→40→50→null의 Node.next 참조이며 점선은 처음 current가 30의 Node를 가리키는 참조입니다. next마다 current를 다음 Node로 옮겨 세 값을 반환하고 total 120입니다. null은 노드가 아닙니다. |
| 18 | 반환 30+40+50=total 120 |
| 구분 | 내용 |
|---|---|
| 설명 | 실선은 30→40→50→null의 Node.next 참조이며 점선은 처음 current가 30의 Node를 가리키는 참조입니다. next마다 current를 다음 Node로 옮겨 세 값을 반환하고 total 120입니다. null은 노드가 아닙니다. |
| 전체 읽기 정보 | current는 30·40·50을 지나 null에 도달한다 current는 30·40·50을 지나 null에 도달한다 연결 목록의 next는 현재 노드의 값을 반환하고 반복자의 current를 다음 노드로 옮깁니다. 마지막 값 50 뒤에는 current가 null이 되어 순회가 끝납니다. 노드 상태는 코드 추적이며 실제 출력은 total=120입니다. ITERATOR · 연결 노드 current는 30·40·50을 지나 null에 도달한다 next는 현재 노드의 값을 반환한 뒤 다음 노드로 이동합니다. 50 이후에는 current가 null입니다. next() 1 current 전 · 코드 추적 값 30 의 노드 반환·판정 30 current 후 · 코드 추적 값 40 의 노드 next() 2 current 전 · 코드 추적 값 40 의 노드 반환·판정 40 current 후 · 코드 추적 값 50 의 노드 next() 3 current 전 · 코드 추적 값 50 의 노드 반환·판정 50 current 후 · 코드 추적 null 마지막 hasNext() current 전 · 코드 추적 null 반환·판정 false · 순회 종료 current 후 · 코드 추적 null |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 next current 30에서 값을 반환하고 40으로 이동합니다. |
|
|
| 둘째 next 40을 반환하고 50으로 이동합니다. |
|
|
| 끝 소비 50을 반환한 뒤 null에 도달합니다. |
|
|
Iterator의 일회성
이 장의 iterator() 구현은 호출마다 독립적인 커서를 가진 새 Iterator를 반환합니다.
Iterator 자체를 필드 하나로 저장해 매번 같은 객체를 반환하면 첫 순회 뒤 두 번째 순회가 빈 결과가 됩니다.
동시에 중첩 순회할 수도 없습니다.
Iterator의 remove는 선택 기능입니다.
지원하지 않으면 기본 구현이 UnsupportedOperationException을 던집니다.
직접 구현에서 remove를 지원하려면 마지막 반환 위치와 한 번의 next 뒤 한 번만 제거할 수 있다는 규칙을 관리해야 합니다. 즉시 실패 정책까지 선택한다면 modCount 같은 변경 감지도 추가합니다.
이번 반복자는 읽기 순회만 제공하며, 순회 중 목록 변경을 감지하는 정책은 구현하지 않습니다.
순회 중 컬렉션이 바뀔 때 스냅샷, 실시간 뷰, 즉시 실패 중 어떤 의미인지도 규칙입니다.
표준 컬렉션의 즉시 실패는 버그 진단을 돕지만 동시성 안전 보장은 아닙니다.
게시글 저장소와 Iterable 의존
import java.util.List;
public final class IterableBoardSummary {
public static void main(String[] args) {
Iterable<Entry> entries = List.of(new Entry("iterator", 45), new Entry("for-each", 55));
System.out.println("total=" + total(entries));
}
static int total(Iterable<Entry> entries) {
int sum = 0;
for (Entry e : entries) {
sum += e.viewCount();
}
return sum;
}
private record Entry(String title, int viewCount) {}
}total은 List의 add·get·size를 요구하지 않습니다.
값을 한 번씩 읽는 최소 역할 Iterable만 받으므로 배열·연결·지연 생성 순회로 바꿀 여지가 있습니다.
연습 문제
3부터 7까지 정수를 포함해 순회하는 Range를 구현하세요.
Range 객체를 두 번 for-each해도 매번 같은 다섯 값이 나와야 합니다.
먼저 각 순회의 출력 3·4·5·6·7과 끝에서 hasNext=false, next 예외를 검사 대상으로 적으세요. 두 번째 순회는 첫 Iterator를 재사용하는 것이 아니라 새 Iterator에서 시작합니다. 해설의 3~7 입력을 완성한 뒤 아래 int 상한 경계를 따로 확인합니다.
정답과 해설
Range는 불변 경계만 보관하고 커서는 각 익명 Iterator 안에 둡니다.
그래서 반복자끼리 독립적입니다.
import java.util.Iterator;
import java.util.NoSuchElementException;
public final class RangeIteratorSolution {
public static void main(String[] args) {
Range r = new Range(3, 7);
for (int v : r) {
System.out.print(v);
}
System.out.print('/');
for (int v : r) {
System.out.print(v);
}
}
private record Range(int start, int end) implements Iterable<Integer> {
Range {
if (start > end) throw new IllegalArgumentException();
}
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int cursor = start;
public boolean hasNext() {
return cursor <= end;
}
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
return cursor++;
}
};
}
}
}출력은 34567/34567입니다.
hasNext를 여러 번 호출해도 커서는 next에서만 변합니다.
해설의 Range는 3~7에서는 정상입니다. 다만 현재 생성자는 start≤end만 검사하므로 end=Integer.MAX_VALUE도 받습니다. 이 상한을 반환한 다음 cursor++가 Integer.MIN_VALUE로 돌아가면 cursor≤end는 다시 true입니다. 소진을 “끝보다 1 큰 int”로만 표시할 수 없는 경계입니다. 해당 범위를 향상된 for문으로 끝까지 돌리지 말고, 다음처럼 마지막 값을 반환했는지 따로 보관하세요.
예시
import java.util.Iterator;
import java.util.NoSuchElementException;
public final class ExhaustionSafeRange {
public static void main(String[] args) {
Iterator<Integer> values = new Range(Integer.MAX_VALUE - 1, Integer.MAX_VALUE).iterator();
System.out.println(values.next());
System.out.println(values.next());
System.out.println("has-next=" + values.hasNext());
try {
values.next();
} catch (NoSuchElementException expected) {
System.out.println("exhausted");
}
}
private record Range(int start, int end) implements Iterable<Integer> {
Range {
if (start > end) throw new IllegalArgumentException();
}
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int cursor = start;
private boolean remaining = true;
public boolean hasNext() { return remaining; }
public Integer next() {
if (!remaining) throw new NoSuchElementException();
int value = cursor;
if (cursor == end) remaining = false;
else cursor++;
return value;
}
};
}
}
}출력은 2147483646, 2147483647, has-next=false, exhausted입니다. 마지막 값에서는 cursor를 더하지 않으므로 넘침이 없고, 각 Iterator의 remaining도 독립적입니다. 같은 방식으로 start=end인 한 원소 범위도 한 번 반환한 뒤 끝납니다. 원 해설 코드를 덮어쓴 것이 아니라 허용 경계를 넓혔을 때 필요한 별도 구현입니다.
이 예제의 Iterator를 검산할 때는 hasNext의 커서 전진 없음, next의 단일 전진, 끝 예외, 반복자 호출별 독립 커서를 확인합니다.
저장 구조별로 효율적인 전진 방법을 선택하면 같은 소비 코드가 선형 순회를 유지합니다.