for-each 패턴 사용
컬렉션의 요소를 반복하는 가장 간단한 방법은 각 패턴을 사용하는 것입니다.
Collection<String> strings = List.of("one", "two", "three");
for (String element: strings) {
System.out.println(element);
}이 코드를 실행하면 다음과 같은 결과가 생성됩니다:
one
two
three이 패턴은 컬렉션의 요소만 읽어야 하는 경우 매우 효율적입니다. Iterator 패턴을 사용하면 컬렉션의 요소를 반복하는 동안 일부 요소를 제거할 수 있습니다. 이 작업을 수행해야 하는 경우 Iterator 패턴을 사용하면 됩니다.
컬렉션에서 이터레이터 사용
컬렉션의 요소를 반복하려면 특수 객체, 즉 Iterator 인터페이스의 인스턴스를 사용합니다. Collection 인터페이스의 모든 확장으로부터 Iterator 객체를 가져올 수 있습니다. iterator() 메서드는 Iterable 인터페이스에 정의되어 있고, Collection 인터페이스에 의해 확장되며, 컬렉션 계층의 모든 인터페이스에 의해 더 확장됩니다.
이 객체를 사용하여 컬렉션의 요소를 반복하는 작업은 두 단계로 이루어집니다.
next() 메서드를 호출했지만 컬렉션에 더 이상 요소가 없는 경우 NoSuchElementException이 반환됩니다. hasNext()를 호출하는 것은 필수는 아니며, 다음 요소가 실제로 있는지 확인하는 데 도움이 됩니다.
패턴은 다음과 같습니다:
Collection<String> strings = List.of("one", "two", "three", "four");
for (Iterator<String> iterator = strings.iterator(); iterator.hasNext();) {
String element = iterator.next();
if (element.length() == 3) {
System.out.println(element);
}
}이 코드는 다음과 같은 결과를 생성합니다:
one
twoIterator 인터페이스에는 세 번째 메서드가 있습니다: remove(). 이 메서드를 호출하면 컬렉션에서 현재 요소가 제거됩니다. 하지만 이 메서드가 지원되지 않는 경우 UnsupportedOperationException이 발생합니다. 불변 컬렉션에서 remove()를 호출하면 작동하지 않는 경우가 여기에 해당합니다. ArrayList, LinkedList, HashSet에서 가져온 Iterator 구현은 모두 이 제거 연산을 지원합니다.
컬렉션을 반복하는 동안 컬렉션 업데이트하기
컬렉션을 반복하는 동안 컬렉션의 콘텐츠를 수정하는 경우 ConcurrentModificationException이 발생할 수 있습니다. 이 예외는 동시 프로그래밍에서도 사용되기 때문에 이 예외가 발생하면 약간 혼란스러울 수 있습니다. 컬렉션 프레임워크의 컨텍스트에서는 멀티스레드 프로그래밍을 건드리지 않고도 이 예외가 발생할 수 있습니다.
다음 코드는 ConcurrentModificationException을 던집니다.
Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
strings.add("three");
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
strings.remove(element);
}주어진 조건을 만족하는 컬렉션의 요소를 제거해야 하는 경우, removeIf() 메서드를 사용할 수 있습니다.
Iterable 인터페이스 구현
이제 컬렉션 프레임워크에서 이터레이터가 무엇인지 살펴보았으니, Iterable 인터페이스의 간단한 구현을 만들 수 있습니다.
두 한계 사이의 정수 범위를 모델링하는 Range 클래스를 만들어야 한다고 가정해 보겠습니다. 첫 번째 정수에서 마지막 정수까지 반복하기만 하면 됩니다.
Java SE 16에 도입된 기능인 레코드로 Iterable 인터페이스를 구현할 수 있습니다:
record Range(int start, int end) implements Iterable<Integer> {
@Override
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int index = start;
@Override
public boolean hasNext() {
return index < end;
}
@Override
public Integer next() {
if (index > end) {
throw new NoSuchElementException("" + index);
}
int currentIndex = index;
index++;
return currentIndex;
}
};
}
}애플리케이션이 아직 Java SE 16을 지원하지 않는 경우 일반 클래스로도 동일한 작업을 수행할 수 있습니다. 참고로 Iterator의 구현 코드는 완전히 동일합니다.
class Range implements Iterable<Integer> {
private final int start;
private final int end;
public Range(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int index = start;
@Override
public boolean hasNext() {
return index < end;
}
@Override
public Integer next() {
if (index > end) {
throw new NoSuchElementException("" + index);
}
int currentIndex = index;
index++;
return currentIndex;
}
};
}
}두 경우 모두 Iterable을 구현하므로 각 문에 Range의 인스턴스를 사용할 수 있습니다.:
for (int i : new Range(0, 5)) {
System.out.println("i = " + i);
}이 코드를 실행하면 다음과 같은 결과가 나타납니다:
i = 0
i = 1
i = 2
i = 3
i = 4