아이템 46. 스트림에는 부작용 없는 함수를 사용하라

wisdom·2022년 9월 16일
0

Effetctive Java

목록 보기
46/80
post-thumbnail

스트림 패러다임의 핵심은 계산을 일련의 변환(transformation)으로 재구성하는 부분이다.

각 변환 단계는 순수 함수 여야 한다.
즉, 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
+) 순수 함수란, 오직 입력만이 결과에 영향을 주는 함수를 말한다.

이렇게 하려면 스트림 연산에 건네는 함수 객체는 모두 부작용(side effect)이 없어야 한다.

1. forEach

forEach 연산은 종단 연산 중 기능이 가장 적고 가장 '덜' 스트림다운 연산이다.

이 연산은 대놓고 반복적이라서 병렬화할 수도 없다.

forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자.
물론 가끔은 스트림 걔산 결과를 기족 컬렉션에 추가하는 등의 다른 용도로도 쓸 수 있다.

예제

다음 코드는 스크림 코드를 가장한 반복적 코드로써, 스트림 API를 사용했지만 이점을 살리지 못 하였다.
이 코드는 같은 기능의 반복적 코드보다 길고, 어렵고, 유지보수에도 좋지 않다.

// 코드 46-1 스트림 패러다임을 이해하지 못한 채 API만 사용했다
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    });
}

위의 예제를 올바르게 작성하면 다음과 같다.

// 코드 46-2 스트림을 제대로 활용해 빈도표를 초기화한다.
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
    freq = words
            .collect(groupingBy(String::toLowerCase, counting()));
}

2. Collector (수집기)

스트림을 사용하려면 꼭 배워야하는 개념이 수집기(collector)다.

java.util.stream.Collectors 클래스는 메서드를 무려 39개나 가지고 있다. (자바 8 기준, 10 기준으로는 43개)

🔖 자세한 내용은 공식 문서를 참고하자.
https://docs.oracle.com/javase/10/docs/api/java/util/stream/Collectors.html


이제 메서드들을 하나씩 알아보자.

1) toList(), toSet(), toCollection(collectionFactory)

각각 스트림의 원소들을 리스트, 집합, 프로그래머가 지정한 컬렉션 타입에 담아 반환한다.

toList()

toSet()

toCollection(collectionFactory)

2) toMap(), toConcurrentMap()

스트림 원소들을 맵에 넣어 반환한다.

toMap(keyMapper, valueMapper)
toMap(keyMapper, valueMapper, mergeFunction)
toMap(keyMapper, valueMapper, mergeFunction, mapFactory)

toConcurrentMap(keyMapper, valueMapper)
toConcurrentMap(keyMapper, valueMapper, mergeFunction)
toConcurrentMap(keyMapper, valueMapper, mergeFunction, mapFactory)
  • keyMapper : 스트림 원소를 키에 매핑하는 함수.
  • valueMapper : 스트림 원소를 값에 매핑하는 함수.
  • mergeFunction : 같은 키를 공유하는 값들은 mergeFunction(병합 함수)를 사용해 기존 값에 합쳐진다.
  • mapFactory : 특정 맵 구현체를 직접 지정한다. (ex. EnumMap, TreeMap)
  • toConcurrentMap() : toMap()의 변종으로써, 병렬 실행된 후 결과로 ConcurrentHashMap 인스턴스를 생성한다.

3) groupingBy(), groupingByConcurrent()

classifier(분류 함수)를 받아 카테고리별로 모아 놓은 맵을 담은 수집기를 반환한다.
카테고리가 해당 원소의 맵 키로 쓰인다.

groupingBy(classifier)
groupingBy(classifier, downstream)
groupingBy(lassifier, mapFactory, downstream)

groupingByConcurrent(classifier)
groupingByConcurrent(classifier, downstream)
groupingByConcurrent(classifier, mapFactory, downstream)
  • classifier : 입력받은 원소가 속하는 카테고리를 반환한다.
  • downstream : 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성한다.
    수집기가 리스트 외의 값을 갖는 맵을 생성하기 위해서 사용한다.
  • mapFactory : 맵과 그 안 담긴 컬렉션의 타입을 지정한다.
  • groupingByConcurrent() : groupingBy()의 동시 수행 버전으로, ConcurrentHashMap 인스턴스를 생성한다.

4) partitioningBy()

분류 함수 자리에 predicate를 받고 키가 Boolean인 맵을 반환한다.

partitioningBy(predicate)
partitioningBy(predicate, downstream)

5) counting()

counting()

counting 메서드가 반환하는 수집기는 다운스트림 수집기 전용이다.

💡 groupingBy의 다운 스트림으로 사용한 예제

Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));

Stream의 count 메서드를 직접 사용하여 같은 기능을 수행할 수 있으니 collect(counting()) 형태로 사용할 일은 전혀 없다.

6) summing(), averaging(), summarizing()

counting() 과 마찬가지로 직접 사용할 일은 없다.

summingDouble(mapper)
summingInt(mapper)
summingLong(mapper)

averagingDouble(mapper)
averagingInt(mapper)
averagingLong(mapper)

summarizingDouble(mapper)
summarizingInt(mapper)
summarizingLong(mapper)

7) reducing(), filtering(), mapping(), flatMapping(), collectingAndThen()

counting() 과 마찬가지로 직접 사용할 일은 없다.
대부분의 프로그래머는 이들의 존재를 모르고 있어도 상관없다.

reducing(op)
reducing(identity, op)
reducing(identity, mapper, op)

filtering(predicate, downstream)

mapping(mapper, downstream)

flatMapping(mapper, downstream)

collectingAndThen(downstream, finisher)

8) minBy(), maxBy()

인수로 받은 비교자를 이용해 스트림에서 값이 가장 작은/큰 원소를 찾아 반환한다.

Stream 인터페이스의 min과 max 메서드를 일반화한 것이다.

minBy(comparator)
maxBy(comparator)

9) joining()

joining 메서드는 CharSequence 인스턴스의 스트림에만 적용할 수 있으며, 원소들을 연결한다.

joining()	
joining(delimiter)	
joining(delimiter, prefix, suffix)	
  • delimiter : 연결 부위에 delimiter(구분 문자)를 삽입한다.
  • prefix : 접두 문자를 삽입한다.
  • suffix : 접미 문자를 삽입한다.

10) toUnmodifiableList(), toUnmodifiableMap(), toUnmodifiableSet()

책에는 없지만, 자바 10부터 다음의 메서드가 추가되었다.

toUnmodifiableList()

toUnmodifiableMap(keyMapper, valueMapper)
toUnmodifiableMap(keyMapper, valueMapper, mergeFunction)

toUnmodifiableSet()



📌 핵심 정리

스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
스트림 뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다.
종단 연산 중 forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용해야 한다. 계산 자체에는 이용하지 말자.
스트림을 올바로 사용하려면 수집기(collector)를 잘 알아둬야 한다.
가장 중요한 수집기 팩터리는 toList, toSet, toMap, groupingBy, joining 이다.

profile
백엔드 개발자

0개의 댓글