Java

[Effective Java] 7-2. 람다와 스트림

noahkim_ 2024. 12. 31. 13:07

조슈아 블로크 님의 "Effective Java" 책을 정리한 포스팅 입니다.

 

1. 스트림은 주의해서 사용하라

스트림

데이터 원소의 시퀀스
  • 유한 혹은 무한
  • 컬렉션, 배열, 파일, 정규표현식 패턴 매처 등
  • 다량의 데이터 처리 작업을 돕고자 추가됨 (Java 8)

 

파이프라인

  • 원소들로 수행하는 연산 단계
  • 순차적으로 수행됨
    • 플루언트 API로 메서드 연쇄를 지원

 

시작
  • 소스 스트림으로 시작

 

중간연산
  • 한 스트림을 다른 스트림으로 변환함

 

종단 연산
  • 마지막 중간 연산이 내놓은 스트림에 최후의 연산을 수행함
  • 지연 평가
    • 무한 스트림이 이루어지도록 함

 

주의사항

재사용 불가
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println);

// 재사용하려 하면 예외 발생
stream.forEach(System.out::println); // IllegalStateException
  • 스트림은 일회성
  • 한번 사용된 스트림은 다시 사용할 수 없음

 

원본 수정 금지
List<String> list = Arrays.asList("a", "b", "c");
list.stream().map(String::toUpperCase); // 원본 리스트는 변경되지 않음
System.out.println(list); // [a, b, c]
  • 불변성을 유지
  • 데이터를 변환하려면 새로운 스트림이나 컬렉션을 생성해야 함

 

외부 상태 변경 금지
List<Integer> numbers = Arrays.asList(1, 2, 3);
int[] sum = {0};
numbers.stream().forEach(n -> sum[0] += n); // 외부 상태 변경
System.out.println(sum[0]); // 위험
  • 순수 함수형 프로그래밍을 지향
  • 사이드 이펙트 유발 금지

 

2. 스트림에서는 부작용 없는 함수를 사용하라

  • 스트림은 함수형 프로그래밍에 기초한 패러다임
  • 스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분

 

순수 함수

  • 각 변환 단계는 이전 단계의 결과를 받아 처리하는 순수 함수
  • 순수 함수는 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않음

 

forEach

  • 스트림 계산 결과를 보고할 때만 사용하기
  • 계산에 사용하지 말기

 

Collectors

toList()
toSet()
toMap()
toMap(keyMapper, valueMapper, mergeFunction, mapFactory)
  • keyMapper: 키를 생성하는 함수.
  • valueMapper: 값을 생성하는 함수.
  • mergeFunction: 키 충돌 시 값을 병합하는 함수.
  • mapFactory: 사용할 Map의 구현체를 생성하는 함수.

 

List<String> items = Arrays.asList("apple", "banana", "apple");

Map<String, Integer> itemCount = items.stream()
    .collect(Collectors.toMap(
        item -> item,                      // keyMapper: 단어 자체를 키로 사용
        item -> 1,                         // valueMapper: 각 단어를 1로 매핑
        Integer::sum,                      // mergeFunction: 같은 키가 있으면 합침
        HashMap::new                       // mapFactory: HashMap 사용
    ));

System.out.println(itemCount); // {apple=2, banana=1}
  • 스트림 요소를 키-값 쌍으로 변환해 Map 생성

 

grouppingBy()
groupingBy(classifier, downstreamCollector, mapFactory)
  • classifier: 그룹화 기준을 지정하는 함수.
  • downstreamCollector: 그룹화 후 데이터를 어떻게 처리할지 지정하는 컬렉터.
  • mapFactory: 결과로 생성될 Map의 구현체를 지정.

 

List<String> items = Arrays.asList("apple", "banana", "cherry", "apricot");

Map<Character, List<String>> groupedByFirstChar = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.charAt(0),               // classifier: 첫 글자를 기준으로 그룹화
        Collectors.toList(),                  // downstreamCollector: 그룹화된 데이터 리스트로 수집
        TreeMap::new                          // mapFactory: TreeMap 사용
    ));

System.out.println(groupedByFirstChar); // {a=[apple, apricot], b=[banana], c=[cherry]}
  • 데이터를 특정 기준으로 그룹화

 

 

partitionBy()
partitioningBy(classifier)
  • classifier: 데이터를 참/거짓으로 분류하는 함수.

 

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

Map<Boolean, List<Integer>> partitioned = numbers.stream()
    .collect(Collectors.partitioningBy(
        num -> num % 2 == 0                 // classifier: 짝수와 홀수로 분리
    ));

System.out.println(partitioned); // {false=[1, 3, 5], true=[2, 4, 6]}
  • 데이터를 두 개의 그룹으로 분리

 

joining()
joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
  • 구분자를 포함하여, 앞뒤에 접두사와 접미사 지정

 

List<String> words = Arrays.asList("apple", "banana", "cherry");

String result = words.stream()
    .collect(Collectors.joining(", ", "[", "]"));

System.out.println(result); // [apple, banana, cherry]