목표
goal : collect() 최종연산에서 스트림을 Map으로 변환하는 두 가지 방법에 대해 알아본다.
예제에서 작성한 코드
collect 최종연산에서 Map을 만들어주는 두 가지 방법
1.
toMap() 은 단순히 스트림을 Map으로 변환해주고 싶을 때 활용합니다.
2.
groupingBy()는 스트림 요소 들을 특정 조건으로 그룹핑하여 맵으로 반환하고 싶을 때 활용합니다.
활용법
grouping by
SQL의 GROUP BY 같은 작업을 수행하고 싶을 때 활용합니다. 각 반 학생들 중 가장 큰 키를 구한다던지, 학급별 점수 총 합을 구한다던지 하는 연산을 특정한 그룹별로 구하고 싶을 때 유효합니다.
예제로 사용할 클래스는 다음과 같습니다.
class BlogPost {
String title;
String author;
BlogPostType type;
int likes;
//constructor, getter, setter, equals...
}
enum BlogPostType {
NEWS,
REVIEW,
GUIDE
}
class Tuple {
BlogPostType type;
String author;
}
List<BlogPost> posts = Arrays.asList(
new BlogPost("제목1", "작가1", GUIDE, 1),
new BlogPost("제목2", "작가1", REVIEW, 2),
new BlogPost("제목3", "작가1", REVIEW, 3),
new BlogPost("제목4", "작가2", NEWS, 4),
new BlogPost("제목5", "작가3", GUIDE, 5),
new BlogPost("제목6", "작가4", REVIEW, 6),
//동일한 2건
new BlogPost("제목7", "작가4", GUIDE, 7),
new BlogPost("제목7", "작가4", GUIDE, 7)
);
Java
복사
하나의 칼럼으로 그룹핑하기
가장 간단하게, key로 삼을 속성을 인자로 줄 수 있습니다. 결과는 스트림에서 key값이 일치하는 요소들을 모아 ArrayList<스트림 요소>로 만들어 value로 삼고, 동일했던 key값을 key로 삼습니다. 예제에서 작가명을 key로 주면, 작가1,작가2,작가3,작가4의 keySet을 가진 HashMap이 반환됩니다.
@DisplayName("keyMapper만 주어서 맵 생성 테스트")
@Test
void groupingByAuthor() {
Map<String, List<BlogPost>> groupByAuthor = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getAuthor));
int expectedAuthorsSize = 4;
assertEquals(groupByAuthor.size(), expectedAuthorsSize);
}
Java
복사
칼럼을 커스텀하여 그룹핑하기
key로 삼을 칼럼을 커스텀하고 싶다면 집적 Function을 전달하는 방법도 있습니다.
@DisplayName("복잡한 keyMapper 활용하여 맵 생성")
@Test
void gropingByTuple(){
Map<Tuple, List<BlogPost>> postsPerTypeAndAuthor = posts.stream()
.collect(Collectors.groupingBy(post -> new Tuple(post.getType(), post.getAuthor())));
assertEquals(postsPerTypeAndAuthor.get(new Tuple(GUIDE,"작가1")).size(), 1);
}
Java
복사
value를 원하는 타입으로 그룹핑하기
두번 째 인자는 그룹핑 된 요소들을 어떤 형태로 구조화 할지 결정합니다.
@DisplayName("value를 List가 아니라 원하는 방식으로 mapping")
@Test
void groupingByModifyingValueType(){
Map<BlogPostType, Set<BlogPost>> postsPerType = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getType, Collectors.toSet()));
int expectedAuthorsSize = 3; //4개 중 중복건 2개
assertEquals(postsPerType.get(GUIDE).size(), expectedAuthorsSize);
}
Java
복사
여러 필드로 그룹핑하기
두번 째 인자를 활용하여 여러 필드로 그룹핑 할 수 도 있습니다. 작가로 그룹핑 한 후, 내부에서 Type으로 그룹핑하는 예제입니다.
@DisplayName("여러 칼럼을 기준으로 그룹핑")
@Test
void groupingByMultipleColumn() {
Map<String, Map<BlogPostType, List<BlogPost>>> groupingAuthorThanType = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getAuthor, Collectors.groupingBy(BlogPost::getType)));
int expectedSize = 2;
assertEquals(groupingAuthorThanType.get("작가1").get(REVIEW).size(), expectedSize);
}
Java
복사
그룹 결과의 개수, 평균값, 합계값 구하기
Collectors.counting()을 이용하여 개수를 구할 수 있습니다
Collectors.avergeInt()를 이용하여 평균값을 구할 수 있습니다.(소수점이 나올 수 있으므로 Double로 반환됩니다.)
Collectors.summingInt()를 이용하면 합계값을 구할 수 있습니다.
@DisplayName("그룹한 결과값의 개수 구하기")
@Test
void gettingGroupingResultsCount() {
Map<String, Long> averageLikesPerAuthor = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getAuthor, Collectors.counting()));
int expectedLikesCount = 3;
//[3]
assertEquals(averageLikesPerAuthor.get("작가1"), expectedLikesCount);
}
@DisplayName("그룹한 결과값의 평균 구하기")
@Test
void gettingGroupingResultsAverage() {
Map<String, Double> averageLikesPerAuthor = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getAuthor, Collectors.averagingInt(BlogPost::getLikes)));
int expectedLikesCount = 2;
//[1,2,3]
assertEquals(averageLikesPerAuthor.get("작가1"), expectedLikesCount);
}
@DisplayName("그룹한 결과값의 합계 구하기")
@Test
void gettingGroupingResultsSum() {
Map<String, Integer> averageLikesPerAuthor = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getAuthor, Collectors.summingInt(BlogPost::getLikes)));
int expectedLikesCount = 6;
//[1,2,3]
assertEquals(averageLikesPerAuthor.get("작가1"), expectedLikesCount);
}
Java
복사
그룹 결과 중 최대, 최소인 요소 구하기
Collectors.maxBy(), minBy()를 이용하면 최대 최소인 요소를 구할 수 있습니다. maxBy 및 minBy는 Comparator를 인자로 받습니다. Comparator API의 comparingInt()를 활용할 수 있습니다.
@DisplayName("그룹 결과의 최대,최소 요소 구하기")
@Test
void gettingGroupingResultsMaxAndMin() {
Map<BlogPostType, Optional<BlogPost>> maxLikesPerPostType = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getType,
Collectors.maxBy(Comparator.comparingInt(BlogPost::getLikes))));
Map<BlogPostType, Optional<BlogPost>> minLikesPerPostType = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getType,
Collectors.minBy(Comparator.comparingInt(BlogPost::getLikes))));
int max = 7;
int min = 1;
assertEquals(maxLikesPerPostType.get(GUIDE).get().getLikes(), max);
assertEquals(minLikesPerPostType.get(GUIDE).get().getLikes(), min);
}
Java
복사
그룹 결과를 내맘대로 커스텀하기
Collectors.mapping()을 통해 결과값을 내가 원하는 방식으로 저장할 수 있습니다.
@DisplayName("value를 커스텀하여 저장하기")
@Test
void mappingGroupedResultsToDifferentType() {
Map<BlogPostType, String> postsPerType = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getType,
Collectors.mapping(BlogPost::getTitle, Collectors.joining(", ", "Post titles: [", "]"))));
String expectedResult = "Post titles: [제목2, 제목3, 제목6]";
assertEquals(postsPerType.get(REVIEW), expectedResult);
}
Java
복사
다른 Map 구현체로 저장하기
두번 째 인자로 Map 생성자를 주면, 다른 Type의 맵을 반환할 수 있습니다. 이럴 경우 downstream(결과 모음)의 조작은 세번째 인자를 통해 하게 됩니다.
@DisplayName("다른 map으로 저장하기")
@Test
void groupingByAnotherMap() {
EnumMap<BlogPostType, List<BlogPost>> postsPerType = posts.stream()
.collect(Collectors.groupingBy(BlogPost::getType,
() -> new EnumMap<>(BlogPostType.class), Collectors.toList()));
//[제목1, 제목5, 제목7, 제목7]
int expectedGuideSize = 4;
assertEquals(postsPerType.get(GUIDE).size(), expectedGuideSize);
}
Java
복사
ThreadSafe하게 만들기
ParallelStream인 경우, Collectors.groupingBy 대신 GroupingByConcurrent()를 활용하여 ConcurrentHashMap을 반환할 수 있습니다.
ConcurrentMap<BlogPostType, List<BlogPost>> postsPerType = posts.parallelStream()
.collect(groupingByConcurrent(BlogPost::getType));
Java
복사
toMap
위의 예제를 활용하여 테스트 하였습니다.
class Book {
private String name;
private int releaseYear;
private String isbn;
// constructor, getters and setters
}
Java
복사
예제용 클래스입니다! 책 이름, isbn번호(국제 표준 도서 번호), 출간년도를 가지고 있습니다.
3권의 책으로 리스트를 만들어보겠습니다.
List<Book> bookList = new ArrayList<>();
bookList.add(new Book("The Fellowship of the Ring", 1954, "0395489318"));
bookList.add(new Book("The Two Towers", 1954, "0345339711"));
bookList.add(new Book("The Return of the King", 1955, "0618129111"));
Java
복사
활용할 메서드 입니다.
기본 활용
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)
제네릭 때문에 다소 복잡해보이지만, keyMapper에 key로 할당할 내용, valueMapper에는 value로 할당할 내용을 전달하면 됩니다.
@DisplayName("Isbn가 key, 책 이름이 value인 맵 생성")
@Test
void toMapTest() {
Map<String, String> IsbnAndName = books.stream()
.collect(Collectors.toMap(Book::getIsbn, Book::getName));
int expectedSize = 3;
assertEquals(IsbnAndName.size(), expectedSize);
}
Java
복사
만약 동일한 key에 value가 들어가는 상황이라면, 기존 map처럼 덮어씌우는게 아닌 IllegalStateException을 throw하게 됩니다.
@DisplayName("동일한 key가 있을 때 오류가 나는지 확인")
@Test
void whenMapHasDuplicatedKeyTest() {
assertThrows(IllegalStateException.class,
() -> books.stream()
.collect(Collectors.toMap(Book::getReleaseYear, Function.identity())));
}
Java
복사
key가 중복될 수 있는 상황이라면 어찌해야 할까요?
Key가 중복될 수 있는 경우
세번째 인자로 key가 겹쳤을 때 수행할 BinaryOperator를 3번째 인자로 전달하면 됩니다.
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator mergeFunction)
@DisplayName("키 중복시 병합 로직도 추가한 toMap테스트")
@Test
void listToMapWithDupKeyTest() {
Map<Integer, Book> releaseYearAndBook = books.stream()
.collect(Collectors.toMap(Book::getReleaseYear, Function.identity(),
(existing, replacement) -> existing)); // (기존 value, 새롭게 들어온 value) → 수행할 행위, 여기선 map.put()과 달리 기존값이 남도록 해보겠습니다.
assertEquals(releaseYearAndBook.size(), 2);
assertEquals(releaseYearAndBook.get(1954).getName(), "The Fellowship of the Ring");
}
Java
복사
마지막으로 toMap은 기본 구현체로 HashMap()을 채택하고 있는데, 다른 Map을 구현체로 삼는 방법을 알아보겠습니다.
다른 Map 구현체를 이용하고 싶은 경우
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator mergeFunction,
Supplier mapSupplier)
4번째 인자로 Map 생성자를 전달하면 해당 map을 구현체로 삼게 됩니다.
@DisplayName("Hash Map이 아닌 다른 Map으로 생성")
@Test
void listToConcurrentMapTest() {
ConcurrentHashMap<Integer, Book> concurrentHashMap = books.stream()
.collect(Collectors.toMap(Book::getReleaseYear, Function.identity(),
(o1, o2) -> o1, ConcurrentHashMap::new));
assertTrue(concurrentHashMap instanceof ConcurrentHashMap);
}
Java
복사
마지막 한마디
인덴트를 한칸 줄여주는 고마운 스트림~ 선형 자료구조를 Map으로 뚝딱 만들어버리는 다재다능한 스트림~ 애정합니다~~