요약
- 스트림에서는 부작용 없는 함수(순수 함수)를 사용하고, 스트림뿐 아니라 스트림 관련 객체에 전해지는 모든 함수 객체가 부작용이 없어야 한다.
- 스트림을 올바르게 사용하려면 수집기를 잘 알아두자.
- 가장 중요한 수집기 팩터리 : toList, toSet, toMap, groupingBy, joining
스트림 패러다임
스트림은 함수형 프로그래밍에 기초한 패러다임이므로, 계산을 일련의 변환으로 재구성하는 부분이 가장 중요하다. 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수함수여야 한다.
순수 함수
순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 의미한다.
- 다른 가변 상태를 참조하지 않는다
- 함수 스스로도 다른 상태를 변경하지 않는다
잘못된 스트림 코드
Map<String, Long> freq = new HashMap<>();
try(Stream<String> words = new Scanner(new File("pathname")).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
- 위 코드는 종단연산인 forEach에서 모든 작업이 일어나는데, 이 때 외부 상태인 freq을 수정하는 람다를 실행하면서 문제가 발생한다.
- 이는 스트림 API의 이점을 활용하지 못한 뿐 아니라, 가독성도 안 좋고, 유지보수에도 좋지 않은 코드이다.
수집기(collector)
축소 전략을 캡슐화한 블랙박스 객체이다. 수집기가 생성하는 객체는 일반적으로 컬렉션이다. 수집기를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있다.
→ 스트림 원소들을 하나로 취합하는 객체
- 수집기 종류
- toList()
- 리스트 반환
- toSet()
- 집합 반환
- toCollection(collectionFactory)
- 지정한 컬렉션 타입 반환
- toList()
- 예시
- freq 에서 가장 흔한 단어 10개를 뽑아내는 파이프라인
List<String> topTen = freq.keySet().stream() .sorted(comparing(freq::get).reversed()) // 내림차순 정렬 .limit(10) // 상위 10개 .collect(toList()); // 리스트 반환
- comparing(freq::get).reversed()
- comparing() : 추출된 키들을 비교하는 함수
- freq::get : freq의 각 key에 해당하는 value 반환
- reversed() : comparing을 역순으로 정렬
- collect(toList()) : 리스트 반환
toList(), toSet(), toCollection 외에 더 많은 Collectors 메서드들을 알아보자.
toMap()
toMap()에도 3가지 종류가 있다.
1. toMap(keyMapper, valueMapper)
스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수를 인수로 받는다. 이 toMap 형태는 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합하다.
- 예시
- toMap 수집기를 사용해 열거형의 모든 요소를 문자열로 변환하고, 변환된 문자열을 키로 사용해 해당 열거형의 값을 맵으로 만든다.
- {enum : enum 값}
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
2. 파라미터 3개인 toMap()
이 toMap()은 어떤 키와 그 키에 연관된 원소들 중 하나를 골라 연관 짓는 맵을 만들 때 유용하다. 추가로 인수가 3개인 toMap은 충돌이 나면 마지막 값을 취하는 수집기를 만들 때도 유용하다. (혹시나 있을 키 충돌을 해결해줄 수 있다)
- 예시1
- 다양한 아티스트의 앨범들을 담은 스트림에서 음악가와 그 음악가의 베스트 앨범(판매량이 가장 많은)을 연관 지을 때
→ 앨범 스트림을 맵으로 바꾸는데, 이 맵은 각 음악가와 그 음악가의 베스트 앨범을 짝지은 것이다.Map<Artist, Album> topHits = albums.collect( toMap(Album::artists, a->a, maxBy(comparing(Album::sales))));
- 예시2 - 마지막에 쓴 값을 취하는 수집기
- toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal);
3. 네번째 인수로 맵 팩터리를 받는 toMap()
세번째 인수는 2번 toMap()과 동일하나, 네번째 인수로는 EnumMap이나 TreeMap처럼 원하는 특정 맵 구현체를 직접 지정할 수 있다.
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal, 특정 맵 구현체 지정);
groupingBy
groupingBy 메서드는 입력으로 분류 함수를 받고, 출력으로는 원소들을 카테고리별로 모아 놓은 맵을 담은 수집기를 반환한다.
1. 분류 함수 하나를 인수로 받는 groupingBy
{카테고리 : [카테고리에 속하는 원소들]} 형태의 맵을 반환한다.
- 예시 - 애너그램 만드는 코드
- 알파벳화한 단어를 알파벳화 결과가 같은 단어들의 리스트로 매핑하는 맵 생성
- {알파벳 조합 : [단어1, 단어2,,,]}
words.collect(groupingBy(word -> alphabetize(word)))
2. 리스트 이외의 맵을 생성하는 groupingBy
groupingBy가 반환하는 수집기가 리스트 외의 값을 갖는 맵을 생성하게 하려면, 분류 함수와 함께 다운스트림 수집기도 명시해야 한다.
- groupingBy(분류 함수, 다운 스트림)
- 다운 스트림
- toSet() : 집합을 갖는 맵 생성
- toCollection : 지정한 컬렉션을 값으로 갖는 맵을 생성
- counting() : {카테고리 : 카테고리 원소 개수} 처럼 각 카테고리(키)를 해당 카테고리에 속하는 원소 개수를 매핑한 맵을 얻을 수 있다.
- 다운 스트림
joining
원소들을 특정 조건 하에 연결하는 함수이다. 이 메서드는 문자열 등의 CharSequence 인스턴스의 스트림에만 적용할 수 있다.
1. 매개변수가 없는 joining()
단순히 원소들을 연결하는 수집기를 반환한다. (문자들 하나로 붙임)
2. 매개변수 하나인 joining(,)
CharSequence 타입의 구분문자를 매개변수로 받는다. 구분 문자를 연결 부위에 삽입한다. 예시로 구분문자로 쉼표(,)를 입력하면 CSV 형태의 문자열을 만들어 준다.
3. 매개변수 3개인 joining
이 joining은 구분문자에 더해 접두문자(prefix)와 접미문자(suffix)도 받는다. 예를 들어 접두, 구분, 접미 문자를 각각 ‘[’, ‘,’ , ‘]’로 지정하여 얻은 수집기는 [came,saw,conquered]처럼 마치 컬렉션을 출력한 듯한 문자열을 생성한다.
'Dev Language > EffectiveJava' 카테고리의 다른 글
[EffectiveJava] 적시에 방어적 복사본을 만들라 (0) | 2025.03.09 |
---|---|
[EffectiveJava] 스트림 병렬화는 주의해서 적용하라 (0) | 2025.03.03 |
[EffectiveJava] 익명 클래스보다는 람다를 사용하라 (2) | 2025.02.16 |
[EffectiveJava] 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2025.02.09 |
[EffectiveJava]명명패턴 대신 애너테이션을 사용하라 (0) | 2025.01.31 |