Collection.stream (). forEach ()와 Collection.forEach ()의 차이점

1. 소개

Java의 컬렉션을 반복하는 몇 가지 옵션이 있습니다. 이 짧은 튜토리얼에서는 비슷한 두 가지 접근 방식 인 Collection.stream (). forEach ()Collection.forEach ()를 살펴볼 것 입니다.

대부분의 경우 둘 다 동일한 결과를 산출하지만 우리가 살펴볼 미묘한 차이가 있습니다.

2. 개요

먼저 반복 할 목록을 만들어 보겠습니다.

List list = Arrays.asList("A", "B", "C", "D");

가장 간단한 방법은 향상된 for 루프를 사용하는 것입니다.

for(String s : list) { //do something with s } 

함수형 자바를 사용하고 싶다면 forEach ()를 사용할 수도 있습니다 . 컬렉션에서 직접 그렇게 할 수 있습니다.

Consumer consumer = s -> { System.out::println }; list.forEach(consumer); 

또는 컬렉션의 스트림에서 forEach () 를 호출 할 수 있습니다 .

list.stream().forEach(consumer); 

두 버전 모두 목록을 반복하고 모든 요소를 ​​인쇄합니다.

ABCD ABCD

이 간단한 경우에는 우리가 사용 하는 forEach ()에 차이가 없습니다 .

3. 실행 순서

Collection.forEach () 는 컬렉션의 반복자를 사용합니다 (지정된 경우). 이는 항목의 처리 순서가 정의되었음을 의미합니다. 반대로 Collection.stream (). forEach () 의 처리 순서 는 정의되어 있지 않습니다.

대부분의 경우 우리가 선택하는 두 가지 중 어느 것을 선택하는지는 차이가 없습니다.

3.1. 병렬 스트림

병렬 스트림을 사용하면 여러 스레드에서 스트림을 실행할 수 있으며 이러한 상황에서는 실행 순서가 정의되지 않습니다. Java는 Collectors.toList () 와 같은 터미널 작업 이 호출 되기 전에 모든 스레드를 완료해야합니다 .

먼저 컬렉션에서 직접 forEach ()를 호출 하고 두 번째로 병렬 스트림에서 호출하는 예제를 살펴 보겠습니다 .

list.forEach(System.out::print); System.out.print(" "); list.parallelStream().forEach(System.out::print); 

코드를 여러 번 실행하면 list.forEach () 가 항목을 삽입 순서대로 처리하는 반면 list.parallelStream (). forEach () 는 실행할 때마다 다른 결과를 생성합니다.

가능한 출력은 다음과 같습니다.

ABCD CDBA

또 하나는 다음과 같습니다.

ABCD DBCA

3.2. 사용자 지정 반복자

컬렉션을 역순으로 반복하는 사용자 지정 반복기를 사용하여 목록을 정의 해 보겠습니다.

class ReverseList extends ArrayList { @Override public Iterator iterator() { int startIndex = this.size() - 1; List list = this; Iterator it = new Iterator() { private int currentIndex = startIndex; @Override public boolean hasNext() { return currentIndex >= 0; } @Override public String next() { String next = list.get(currentIndex); currentIndex--; return next; } @Override public void remove() { throw new UnsupportedOperationException(); } }; return it; } } 

목록을 반복 할 때 컬렉션에서 직접 forEach () 를 사용한 다음 스트림에서 :

List myList = new ReverseList(); myList.addAll(list); myList.forEach(System.out::print); System.out.print(" "); myList.stream().forEach(System.out::print); 

우리는 다른 결과를 얻습니다.

DCBA ABCD 

다른 결과의 이유 는 목록에서 직접 사용되는 forEach () 가 사용자 정의 반복기를 사용하는 반면 stream (). forEach () 는 반복기를 무시하고 목록에서 요소를 하나씩 가져 오기 때문입니다.

4. 컬렉션 수정

많은 컬렉션 (예 : ArrayList 또는 HashSet )을 반복하는 동안 구조적으로 수정해서는 안됩니다. 반복 중에 요소가 제거되거나 추가되면 ConcurrentModification 예외가 발생합니다.

또한 컬렉션은 오류가 발생하지 않도록 설계되었으므로 수정이 발생하는 즉시 예외가 발생합니다.

마찬가지로 스트림 파이프 라인을 실행하는 동안 요소를 추가하거나 제거하면 ConcurrentModification 예외가 발생합니다. 그러나 나중에 예외가 발생합니다.

forEach () 메서드 의 또 다른 미묘한 차이점은 Java가 반복기를 사용하여 요소를 명시 적으로 수정할 수 있다는 것입니다. 반대로 스트림은 방해가되지 않아야합니다.

요소 제거 및 수정에 대해 자세히 살펴 보겠습니다.

4.1. 요소 제거

목록의 마지막 요소 ( "D")를 제거하는 작업을 정의 해 보겠습니다.

Consumer removeElement = s -> { System.out.println(s + " " + list.size()); if (s != null && s.equals("A")) { list.remove("D"); } };

목록을 반복하면 첫 번째 요소 ( "A")가 인쇄 된 후 마지막 요소가 제거됩니다.

list.forEach(removeElement);

이후 대해 forEach ()가 르파, 우리는 반복하기를 중단하고 다음의 요소가 처리되기 전에 예외 참조 :

A 4 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList.forEach(ArrayList.java:1252) at ReverseList.main(ReverseList.java:1)

대신 stream (). forEach () 를 사용하면 어떻게되는지 보자 :

list.stream().forEach(removeElement);

여기에서 예외가 발생하기 전에 전체 목록을 계속 반복합니다.

A 4 B 3 C 3 null 3 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at ReverseList.main(ReverseList.java:1)

그러나 Java는 ConcurrentModificationException 이 전혀 발생 한다고 보장하지 않습니다 . 즉,이 예외에 의존하는 프로그램을 작성해서는 안됩니다.

4.2. 요소 변경

목록을 반복하는 동안 요소를 변경할 수 있습니다.

list.forEach(e -> { list.set(3, "E"); });

그러나 Collection.forEach () 또는 stream (). forEach ()를 사용하여이 작업을 수행하는 데 문제가 없지만 Java는 스트림에 대한 작업이 비 간섭 적이어야합니다. 즉, 스트림 파이프 라인 실행 중에 요소를 수정해서는 안됩니다.

The reason behind this is that the stream should facilitate parallel execution. Here, modifying elements of a stream could lead to unexpected behavior.

5. Conclusion

In this article, we saw some examples that show the subtle differences between Collection.forEach() and Collection.stream().forEach().

However, it's important to note that all the examples shown above are trivial and are merely meant to compare the two ways of iterating over a collection. We shouldn't write code whose correctness relies on the shown behavior.

If we don't require a stream but only want to iterate over a collection, the first choice should be using forEach() directly on the collection.

이 기사의 예제에 대한 소스 코드는 GitHub에서 사용할 수 있습니다.