Language/Java

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

JOYERIM 2023. 7. 3. 13:32

 

 

 

 

 

 

 

핵심 요약

 

스트림 API에서는 람다 표현식이나 메서드 참조 등의 동작에 대해 외부 상태를 변경하지 않아야 한다. 이러한 원칙은 함수형 프로그래밍에서 가져온 개념으로, 데이터의 불변성을 유지하고 병렬 처리에 안전함을 보장하기 위함이다.

이와 관련하여 Collectors는 외부 상태를 변경하지 않고 스트림의 요소들을 수집하는 방법을 제공한다. Collectors의 여러 메서드들은 스트림의 결과를 수집하는 동안 상태를 변경하지 않는다. 대신, 새로운 컬렉션을 반환하거나, 집계값을 계산하거나, 그룹을 형성하는 등의 연산을 수행한다. 

예를 들어, Collectors.toList()는 스트림의 모든 요소를 새로운 리스트에 수집하고, Collectors.groupingBy()는 주어진 분류 함수에 따라 요소들을 그룹화하여 새로운 맵을 반환한다. 이러한 연산들은 모두 "부작용 없는 함수"를 사용하여 외부 상태를 변경하지 않으며, 함수형 프로그래밍의 원칙을 따른다.

 

 

 

스트림의 패러다임

 

스트림은 함수형 프로그래밍에 기초한 패러다임이다.

 

부작용 없는 함수

다른 곳에 영향을 끼치지 않는 함수를 말한다.

 

순수 함수

함수형 프로그래밍에서 사용되는 용어로, 다른 곳에 영향을 끼치지 않는 함수이다. 즉, 같은 입력이 주어지면 항상 같은 결과를 내는 함수이다.

 

 

 

 

 

 

 

예제 1
: 빈도수 저장하는 프로그램
forEach는 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 사용하지 말자.

 

// 스트림 패러다임을 이해하지 못한 코드
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라는 종단 연산에서 발생하며, 외부의 HashMap 상태를 직접 변경하므로 적절하지 않다. 이를 개선해보자.

 

Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
     freq = words.collect(groupingBy(String::toLowerCase, counting()));
}

 

위 코드는 collect() 메소드를 이용하여 스트림의 각 요소를 소문자로 변환하고 그룹화하는 동시에 각 그룹의 개수를 세어 freq에 저장한다. 즉, collect 메소드를 이용해 스트림을 Map으로 수집한다. 이는 스트림 내부에서 일어나는 연산이며, 외부 상태를 직접 변경하지 않는다.

 

 

 

 

 

 

 

 

java.util.Collectors

 

 

toList() 스트림의 요소를 List로 수집
toSet() 스트림의 요소를 Set으로 수집
toMap() 스트림의 요소를 Map으로 수집(key와 value를 어떻게 결정할지에 대한 람다를 인자로 받음)
groupingBy() 특정 기준에 따라 스트림의 요소를 그룹화하고 그 결과를 Map으로 변환한다.(기준을 결정하는 람다를 인자로 받음)
joining() 스트림의 각 문자열 요소를 결합하여 하나의 문자열로 만듬
counting() 스트림의 요소 개수를 세어서 반환
summingInt(), summingLong(), summingDouble() 스트림의 요소를 합산
averageInt(), averageLong(), averageDouble() 스트림의 요소의 평균값을 계산
maxBy(), minBy() 스트림의 요소 중 최대값 또는 최소값을 반환(비교 기준을 결정하는 Comparator를 인자로 받음)

 

 

 

Collectors (Java SE 10 & JDK 10 )

Returns a Collector implementing a "group by" operation on input elements of type T, grouping elements according to a classification function, and returning the results in a Map. The classification function maps elements to some key type K. The collector p

docs.oracle.com

 

 

 

 

 

 

 

 

예제 2
: 빈도표에서 가장 흔한 단어 10개를 뽑아내는 코드

 

List<String> topTen = freq.keySet().stream()
                .sorted(comparing(freq::get).reversed())
                .limit(10)
                .collect(toList());

 

 

 

 

 

예제 3
: toMap()

 

// 문자열을 열거 타입 상수에 매핑
private static final Map<String, Operation> stringToEnum = 
    Stream.of(values()).collect(
        toMap(Object::toString, e->e));

 

위 코드는 키가 중복되는 경우 IllegalStateException을 던진다. 즉, 동일한 키에 대해 두 개 이상의 값이 매핑되는 경우에는 이 메소드를 사용할 수 없다.

 

toMap() 메소드는 세 번째 선택적 매개변수로 merge 함수를 받을 수 있다. merge 함수는 BinaryOperator 인터페이스를 구현해야 하며, 같은 키에 대해 여러 값이 있는 경우 이를 합치는 방법을 정의한다.

 

// 여러 value 중 하나만 골라 key와 매핑 - sales가 최대인 value를 뽑아 map
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

 

// 여러 value 중 하나만 골라 key와 매핑 - 마지막 값으로 map
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal);

 

네 번째 선택적 매개변수로 특정 맵 구현체(EnumMap, TreeMap 등)를 직접 지정할 수 있다.

 

 

 

 

 

예제 4
: groupingBy()

 

1. 기본 사용 : groupingBy(classifier)

 

// 아나그램 프로그램
words.collect(groupingBy(word -> alphabetize(word)))

 

2. 추가 다운스트림 사용 : groupingBy(classifier, downstream)

 

이때 다운스트림은 toSet(), toCollection(collectionFactory), counting() 등이 될 수 있다.

 

// 키: 카테고리, 값: 원소의 개수
Map<String, Long> freq = words
						.collect(groupingBy(String::toLowerCase, counting()));

 

3. 맵 팩토리 사용 : groupingBy(classifier, mapFactory, downstream)

 

사용할 맵의 구현체를 지정할 수 있다.