Java Stream Collection

김영한님의 김영한의 실전 자바 – 고급 3편, 람다, 스트림, 함수형 프로그래밍 내용 중 일부로 실무에서 자주 사용될 법한 Collection의 종류들이다

  • collect연산은 Collectors에 이미 구현되어 있기 때문에, Collector 인터페이스를 직접 구현하는 것보다 그 사용법을 익히는 것이 중요하다
  • Collectors를 사용할 때는 static import 사용을 추천한다

기본 List 수집

List<String> list = Stream.of("Java", "Spring", "JPA")
        .collect(Collectors.toList()); // 수정 가능 리스트를 반환
list.add("MVC");

System.out.println(list); // [Java, Spring, JPA, MVC]

수정 불가능 리스트

 // 수정 불가능 리스트
List<Integer> toUnmodifiableList = Stream.of(1, 2, 3)
        // .toList() Java 16부터 가능하고 불변 리스트를 제공
        .collect(Collectors.toUnmodifiableList()); // 런타임 예외 UnsupportedOperationException

// toUnmodifiableList.add(4);

Set으로 수집

Set<Integer> set = Stream.of(1, 2, 2, 3, 3, 3)
                .collect(Collectors.toSet());
타입 지정 - TreeSet은 정렬 상태를 유지
System.out.println(set); // [1, 2, 3]

타입 지정 – TreeSet은 정렬 상태를 유지

TreeSet<Integer> treeSet = Stream.of(3, 4, 5, 2, 1)
                .collect(Collectors.toCollection(TreeSet::new));
        
System.out.println(treeSet); // [1, 2, 3, 4, 5]

기본 Map 수집

Map<String, Integer> map = Stream.of("Apple", "Banana", "Tomato")
                .collect(Collectors.toMap(
                        name -> name, // keyMapper
                        name -> name.length() // valueMapper
                ));
        
System.out.println(map);
// {Apple=5, Tomato=6, Banana=6}

키 중복 예외 Map

Map<String, Integer> map = Stream.of("Apple", "Apple", "Tomato")
                .collect(Collectors.toMap(
                        name -> name, // keyMapper
                        name -> name.length() // valueMapper
                ));

java.lang.IllegalStateException: Duplicate key Apple

키 중복 대안 Map (병합)

Map<String, Integer> map = Stream.of("Apple", "Apple", "Tomato")
                .collect(Collectors.toMap(
                        name -> name, // keyMapper
                        name -> name.length(), // valueMapper
                        (oldValue, newValue) -> oldValue + newValue // 중복될 경우 기존 값 + 새 값
                ));

System.out.println(map);
// {Apple=10, Tomato=6}

Map 타입 지정

Map<String, Integer> map = Stream.of("Apple", "Apple", "Tomato")
                .collect(Collectors.toMap(
                        name -> name, // keyMapper
                        name -> name.length(), // valueMapper
                        (oldValue, newValue) -> oldValue + newValue, // 중복될 경우 기존 값 + 새 값
                        LinkedHashMap::new // 원하는 타입 지정 가능
                ));

System.out.println(map.getClass());
// class java.util.LinkedHashMap

그룹과 분할 수집

  • 첫 글자 알파벳을 기준으로 그룹화
List<String> names = List.of("Apple", "Avocado", "Banana", "Blueberry", "Cherry");

Map<String, List<String>> grouped = names.stream()
        .collect(Collectors.groupingBy(name -> name.substring(0, 1)));

System.out.println(grouped);
// {A=[Apple, Avocado], B=[Banana, Blueberry], C=[Cherry]}
  • 짝수(even)인지 여부로 분할(파티셔닝)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> partitioned = numbers.stream()
        .collect(Collectors.partitioningBy(n -> n % 2 == 0));

System.out.println(partitioned);
// {false=[1, 3, 5], true=[2, 4, 6]}
  • groupingBy는 특정 기준에 따라 스트림 요소를 여러 그룹으로 묶는다
  • partitioningBy는 단순하게 true, false 두 그룹으로 나눈다

최소값 최대값 수집

Integer max1 = Stream.of(1, 2, 3)
        .collect(Collectors.maxBy((i1, i2) -> i1.compareTo(i2)))
        .get();

System.out.println(max1); // 3

// 람다
Integer max2 = Stream.of(1, 2, 3)
        .max((i1, i2) -> i1.compareTo(i2))
        .get();

System.out.println(max2); // 3

// 메서드 참조
Integer max3 = Stream.of(1, 2, 3)
        .max(Integer::compareTo)
        .get();

System.out.println(max3); // 3

// 기본형 특화 스트림 사용
int max4 = IntStream.of(1, 2, 3)
        .max().getAsInt();

System.out.println(max4); // 34
  • Collectors.maxBy(), Collectors.minBy()를 통해 최소, 최대값을 구할 수 있다
  • 다만 Stream 자체가 제공하는 max(), min() 메서드를 쓰면 더 간단히 처리된다

통계 수집

  • count
Long count1 = Stream.of(1, 2, 3)
        .collect(Collectors.counting());

System.out.println(count1); // 3

long count2 = Stream.of(1, 2, 3)
        .count();

System.out.println(count2); // 3
  • average
Double average1 = Stream.of(1, 2, 3)
        .collect(Collectors.averagingInt(i -> i));

System.out.println(average1); // 2.0

// 기본형 특화 스트림으로 변환 후 사용
double average2 = Stream.of(1, 2, 3)
        .mapToInt(i -> i)
        .average().getAsDouble();

System.out.println(average2); // 2.0

// 기본형 특화 스트림 사용
double average3 = IntStream.of(1, 2, 3)
        .average().getAsDouble();

System.out.println(average3); // 2.0
  • 통계
IntSummaryStatistics stats = Stream.of("Apple", "Banana", "Tomato")
                .collect(Collectors.summarizingInt(s -> s.length()));

System.out.println(stats.getCount()); // 3
System.out.println(stats.getSum()); // 17
System.out.println(stats.getMin()); // 5
System.out.println(stats.getMax()); // 6
System.out.println(stats.getAverage()); // 5.6667

리듀싱 수집

  • 컬렉션의 리듀싱은 주로 다운 스트림에 활용. 모든 이름을 하나의 문자열로 이어 붙이기
List<String> names = List.of("a", "b", "c", "d");

// Collectors.reducing
String joined1 = names.stream()
        .collect(Collectors.reducing(
                (s1, s2) -> s1 + ", " + s2
        )).get();

System.out.println(joined1); // a, b, c, d

// reduce
String joined2 = names.stream()
        .reduce((s1, s2) -> s1 + ", " + s2).get();
System.out.println(joined2); // a, b, c, d

// 문자열 전용 기능
String joined3 = names.stream()
        .collect(Collectors.joining(", "));
System.out.println(joined3); // a, b, c, d


String joined4 = String.join(", ", "a", "b", "c", "d");
System.out.println(joined4); // a, b, c, d

출처 – 인프런 강의 중 김영한의 실전 자바 – 고급 3편, 람다, 스트림, 함수형 프로그래밍