김영한님의 김영한의 실전 자바 – 고급 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