forEach는 컬렉션의 각 원소에 대해 작업을 수행할 때 쓴다. 새 값을 만들어 반환하는 API가 아니라, 이미 있는 원소에 대해 동작을 실행하는 API라고 보는 편이 안전하다.
forEach가 어울리는 경우
각 장르의 노래 목록을 정렬하는 코드는 forEach와 잘 맞는다.
genreMap.values().forEach(g -> g.songs.sort(Song::compareTo));
여기서는 새 컬렉션을 만드는 것이 아니라, 이미 존재하는 각 Genre의 songs 리스트를 정렬한다. 즉 부수 효과가 목적이다.
일반 for문으로 쓰면 다음과 같다.
for (Genre genre : genreMap.values()) {
genre.songs.sort(Song::compareTo);
}
둘은 의미상 거의 같다. 코딩테스트에서는 더 읽기 쉬운 쪽을 고르면 된다.
forEach가 덜 어울리는 경우
값을 변환해서 새 리스트를 만들고 싶다면 map, filter, collect가 더 맞다.
List<Integer> indexes = songs.stream()
.map(song -> song.index)
.collect(Collectors.toList());
이런 작업을 forEach로도 만들 수는 있다.
List<Integer> indexes = new ArrayList<>();
songs.forEach(song -> indexes.add(song.index));
하지만 스트림을 이미 쓰고 있다면 map이 의도를 더 잘 드러낸다. forEach는 "각각 실행"이고, map은 "변환"이다.
stream 안에서 forEach를 조심해야 하는 이유
스트림 중간에서 외부 리스트에 값을 추가하는 방식은 코드 흐름이 흐려질 수 있다.
List<Integer> answer = new ArrayList<>();
genreMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.forEach(e -> answer.add(...));
이 방식이 항상 틀린 것은 아니다. 다만 반환값을 만드는 흐름이라면 다음처럼 스트림의 마지막까지 연결하는 쪽이 더 읽기 쉽다.
int[] answer = genreMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.flatMap(e -> e.getValue().songs.stream().limit(2))
.mapToInt(song -> song.index)
.toArray();
기억할 기준
| 목적 | API |
|---|---|
| 각 원소에 작업 실행 | forEach |
| 원소를 다른 값으로 변환 | map |
| 일부만 남기기 | filter |
| 여러 스트림을 하나로 펴기 | flatMap |
| 결과 컬렉션 만들기 | collect |
| primitive 배열 만들기 | mapToInt(...).toArray() |
실수 체크
- 새 값을 만들려는 작업에
forEach를 억지로 쓰지 않았는가 - 기존 객체를 정렬하거나 갱신하는 작업이라
forEach가 자연스러운가 - 스트림 결과를 반환해야 한다면
map,flatMap,collect,toArray흐름이 더 맞지 않은가 - 일반 for문이 더 읽기 쉬우면 무리해서
forEach를 쓰지 않았는가