스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다.
각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.
각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.
순수 함수란 오직 입력만이 결과에 영향을 주는 함수를 말한다.
다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
이렇게 하려면 스트림 연산에 건네는 함수 객체는 모두 부작용(side effect)이 없어야 한다.
스트림을 잘못 사용한 예
스트림 코드로 텍스트 파일에서 단어별 수를 세어 빈도표를 만드는 프로그램
- 스트림 코드를 가장한 반복적 코드이다.
- 스트림 API의 이점을 살리지 못하여 같은 기능의 반복적 코드보다 조금 더 길고, 읽기 어렵고, 유지보수에도 좋지 않다.
- 코드의 모든 작업이 종단 연산인 forEach에서 일어나는데, 이때 외부 상태를 수정하는 람다를 실행하면서 문제가 생긴다.
- forEach가 그저 스트림이 수행한 연산 결과를 보여주는 일 이상을 하고 있다.
스트림을 제대로 활용해 빈도표를 초기화
앞서와 같은 일을 하지만 짧고 명확하게 스트림 API을 제대로 사용했다.
- forEach 연산은 종단 연산 중 기능이 가장 적고 가장 덜 스트림답다.
- 대놓고 반복적이라 병렬화할 수 없다.
- forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자.
collector
위 예제 코드는 collector를 사용하는데, 스트림을 사용하려면 꼭 배워야 하는 개념이다.
- collector가 생성하는 객체는 일반적으로 컬렉션이다.
- 이를 이용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있다.
- collector는 총 3가지로 toList(), toSet(), toCollection(collectionFactory)으로 리스트, 집합, 프로그래머가 지정한 컬렉션 타입을 반환한다.
빈도표에서 가장 흔한 단어 10개를 뽑아내는 파이프라인
comparing(freq::get).reversed()를 살펴보자.
- comparing 메서드는 키 추출 함수를 받는 비교자 생성 메서드다.
- 한정적 메서드 참조이자, 해당 코드에서 키 추출 함수로 쓰인 freq::get은 입력받은 단어(키)를 빈도표에서 찾아 추출해 그 빈도를 반환한다.
- 그 후 가장 흔한 단어가 위로 오도록 비교자 comparing을 역순으로 정렬한다.
collector를 이용해 스트림에서 단어 10개를 뽑아 리스트에 담는다.
toMap
toMap(keyMapper, valueMapper)
스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수를 인수로 받는다.
toMap collector를 사용하여 문자열을 열거 타입 상수에 매핑
- Operation이라는 열거형(enum) 타입에 대한 맵을 생성하는 예시다.
- stringToEnum은 문자열을 key로 가지고 Operation을 value로 가지는 맵이다.
- Stream.of(values())는 Operation 열거형의 모든 상수를 스트림으로 생성하고, values() 메서드는 열거형의 모든 상수를 배열로 반환하는 메서드다.
- collect(toMap(Object::toString, e->e))은 스트림의 각 요소를 맵으로 수집하는 연산을 수행한다.
- toMap 메서드는 스트림의 요소를 기반으로 맵을 생성 하는데, 첫 번째 매개변수로는 키를 추출하는 함수를 전달하고, 두 번째 매개변수로는 값을 추출하는 함수를 전달한다.
인수 3개를 받는 toMap
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
- 어떤 키와 그 키에 연관된 원소들 중 하나를 골라 연결 짓는 맵을 만들 때 유용하다.
- 위 코드는 앨범 스트림을 각 음악가와 그 음악가의 베스트 앨범을 짝지은 것 맵으로 바꾸는 것이다.
- 비교자 maxBy는 BinaryOperator에서 정적 임포트한 정적 팩터리 메서드다.
마지막에 쓴 값을 취하는 collect
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal);
- 인수가 3개인 toMap은 충돌이 나면 마지막 값을 취하는 수집기를 만들 때도 유용하다.
- 매핑 함수가 키 하나에 연결해 준 값들이 모두 같을 거나 값이 다르더라도 모두 허용되는 값일 때 유용하다.
인수가 4개를 받는 toMap
- 4번째 인수로 맵 팩터리를 받는다. 이 인수로 EnumMap이나 TreeMap처럼 원하는 특정 맵 구현체를 직접 지정할 수 있다.
groupingBy
groupingBy(Function<? super T, ? extends K> classifier)
- Function은 함수형 인터페이스로, T 타입의 요소를 K 타입의 키로 매핑하는 함수를 나타낸다.
- 분류 함수 매개변수에는 요소를 그룹화할 때 사용할 함수를 전달한다.
- groupingBy 메서드는 맵을 반환 하며, 각 그룹의 키를 표현하는 맵의 키에 해당하는 그룹화된 요소들의 리스트를 값으로 가진다.
알파벳화한 단어를 알파벳화 결과가 같은 단어들의 리스트로 매핑하는 맵 생성
words.collect(groupingBy(word -> alphabetsize(word)));
collect가 반환하는 형태 변환
Map<String, Set<String>> groupedFruits = fruits.stream()
.collect(Collectors.groupingBy(fruit -> fruit, Collectors.toSet()));
- groupingBy가 반환하는 collect가 리스트 외의 값을 갖는 맵을 생성하게 하려면 분류 함수와 함께 다운스트림(downstream) collect도 명시해야 한다.
- toSet()이나 toCollection을 건네 리스트 대신 집합이나 컬렉션을 값으로 갖는 맵을 생성 한다.
맵 팩터리 지정
List<String> fruits = Arrays.asList("apple", "banana", "cherry", "apple", "banana");
Map<String, List<String>> groupedFruits = fruits.stream()
.collect(Collectors.groupingBy(fruit -> fruit, Collectors.toMap(fruit -> fruit, fruit -> fruit)));
- mapFactory 매개변수가 downStream 매개변수보다 앞에 놓인다.
- 이 버전의 groupingBy를 사용하면 맵과 그 안에 담긴 컬렉션의 타입을 모두 지정할 수 있다.
출력 결과
{cherry=[cherry], banana=[banana, banana], apple=[apple, apple]}
맵 팩터리
자바에서 맵(Map)을 생성하는 유틸리티 메서드를 제공하는 팩터리(Factory) 클래스를 말한다. 맵 팩터리는 일반적인 new 키워드를 사용하여 맵을 생성하는 대신, 더 간편하게 맵을 생성하고 초기화할 수 있는 방법을 제공한다.
Ex) Map<String, Integer> map = Map.of("A", 1, "B", 2, "C", 3);
결론
- 스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
- 종단 연산 중 forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용하고 계산 자체에는 이용하지 말자.
- 스트림을 올바로 사용하려면 collector를 알아야 한다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] Item 48. 스트림 병렬화는 주의해서 사용하라 (0) | 2023.07.09 |
---|---|
[Effective Java] Item 47. 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2023.07.09 |
[Effective Java] Item 45. 스트림은 주의해서 사용하라 (0) | 2023.07.08 |
[Effective Java] Item 44. 표준 함수형 인터페이스를 사용하라 (0) | 2023.07.08 |
[Effective Java] Item 43. 람다보다는 메서드 참조를 사용하라 (0) | 2023.07.08 |