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 인터페이스에 의해 확장되며, 컬렉션 계층의 모든 인터페이스에 의해 더 확장됩니다.

이 객체를 사용하여 컬렉션의 요소를 반복하는 작업은 두 단계로 이루어집니다.

  1. 먼저 hasNext() 메서드를 사용하여 방문해야 할 요소가 더 있는지 확인해야 합니다.
  2. 그런 다음 next() 메서드를 사용하여 다음 요소로 진행할 수 있습니다.

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
two

Iterator 인터페이스에는 세 번째 메서드가 있습니다: 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