for loop vs stream forEach
Java8부터 Stream의 도입으로 기존의 코드를 보다 깔끔하고 가독성이 좋은 코드로 바꿀 수 있게 되었습니다.
Stream은 컬렉션 등의 요소를 하나씩 참조해 함수형 인터페이스(람다식)를 통해 반복적인 작업의 처리를 가능하게 해 줍니다. 반복적인 처리가 가능하므로, 반복문(for-loop 등)을 대신해 Stream을 사용하는 경우가 많습니다.
// for-loop
for (int i = 0; i < items.size(); i++) {
System.out.println(items.get(i));
}
// 향상된 for-Each
for (Iteam item : items)
System.out.println(item);
}
// stream.forEach()
items.stream().forEach(System.out::println);
위와 같이 기존 for-loop 방식에 비해 stream은 간결하고 가독성이 좋아 보입니다. 그렇다면 for-loop를 지양하고 stream만 써야 하는가?라고 생각이 들기도 합니다. 결론부터 말하면 아닙니다.
Stream은 강제로 종료할 수 없다.
Stream 자체를 강제적으로 종료시키는 방법은 존재하지 않습니다. 특정 조건에 부합하면 강제적으로 종료하는 로직이 있는 for-loop를 stream.forEach()로 구현하게 된다면 기존 for-loop에 비해 비효율적일 수 있습니다.
Item 배열을 순회하면서 조건이 만족될 경우 로직을 수행 후, loop를 종료하는 코드가 있다고 가정해 보겠습니다.
// for-loop
for (Item item : items) {
if (item.getName().equals("a")) {
item.setName("A");
break;
}
}
// Stream forEach
items.stream().forEach(item -> {
if (item.getName().equals("a")) {
item.setName("A");
return;
}
});
for-loop의 경우 해당 조건이 만족되면 break를 통해 다른 요소의 조건을 확인하지 않고 바로 종료되며
stream.forEach()는 각 수행에 대해 다음 수행을 막을 수 있을 뿐, 결국 모든 요소의 조건을 순회하고서 종료됩니다.
forEach를 사용하여 컬렉션을 반복할 때 상태를 수정하면 안된다.
Stream 병렬화에 대한 공식 문서의 Side-effects 항목을 참고하면, stream.forEach()는 쓰레드 안전성이 낮으며, 필요한 동기화를 추가하면 경합이 발생하여 동시성 문제가 발생할 수 있다고 경고합니다.
대부분의 컬렉션은 반복하는 동안 구조적으로 수정되어서는 안 됩니다. 반복 중에 요소가 제거되거나 추가되면 ConcurrentModification 예외가 발생합니다. 즉 수정이 발생하는 즉시 예외가 발생됩니다.
마찬가지로 Stream은 파이프라인 통해 실행 중에 요소를 추가하거나 제거하면 ConcurrentModification 예외가 발생합니다. 그러나 나중에 예외가 발생합니다.
두 forEach() 메서드 사이의 또 다른 차이점은 Java가 반복자를 사용하여 요소 수정을 명시적으로 허용한다는 것입니다. 대조적으로 Stream은 간섭하지 않아야 합니다.
목록을 정의하고, 마지막 요소 "D"를 제거하는 작업을 가정해 보겠습니다.
List<String> list = Arrays.asList("A", "B", "C", "D");
Consumer<String> removeElement = s -> {
System.out.println(s + " " + list.size());
if (s != null && s.equals("A")) {
list.remove("D");
}
};
Collection.forEach()는 다음과 같이 순회 중 다음 요소가 처리되기 전에 예외를 확인합니다.
list.forEach(removeElement);
A 4
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList.forEach(ArrayList.java:1252)
at ReverseList.main(ReverseList.java:1)
Collection.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)
thread-safe 하지 않은 stream().forEach()는 반복 도중에 다른 쓰레드에 의해 수정될 수 있고 바로 예외가 발생하지 않고 요소를 끝까지 반복되는 문제가 발생할 수 있습니다.
Stream forEach는 어떻게 사용해야 할까?
결론부터 말하자면 stream.forEach()의 올바른 사용법은 최종 연산으로써 사용해야 합니다.
IntStream.range(1, 10).forEach(i -> {
if (i > 5)
return;
System.out.println(i);
});
Stream의 중간연산인 filter, map, sort 등을 통해 필요한 로직을 수행 뒤 forEach()는 데이터를 다루기보단 출력용으로 사용하는 것이 적합합니다. 또한 이펙티브 자바에 따르면, forEach 연산은 최종 연산 중 기능이 가장 적고 가장 ‘덜’ 스트림답기 때문에, forEach 연산은 스트림 계산 결과를 보고할 때(주로 print 기능)만 사용하고 계산하는 것을 권장하고 있습니다.
IntStream.range(1, 10)
.filter(i -> i <= 5)
.forEach(System.out::println);
java.util.stream (Java Platform SE 8 )
Interface Summary Interface Description BaseStream > Base interface for streams, which are sequences of elements supporting sequential and parallel aggregate operations. Collector A mutable reduction operation that accumulates input elements into a mutab
docs.oracle.com
Stream의 foreach 와 for-loop 는 다르다.
Stream에 대한 기본적인 학습을 위해 찾아왔다면, 공식 오라클 문서를 참고하면 좋을 것 같다. (java8 부터는 Stream과 Lambda를 제공한다.) 자바에서 Stream…
tecoble.techcourse.co.kr
'Java' 카테고리의 다른 글
일급 컬렉션(First Class Collection) (1) | 2024.07.16 |
---|---|
가변 인자(Vararags) (0) | 2024.06.10 |
직렬화(Serialize)란? (2) | 2024.01.22 |