[아이템 46] 스트림에서는 부작용 없는 함수를 사용하라
2023. 5. 30. 22:36ㆍJava/Effective Java
스트림 패러다임
- 스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다
- 이때 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수이다.
- 순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다.
- 다른 가변상태 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
- 스트림 연산에 건네는 함수 객체는 모두 부작용(side effect)가 없어야 한다.
예제) 텍스트 파일에서 단어별 수를 세어 빈도표를 생성하는 코드
// 스트림 API를 적절히 사용하지 못하는 코드
public static void main(String[] args) {
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner("file").tokens()) {
// freq.merge 외부 상태를 수정
words.forEach(word -> freq.merge(word.toLowerCase(), 1L, Long::sum));
}
}
// 스트림 API를 제대로 사용하는 코드
public static void main(String[] args) {
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner("file").tokens()) {
words.collect(groupingBy(String::toLowerCase, counting()));
}
}
수집기(Collector)
- java.util.stream.Collectors 클래스에 있는 메서드들을 사용
- 축소(reduction) 전략을 캡슐화한 블랙박스 객체
- 축소는 스트림의 원소들을 객체 하나에 취합한다는 뜻
- 수집기가 생성하는 객체는 일반적으로 컬렉션이며, collector 라는 이름을 쓴다.
- 종류
- toList()
- toSet()
- toCollection(collectionFactory)
1) toMap
- toMap(keyMapper, valueMapper)
- 스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수를 인수로 받는다.
- toMap((keyMapper, valueMapper, mergeFunction)
- 같은 키를 공유하는 값들은 병합 함수를 사용해 기존 값에 합쳐진다.
class Artist {
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Artist(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Artist{" +
"name='" + name + '\\'' +
", age=" + age +
'}';
}
}
class Album {
Artist artist;
String name;
Integer sales;
public Artist getArtist() {
return artist;
}
public String getName() {
return name;
}
public Integer getSales() {
return sales;
}
public Album(Artist artist, String name, Integer sales) {
this.artist = artist;
this.name = name;
this.sales = sales;
}
@Override
public String toString() {
return "Album{" +
"artist=" + artist +
", name='" + name + '\\'' +
", sales=" + sales +
'}';
}
}
@Test
public void toMap_테스트() {
List<Album> albums = new ArrayList<>();
Artist artist = new Artist("ARTIST", 10);
Artist artist2 = new Artist("ARTIST2", 20);
Artist artist3 = new Artist("ARTIST3", 30);
Album album = new Album(artist, "ALBUM", 10);
Album album2 = new Album(artist2, "ALBUM2", 20);
Album album3 = new Album(artist3, "ALBUM3", 30);
albums.add(album);
albums.add(album2);
albums.add(album3);
Map<Artist, String> collect = albums.stream()
.collect(toMap(Album::getArtist, Album::getName));
for (Artist a : collect.keySet()) {
System.out.println(a.toString());
}
-- 출력 결과 --
Artist{name='ARTIST', age=10}
Artist{name='ARTIST3', age=30}
Artist{name='ARTIST2', age=20}
}
@Test
public void toMap_병합함수_테스트() {
List<Album> albums = new ArrayList<>();
Artist artist = new Artist("ARTIST", 10);
Artist artist2 = new Artist("ARTIST2", 20);
Album album = new Album(artist, "ALBUM", 10);
Album album2 = new Album(artist2, "ALBUM2", 20);
Album album3 = new Album(artist2, "ALBUM3", 30);
Album album4 = new Album(artist2, "ALBUM4", 40);
Album album5 = new Album(artist2, "ALBUM5", 50);
albums.add(album);
albums.add(album2);
albums.add(album3);
albums.add(album4);
albums.add(album5);
Map<Artist, Album> collect = albums.stream()
.collect(toMap(Album::getArtist, Function.identity(), maxBy(comparing(Album::getSales))));
for (Artist a : collect.keySet()) {
System.out.println(collect.get(a));
}
-- 출력 결과 --
Album{artist=Artist{name='ARTIST', age=10}, name='ALBUM', sales=10}
Album{artist=Artist{name='ARTIST2', age=20}, name='ALBUM5', sales=50}
}
2) groupBy
- 분류함수(classifier)를 입력받아서 출력으로 원소들을 분류기가 분류한 카테고리별로 모아놓은 맵을 반환한다.
// 알파벳화 결과가 같은 단어들의 리스트로 매핑하는 맵을 반환
words.collect(groupingBy(words -> alphabetize(word)))
- 인자로 다운스트림 수집기(down stream collector)를 같이 전달해주면 원소 리스트를 다른걸로 바꿀 수 있음
// 결과로 각 카테고리에 속하는 원소의 개수를 매핑한 맵을 얻을 수 있음
words.collect(groupingBy(String::toLowerCase, counting())
3) joining
- CharSequence의 인스턴스 스트림에만 적용 가능한 메서드로, 원소들을 연결(concat)함.
- joining() : 단순 concat
- joining(delimiter) : delimiter를 넣어서 연결
- joining(prefix, delimiter, suffix) : 접두문자, 구분자, 접미문자를 넣어서 연결
정리
- 스트림파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
- 스트림을 올바로 쓰려면 collector(수집기) 함수 객체를 잘 알아두고 사용하자.
'Java > Effective Java' 카테고리의 다른 글
[아이템 48] 스트림 병렬화는 주의해서 사용하라 (0) | 2023.05.31 |
---|---|
[아이템 47] 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2023.05.31 |
[아이템 45] 스트림은 주의해서 사용하라 (1) | 2023.05.17 |
[아이템 43] 람다보다는 메서드 참조를 사용하라 (0) | 2023.05.13 |
[아이템 42] 익명 클래스보다는 람다를 사용하라 (0) | 2023.05.12 |