Effective Java item 46 스트림(Stream)에서는 부작용 없는 함수를 사용하라에서의 부작용 없는 함수와 어떤 함수를 사용하기를 권장하는지 확인해보도록 하겠습니다.
stream과 부작용 없는 함수
stream은 새로 추가된 또 하나의 API가 아닌 함수형 프로그래밍에 기초한 패러다임이기 때문에
장점이 무엇인지 쉽게 와닿지 않을 수도 있습니다.
Stream 패러다임에 핵심은 계산 로직을 일련의 변환(transformation)으로 재구성하는 부분입니다.
각 변환 단계는 가변 상태를 참조하지 않고 오로지 입력값에만 영향을 받는 순수함수로 이루어져
이전 단계의 결과를 받아 처리해야 합니다.
이 핵심을 지키려면 stream 연산 내에 건네는 함수 객체는 모두 함수가 결과값 이외에
다른 상태를 변경시켜 순수함수가 아니게 되어 예상하는 값을 반환할 수 없는 side effect가 없어야 합니다.
외부 값을 변경하는 예시
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
}
);
스트림을 사용해 나오는 결과 값은 같지만, 스트림 코드를 가장한 반복적 코드
foreach에서 외부 상태를 수정하는 lambda를 실행하면서 문제가 생긴다.
stream 답게 사용한 예시
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
};
짧고 명확하다.
ForEach() 연산은 stream 계산하는 데는 쓰지 말자.
forEach 연산은 종단 연산 중 기능이 적고 가장 덜 stream 답다.
대놓고 반복이라 병렬화할 수도 없다.
forEach 연산은 stream 계산 결과를 보고할 때만 사용하고 계산하는 데는 쓰지 말자.
가끔 stream 계산 결과를 기존 collection에 추가하는 등 다른 용도로 쓸 수는 있다.
stream을 올바르게 사용하려면 수집기(collector)를 잘 알아 두자.
수집기를 사용하면 stream의 원소를 손쉽게 컬렉션으로 모을 수 있습니다.
- 수집기는 총 세 가지로, toList(), toSet(), toCollection(collectionFactory) 가 있습니다.
- http://bit.ly/2MvTOAR (API 문서)
toList() 예시와 Java8, Java10, Java16에서의 사용법
List로 모아서 반환하는 간단한 예시를 보도록 하겠습니다.
Java 버전별로 toList()의 경우 사용 방식이 조금 차이가 나는데 이유가 존재합니다.
// Java8
List<String> collectorsToListCase = Stream.of("one", "two")
.collect(Collectors.toList());
collectorsToListCase.add("new");
// Java10
List<String> unmodifiableCase = Stream.of("one", "two")
.collect(Collectors.toUnmodifiableList());
unmodifiableCase.add("new");
// Java16
List<String> toListCase = Stream.of("one", "two").toList();
toListCase.add("new");
Collectors.toList();
- 변경 가능한 List를 반환하며 null값을 허용합니다.
Collectors.toUnmodifiableList();
- 변경 가능하지 않은 List를 반환하며 null값을 허용하지 않습니다.
to.List();
- 변경 가능하지 않은 List를 반환하며 null값을 허용합니다.
위와 같은 이유로 java version 8 이상을 사용 중인 경우에 intellij 나 sonarlint에서 immutable 한 list를 반환하도록 toList()를 사용하도록 권장하고 있습니다.
Map 수집기
Map을 수집하여 반환해 주는 Map수집기를 확인해 보자
toMap
- Collectors의 메서드는 대부분 Stream을 Map으로 취합하는 기능으로, Stream의 각 원소는 키 하나와 값 하나에 연관되어 있다. 또한, 다수의 Stream 원소가 같은 키에 연관될 수 있습니다.
toMap(keyMapper, valueMapper)
// 2개의 인수를 받는 toMap
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e)
)
// 스트림 원소 다수가 같은 키를 사용한다면 파이프라인이 IllegalStateException을 던지며 종료
// 3개의 인수를 받는 toMap
Map<Artist, Album> toHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales)))
;
//albums stream을 map으로 변경하는데 각 artist와 그 artist의 best album을 짝지은 것
// 비교자로 BinaryOperator에서 정적 임포트한 maxBy라는 정적 팩터리 메서드를 사용
// 충돌이 나면 마지막 값을 취하는 수집기도 만들 수 있다.
toMap(KeyMapper, valueMapper, (oldVal, newVal) -> newVal)
네 번째 인수로는 Map 팩토리를 받아 EnumMap이나 TreeMap처럼 원하는 특정 Map 구현체를 직접 지정할 수 있습니다.
GroupingBy
특정 속성값에 의해서 그룹핑을 짓는 것입니다.
매개변수로 3가지를 받을 수 있습니다.
- classifier (Function<? super T,? extends K> ): 분류 기준을 나타낸다.
- mapFactory (Supplier) : 결과 Map 구성 방식을 변경할 수 있다.
- downStream (Collector<? super T,A,D>): 집계 방식을 변경할 수 있다.
classifier만 받는 경우 (예시 A)
static <T,K> Collector<T,?,Map<K,List<T>>>
groupingBy(Function<? super T,? extends K> classifier)
classifier와 downStream(Collector)를 받는 경우 (예시 B)
static <T,K,A,D> Collector<T,?,Map<K,D>>
groupingBy(Function<? super T,? extends K> classifier,
Collector<? super T,A,D> downstream)
classifier와 mapFactory, downStream(Collector)를 받는 경우(mapFactory)가 앞에 온다 (예시 C)
static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M>
groupingBy(Function<? super T,? extends K> classifier,
Supplier<M> mapFactory, Collector<? super T,A,D> downstream)
😀 아래에서 각각에 매칭한 예시들을 확인해 봅시다.
입력으로 classifier를 받고 출력으로는 원소들을 카테고리별로 모은 Map을 담은 수집기를 반환합니다. (예시 A)
// 알파벳화한 단어를 알파벳화 결과가 같은 단어들의 리스트로 매핑하는 맵을 생성한다.
words.collect(groupingBy(word -> alpahabetize(word)))
리스트 외의 값을 갖는 Map을 생성하게 하려면 classifier와 함께 downStream도 명시해야 합니다. (예시 B)
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
매개 변수로 toSet()을 넘기면 List 대신 set으로 매핑합니다.
toSet() 대신 toCollection(collectionFactory)를 건네는 방법도 있습니다.
다운스트림 수집기에 더해 Map 팩토리도 지정할 수 있습니다. (예시 C)
EnumMap<OrderType, List<Order>> collect9 = orders.stream().collect(groupingBy(Order::getOrderType, () -> new EnumMap<>(OrderType.class), toList()));
mapFactory 매개변수가 downStream 매개변수보다 앞에 놓입니다.
mapFactory를 지정하면 맵과 그 안에 담긴 컬렉션의 타입을 모두 지정할 수 있습니다.
그 외 동시 수행 버전인 groupingByConcurrent 메서드, 키가 Boolean인 맵을 반환하는 partitioningBy도 있습니다. → 많이 쓰이지 않습니다.
Collectors에 정의되어 있지만 '수집'과는 관련 없는 메서드
maxBy, minBy → 인수로 받은 비교자를 이용해 Stream에서 값이 가장 작은 혹인 큰 원소를 찾아 반환합니다.
joining → CharSequence 타입의 구분문자를 매개 변수로 받아 연결 부위에 구분문자를 삽입해서 반환합니다.
- ex) 구분문자를 ", "를 받아 CSV형태의 문자열을 만들 수 있습니다.
참조
'Programming Language > Java' 카테고리의 다른 글
[Java] Java14 레코드(Record)를 알아보자 (1) | 2023.02.19 |
---|---|
[Effective Java] 반환 타입으로는 Stream 보다 Collection이 낫다. (item 47) (1) | 2023.01.13 |
[Effective Java] Stream은 주의해서 사용해라(Item 45) (1) | 2023.01.12 |
[Java] Stream (Stream이 밀려온다) (0) | 2023.01.09 |
[Java] 람다(Lambda)를 소화시켜 보자 (0) | 2023.01.08 |
댓글