1. Iterable 인터페이스
Java에서 거의 모든 컬렉션(Map 제외)은 인터페이스 Iterable을 구현합니다. 이는 내부 구조의 세부사항에 신경 쓰지 않고 요소를 하나씩 순서대로 순회할 수 있다는 뜻입니다. 프로그래머 관점에서 보면 “컬렉션에는 모든 요소를 순회하는 내장 방법이 있다”로 이해할 수 있습니다.
인터페이스 Iterable은 단 하나의 메서드를 정의합니다:
Iterator<E> iterator();
메서드 iterator()는 컬렉션을 한 걸음씩 순회하는 방법을 아는 Iterator 타입의 객체—즉 “도우미”—를 반환합니다. 덕분에 우리가 익숙한 for-each 루프가 동작합니다:
for (ElementType e : collection) {
// ...
}
— 바로 그 Iterator가 무대 뒤에서 숨겨져 있습니다. 추가 장점으로, 순회 중에 remove()로 요소를 안전하게 삭제할 수 있습니다. 일반적인 방식으로 제거하려고 하면 ConcurrentModificationException이 쉽게 발생합니다.
2. Iterator 인터페이스
Iterator는 요소를 건너뛰지 않고 컬렉션의 순서에 맞춰 이동하는 “배달원”과 같습니다.
| 메서드 | 설명 |
|---|---|
|
더 순회할 요소가 있는가? |
|
다음 요소를 반환하고 그 위치로 이동 |
|
현재 요소를 안전하게 삭제(오류 없이) |
Iterator로 컬렉션 순회 예시
import java.util.*;
public class IteratorDemo {
public static void main(String[] args) {
List<String> tasks = new ArrayList<>();
tasks.add("고양이 쓰다듬기");
tasks.add("숙제하기");
tasks.add("드라마 보기");
Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
String task = it.next();
System.out.println("작업: " + task);
}
}
}
여기서 무슨 일이 일어나나요?
- tasks.iterator()로 이터레이터를 얻습니다.
- 다음 요소가 있는 동안(hasNext()가 true를 반환), next()로 가져와 출력합니다.
- 이터레이터가 순회 순서를 스스로 관리하므로 컬렉션이 내부에서 요소를 어떻게 저장하는지 알 필요가 없습니다.
3. 루프가 있는데 왜 Iterator가 필요할까?
Iterator를 사용하면 인덱스가 없는 컬렉션(예: Set)도 순회할 수 있습니다. 컬렉션의 구체적 타입에 의존하지 않는 범용적인 방법입니다.
요소를 안전하게 삭제하기
흔한 작업으로, 컬렉션을 순회하면서 몇몇 요소를 삭제하고 싶을 때가 있습니다. 이를 for-each로 하면 오류가 발생할 수 있습니다:
for (String task : tasks) {
if (task.contains("고양이")) {
tasks.remove(task); // 쾅! ConcurrentModificationException
}
}
왜 이런 일이 생길까요? 이터레이터가 시작한 순회 중에 컬렉션의 구조가 직접 변경될 것이라고 컬렉션은 기대하지 않기 때문입니다.
올바른 방법:
Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
String task = it.next();
if (task.contains("고양이")) {
it.remove(); // 모든 것이 매끄럽게 진행됩니다!
}
}
왜 단순히 인덱스를 쓰면 안 될까?
모든 컬렉션에 인덱스가 있는 것은 아니기 때문입니다. 예를 들어 HashSet이나 TreeSet에는 “다섯 번째 요소”라는 개념이 없습니다. Iterator는 언제나 동작합니다 — 이것이 강점입니다.
4. for-each 자세히 보기
향상된 for 루프(for-each)는 Java 5에서 도입되었습니다. 본질적으로 요소를 아주 간단히 순회하게 해 주는 문법 설탕(syntactic sugar)입니다:
for (String task : tasks) {
System.out.println("작업: " + task);
}
내부적으로 컴파일러는 iterator()를 호출하고, hasNext()로 확인하며, next()로 요소를 꺼냅니다. 말 그대로 “목록의 각 작업에 대해”라고 읽힙니다.
for-each가 맞지 않는 경우
- 순회 중 요소를 삭제해야 하는 경우(for-each는 remove()를 직접 호출할 수 없습니다).
- 인덱스 접근이 필요한 경우(예: 특정 위치의 요소를 교체).
- 여러분이 Map을 다루는 경우 — Map은 “키-값” 쌍이므로 순회를 위한 별도의 로직이 필요합니다.
5. Map 순회: 요령과 주의점
인터페이스 Map은 Iterable을 직접 구현하지 않습니다. 이는 “키-값” 쌍의 집합이기 때문입니다. 그럼에도 Map은 순회를 위한 편리한 뷰를 제공합니다.
키 순회
Map<String, String> users = new HashMap<>();
users.put("vasya", "vasya@example.com");
users.put("petya", "petya@gmail.com");
for (String login : users.keySet()) {
System.out.println("로그인: " + login);
}
값 순회
for (String email : users.values()) {
System.out.println("Email: " + email);
}
쌍(키-값) 순회
가장 범용적인 방법은 entrySet()을 순회하는 것입니다:
for (Map.Entry<String, String> entry : users.entrySet()) {
System.out.println("로그인: " + entry.getKey() + ", Email: " + entry.getValue());
}
흥미로운 사실: Entry는 Map의 내부 인터페이스로, getKey()와 getValue() 메서드를 제공합니다. 이를 통해 한 번에 쌍의 양쪽을 모두 얻을 수 있습니다.
Iterator로 순회하기
Iterator<Map.Entry<String, String>> it = users.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
// 요소를 안전하게 삭제할 수도 있습니다:
if (entry.getKey().startsWith("v")) {
it.remove();
}
}
6. 실전 예시: 컬렉션 순회가 애플리케이션에 주는 도움
예시: 사용자의 모든 작업 출력
List<String> tasks = new ArrayList<>();
tasks.add("숙제하기");
tasks.add("고양이 쓰다듬기");
tasks.add("드라마 보기");
System.out.println("오늘의 작업:");
for (String task : tasks) {
System.out.println("- " + task);
}
이제 "고양이"라는 단어를 포함한 모든 작업을 삭제해 봅시다:
Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
String task = it.next();
if (task.contains("고양이")) {
it.remove();
}
}
System.out.println("남은 작업:");
for (String task : tasks) {
System.out.println("- " + task);
}
예시: Set으로 고유한 로그인 순회
Set<String> logins = new HashSet<>();
logins.add("vasya");
logins.add("petya");
logins.add("masha");
for (String login : logins) {
System.out.println("사용자: " + login);
}
주의: Set의 출력 순서는 임의일 수 있습니다!
예시: 사용자 표시를 위한 Map 순회
Map<String, String> users = new HashMap<>();
users.put("vasya", "vasya@example.com");
users.put("petya", "petya@gmail.com");
for (Map.Entry<String, String> entry : users.entrySet()) {
System.out.println("로그인: " + entry.getKey() + ", Email: " + entry.getValue());
}
7. Iterator.remove(): 요소를 안전하게 삭제하기
초보자가 가장 자주 범하는 실수 중 하나는 for-each로 순회하면서 컬렉션의 요소를 삭제하려는 것입니다. 이터레이터는 remove()로 이 문제를 해결합니다.
어떻게 동작하나요?
- it.remove()를 호출하면 현재 요소—마지막 next()가 반환한 요소—가 삭제됩니다.
- 이는 안전합니다. 컬렉션이 ConcurrentModificationException을 던지지 않습니다.
예시:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
int n = it.next();
if (n % 2 == 0) {
it.remove(); // 짝수는 모두 삭제
}
}
System.out.println(numbers); // [1, 3, 5]
컬렉션 순회 다이어그램
+---------+ +---------+ +---------+
| Element | --> | Element | --> | Element | ...
+---------+ +---------+ +---------+
^ ^
| |
next() next()
이터레이터는 hasNext()가 false를 반환할 때까지 요소를 “한 걸음씩” 이동합니다.
9. Iterator와 컬렉션 순회에서 흔한 실수
실수 №1: for-each 순회 중 컬렉션을 수정.
for-each 내부에서 요소를 직접 삭제하려 하면 ConcurrentModificationException이 발생합니다:
for (String task : tasks) {
if (task.contains("고양이")) {
tasks.remove(task); // 쾅! ConcurrentModificationException
}
}
Iterator와 그 remove()를 사용하세요.
실수 №2: next() 전에 remove() 호출.
먼저 next()로 현재 요소를 받아야 합니다. 그렇지 않으면 이터레이터는 무엇을 삭제해야 하는지 알 수 없습니다.
Iterator<String> it = tasks.iterator();
it.remove(); // 오류! 먼저 next()가 필요함
실수 №3: Map을 for-each로 직접 순회하려 함.
Map은 Iterable을 직접 구현하지 않습니다 — keySet(), values() 또는 entrySet()을 사용하세요.
Map<String, String> users = new HashMap<>();
// for (String entry : users) { ... } // 오류: 이렇게 하면 안 됨
for (Map.Entry<String, String> e : users.entrySet()) {
// 올바름
}
실수 №4: 이터레이터로 순회 중 이터레이터 밖에서 컬렉션 수정.
순회 중에는 요소를 컬렉션의 메서드가 아닌 it.remove()로만 삭제하세요.
Iterator<String> it = tasks.iterator();
while (it.hasNext()) {
String task = it.next();
if (task.contains("고양이")) {
tasks.remove(task); // 오류! it.remove()를 사용해야 함
}
}
GO TO FULL VERSION