Collections.forEach vs Stream.forEach

홍혁준·2023년 3월 19일
1

forEach

forEach는 for문을 연속된 데이터를 가진 집합 에서 편하게 사용하기 위해 추가된 문법이다.

자바에서 연속된 데이터를 가진 집합엔 컬렉션과 stream이 있고, 둘 모두 forEach를 지원한다.

Collections

컬렉션은 대표적인 데이터들의 집합이며, 자바에서 가장 많이 사용되는 API중 하나지 않을까 싶다.

컬렉션은 forEach가 도입되기 전 다음과 같이 반복을 사용하였다.

List<String> crewNames = List.of(
	"홍실", "에단", "로지", "져니", "블랙캣" ,"준팍");
int size = crewNames.size();
for(int i = 0; i < size; i++){
	System.out.println(crewNames.get(i));
}

위는 전통적인 for-loop입니다. 직접적인 index를 이용해, Collection을 순회하고 있습니다. forEach를 이용해 같은 결과를 내는 코드를 아래와 같이 바꿀 수 있습니다.

List<String> crewNames = List.of(
                "홍실", "에단", "로지", "져니", "블랙캣", "준팍");
for (String crewName : crewNames) {
    System.out.println(crewName);
}

직접적으로 index를 이용하여 순회하지 않고, Collections의 Iterator를 이용하여, 순회합니다.
내부 Iterator를 쓰기에, 다음과 같은 장점을 가집니다.

  1. IndextOutOfBoundException과 같은 에러가 발생할 여지가 없습니다.
  2. Iterator로 순회하기에 get과 같은 메서드로 값을 참조하는 기존 for-loop보다 빠릅니다.

장점만 있는 것은 아닙니다. 내부적으로, Iterator를 통해서 순회하기 때문에, 반복을 직접적으로 핸들링 할 수 없습니다.
각 요소를 한 번만 순회할 수 있습니다.

추가적으로 Collections의 forEach는 람다 표현식을 사용하여 아래와 같이 사용할 수 도 있습니다.

List<String> crewNames = List.of(
                "홍실", "에단", "로지", "져니", "블랙캣", "준팍");
crewNames.forEach(System.out::println);

Stream

stream 또한 연속된 데이터들의 집합을 처리하는 연산입니다.
스트림에도 forEach가 있고, 아래와 같이 사용할 수 있습니다.

Stream<String> stream = Stream.of("홍실", "에단", "로지", "져니", "블랙캣", "준팍");
stream.forEach(System.out::println);

사용하는 방법과 결과 모두 Collections.forEach와 동일합니다.

Collections vs Stream

둘 모두 forEach라는 메서드를 가지고 있고, Consumer를 파라미터로 받습니다.

그럼 저희는 forEach로, 연속적인 값들을 이용하고 싶을 때, 어느것을 사용하면 될까요?
동일한 파라미터를 받고, 동일한 결과를 반환하니 아무거나 사용하면 될까요?

정답부터 말씀드리자면, Stream.forEach보다 Collections.forEach를 사용하는게 좋습니다.

이유는 크게 두 가지 입니다,

순서

stream의 forEach는 순서를 보장하지 않습니다.
일반적인 stream에서는 순서가 보장되지만,
만약 parallelStream으로 생성된 stream에서는 순서가 보장되지 않습니다.

final List<Integer> ints = List.of(1, 2, 3, 4, 5, 6);
ints.parallelStream()
        .forEach(System.out::println);
//출력 순서
4
5
6
1
2
3

forEachOrdered라는 메서드로, 순서를 보장할 수 있긴합니다.
하지만, 그런식으로 순서를 보장할거면, Collections.forEach가 더 낫습니다.

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) 
        action.accept(t);
    }
}
//실제 Collections.forEach의 내부 코드

내부 iterateor로 순회하므로 순서가 항상 보장됩니다.

Exception

Collections

Collections.forEach에서 내부 요소가 순회되는 중 기존 Collection에 값이 추가 또는 삭제 되면, 바로 ConcurrentModificationException을 던집니다.

Stream

하지만 stream은 모든 요소를 전부 순회한 후에 Exception을 던집니다.

연산 후

심지어, Exception을 나중에 던지기 때문에, 문제가 있는 연산을 계속해서 실행합니다.

collection

stream

try-catch에서 출력된 상태를 보면, stream은 나중에 Exception을 던지기 때문에 모든 원소들이 뒤에 추가된 것을 알 수가 있습니다.

결론

거의 모든 상황에서, Stream의 forEach보다 Collections.forEach가 낫습니다.
collections.forEach가 사용가능한 상황이면, stream.forEach를 사용하지 맙시다.

참고 자료 : https://www.baeldung.com/java-collection-stream-foreach

profile
끊임없이 의심하고 반증하기

2개의 댓글

comment-user-thumbnail
2023년 3월 19일

좋은 글 감사합니다. 둘 사이에 이런 차이점이 있었군요.
인텔리제이는 stream의 forEach를 쓰면 Collections의 forEach를 추천해주던데 이런 이유 때문일 수도 있겠네요

1개의 답글