[이펙티브 자바 아이템 46] 스트림에서는 부작용 없는 함수를 사용하라.

박상준·2024년 6월 15일
0

이펙티브 자바

목록 보기
12/16

스트림에서 부작용없는 함수 사용하기

  • 스트림은 자바에서 함수형 프로그래밍 패러다임을 도입한 중요한 기능이다.
  • 스트림에서는 부작용(side effect) 없는 함수를 사용해야함.

스트림의 기본 개념

  • 스트림(Stream)
    • 데이터의 연속적인 흐름을 의미한다.
    • 컬렉션이나 배열 등의 데이터 소스로부터 생성된다.
  • 변환(transformation)
    • 스트림의 요소들을 연속적인 함수 적용을 통해 변환하는 과정이다,
    • 예를 들어
      • map , filter , sorted 등의 중간 연산이 있다.
  • 종단 연산(terminal operation)
    • 스트림의 요소들을 소비하여 결과를 생성하는 연산이다.
    • 예를 들어
      • collect foreach reduce 등이 있다.

부작용 없는 함수의 중요성

  • 입력값만을 사용하여 결과를 계산하고, 외부 상태를 변경하지 않는 함수를 말한다.
  • 스트림에서 이러한 함수를 사용해야하는 이유
    1. 가독성
    2. 병렬 처리
      • 스트림은 병렬 처리를 지원하는데,, 부작용이 없다면 병렬 처리 시 동기화 문제를 회피가능하다
    3. 유지보수성

잘못된 스트림 사용 예시

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 를 사용해 외부 상태인 freq 맵을 수정한다
    • 스트림의 장점을 살리지도 못하고, 병렬 처리에 부적합
    • foreach 는 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말아야 한다.

올바른 스트림

Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
    freq = words.collect(Collectors.groupingBy(String::toLowerCase, Collectors.counting()));
}
  • 스트림의 각 단계를 순수함수로 구성
    • 사이드 이펙트 없이 단어 빈도표를 생성함.
    • collect 메서드를 사용하여 스트림의 원소를 그룹화, 각 그룹의 원소 개수를 세는 수집기를 사용함.

상위 10개의 빈도가 높은 단어 추출

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

스트림에서 toMap 수집기 사용

  • toMap 은 원소들을 맵으로 수집시 사용.
  • 스트림 원소를
    1. 키에 매핑하는 함수

    2. 값에 매핑하는 함수를 인수로 받는다.

      기본 형태의 toMap 수집기

    • 기본 형태의 toMap 수집기는 각 스트림 원소가 고유한 키를 가질 때 적합함.

    • 만약 키가 중복되는 경우, IllegalStateException 이 발생할 수 있음.

      private static final Map<String, Operation> stringToEnum = 
          Stream.of(values()).collect(toMap(Object::toString, e -> e));

      중복 키를 처리하는 toMap 수집기

    • 키 중복 문제를 해결하기 위해 세 번째 인수로 병합 함수 제공가능
      - 키에 대해 값을 병합하는 역할을 한다

      Map<Artist, Album> topHits = albums.collect(
          toMap(Album::artist, a -> a, maxBy(comparing(Album::sales)))
      );
    • 각 음악가의 앨범들 중 가장 많이 판매된 앨범을 선택하는 병합 함수이다.

      마지막 값을 취하는 toMap 수집기

      toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
    • 키가 중복되는 경우 마지막 값(새로운 값) 을 선택한다.

      특정 맵 구현체를 사용하는 toMap 수집기

    • 4 번째 인수로 맵 팩토리를 제공하여 특정 맵 구현체를 사용한다.

    • 예를 들어
      - TreeMap 사용하고 싶자면 다음과 같이 작성한다

      ```java
      toMap(keyMapper, valueMapper, mergeFunction, TreeMap::new)
      ```

      groupingBy 수집기

    • 스트림의 원소들을 카테고리별로 그룹화해 맵으로 수집한다.

      public class Main {
          public static void main(String[] args) {
              // 단어 목록을 리스트로 생성
              List<String> wordList = Arrays.asList("listen", "silent", "enlist", "google", "gogole", "evil", "vile", "live");
              
              // 리스트를 스트림으로 변환
              Stream<String> words = wordList.stream();
              
              Map<String, List<String>> anagrams = words.collect(
                      groupingBy(Main::alphabetize)
              );
              
              anagrams.forEach((k, v) -> {
                  System.out.println(k + ": " + v);
              });
          }
          
          private static String alphabetize(String s) {
              char[] a = s.toCharArray();
              Arrays.sort(a);
              return Arrays.toString(a);
          }
      }
    • words.collect 에서

      • 배열의 값을 순차적으로 alphabetize 를 수행한다.
      • "listen"이 입력되면, 문자 배열 ['l', 'i', 's', 't', 'e', 'n']이 정렬됨.
        • 이후 sort 를 통해
        • ['e', 'i', 'l', 'n', 's', 't'] 가 되며, [e, i, l, n, s, t] 문자열로 반환
      • 모든 단어에 대해 반환된 문자열을 키로 삼아서
      • 해당 값을 가진 단어들이 하나의 그룹으로 묶인다

다운스트림 수집기를 사용하는 groupingBy

  • 더 복잡한 형태의 그룹화를 수행할 수 있다.
  • 예를 들어, 각 카테고리의 원소 개수를 세는 맵을 생성가능.
public class Main {
    public static void main(String[] args) {
        // 단어 목록을 리스트로 생성
        List<String> wordList = Arrays.asList("apple", "BANANA", "apple", "banana", "apple", "banana", "apple", "banana", "apple", "banana");
        
        // 리스트를 스트림으로 변환
        Stream<String> words = wordList.stream();
        
        Map<String, Long> anagrams = words.collect(
                groupingBy(String::toLowerCase, counting())
        );
        
        anagrams.forEach((k, v) -> {
            System.out.println(k + ": " + v);
        });
    }
}

banana: 5
apple: 5
  • 단어 빈도 출력

특정 맵 구현체 사용하는 groupingBy

Map<String, Long> anagrams = words.collect(
    groupingBy(String::toLowerCase, TreeMap::new, counting())
);
  • 해당 코드는 단어즐을 TreeMap 으로 그룹화하고, 각 그룹의 값을 Set 으로 수집한다.

  • 스트림, 스트림 관련 객체에 전달되는 모든 함수 객체에 부작용이 없어야함.
  • 종단 연산중 foreach 는 스트림이 수행한 계산 결과를 보고할 때만 사용해야한다.
    • 계산 자체에 사용하는 것을 금지!
profile
이전 블로그 : https://oth3410.tistory.com/

0개의 댓글