책/이펙티브 자바
item 46 스트림에서는 부작용 없는 함수를 사용하라
함께자라기
2022. 4. 12. 16:56
ITEM 46 스트림에서는 부작용 없는 함수를 사용하라
스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성 하는것
- 각 변환 단계는 가능한 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다
- 순수 함수 : 오직 입력만이 결과에 영향을 주는 함수
- 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않고 함수 스스로도 다른 상태를 변경하지 않음
- 따라서 중간 단계든 종단 단계든 스트림 연산에 건네는 함수 객체는 모두 부작용이 없어야 한다
스트림 패러다임을 이해하지 못한 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);
});
}
위 코드는 스트림, 람다, 메서드 참조를 사용했으며 결과도 잘 나온다
하지만 스트림 코드라 할 수 없으며 스트림 코드를 가장한 반복적 코드다
같은 동작을 하는 반복적 코드보다 더 길고, 가독성도 좋지 않으며 유지보수도 힘들다
- 종단 연산인 forEach에서 모든 작업을 수행하며 외부 상태(빈도표)를 수정하는 람다를 실행하면서 문제가 생긴다
- forEach가 스트림이 수행한 연산 결과를 보여주는 일만 하는것이 아니라 람다가 상태를 수정하고 있다
같은 동작을 하는 좋은 예시를 보자
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
위 코드는 스트림 api를 잘 사용했으며 짧고 명확하다
첫번째 코드처럼 짜는 사람은 대부분 익숙하기 때문에 그럴것이다
- 평소에 자주 사용하던 for-each 반복문과 forEach 종단이 비슷하게 생겼다
- forEach 연산은 종단 연산 중 기능이 가장 적으며 가장 스트림 답지 않다
- 대놓고 반복적이라 병렬화도 어렵다
- forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고 계산할때는 쓰지말자
또한 위 코드는 collect(수집기)를 사용하는데 수집기는 스트림을 사용하려면 꼭 배워야 하는 개념이다
- java.util.stream.Collectors 클래스는 메서드를 39개나 가지고 있고 타입 매개변수가 5개나 되는것도 있다
- 하지만 복잡한 세부 내용을 잘 몰라도 이 api의 장점을 대부분 활용 가능하다
- 익숙해지기 전까지는 Collector 인터페이스를 잠시 잊고 축소(reduction) 전략을 캡슐화한 블랙박스 객체라고 생각하자
- 축소는 스트림의 원소들을 객체 하나에 취합한다는 뜻
- 수집기가 생성하는 객체는 일반적으로 컬렉션이며 collector라는 이름을 쓴다
- 수집기를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을수 있으며 수집기는 toList(), toSet(), toCollection(collectionFactory) 세가지가 있다
- 차례대로 리스트, 집합, 프로그래머가 지정한 컬렉션 타입을 반환한다