Java Collectors toMap
1. List to Map
List를 Map으로 변환하는 가장 간단한 방법이다.
Book 클래스를 정의하는 방법은 다음과 같다.
1
2
3
4
5
6
7
class Book {
private String name;
private int releaseYear;
private String isbn;
// getters and setters
}
그리고 코드를 검증하기 위해 책 목록을 만든다.
1
2
3
4
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"));
이 시나리오에서는 toMap()
메서드의 다음 오버로드를 사용한다.
1
2
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
toMap을 사용하면 map의 키와 값을 얻는 방법에 대한 전략을 나타낼 수 있다.
1
2
3
public Map<String, String> listToMap(List<Book> books) {
return books.stream().collect(Collectors.toMap(Book::getIsbn, Book::getName));
}
그리고 그것이 작동하는지 쉽게 검증할 수 있다.
1
2
3
4
@Test
public void whenConvertFromListToMap() {
assertTrue(convertToMap.listToMap(bookList).size() == 3);
}
2. 주요 갈등 해결
위의 예는 잘 작동했지만 중복 키가 있으면 어떻게 되는지 확인한다.
각 책의 출시 연도 별로 지도에 키를 입력했다고 가정해 본다.
1
2
3
4
public Map<Integer, Book> listToMapWithDupKeyError(List<Book> books) {
return books.stream().collect(
Collectors.toMap(Book::getReleaseYear, Function.identity()));
}
이전 도서 목록을 보면 IllegalStateException이 표시된다.
1
2
3
4
@Test(expected = IllegalStateException.class)
public void whenMapHasDuplicateKey_without_merge_function_then_runtime_exception() {
convertToMap.listToMapWithDupKeyError(bookList);
}
이 문제를 해결하려면 추가 매개변수인 mergeFunction과 함께 다른 메서드를 사용해야 한다.
1
2
3
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
충돌이 발생하는 경우 기존 항목을 유지함을 나타내는 병합 기능이다.
1
2
3
4
public Map<Integer, Book> listToMapWithDupKey(List<Book> books) {
return books.stream().collect(Collectors.toMap(Book::getReleaseYear, Function.identity(),
(existing, replacement) -> existing));
}
즉, 첫 번째 승리 동작을 얻는다.
1
2
3
4
5
6
@Test
public void whenMapHasDuplicateKeyThenMergeFunctionHandlesCollision() {
Map<Integer, Book> booksByYear = convertToMap.listToMapWithDupKey(bookList);
assertEquals(2, booksByYear.size());
assertEquals("0395489318", booksByYear.get(1954).getIsbn());
}
3. 기타 Map 유형
기본적으로 toMap()
메서드는 HashMap을 반환한다.
하지만 다른 Map 구현을 반환할 수 있다.
1
2
3
4
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier)
여기서 mapSupplier는 결과가 포함된 새로운 빈 맵을 반환하는 함수이다.
1) List to ConcurrentMap
동일한 예를 들어 ConcurrentHashMap을 반환하는 mapSupplier 함수를 추가해 본다.
1
2
3
4
public Map<Integer, Book> listToConcurrentMap(List<Book> books) {
return books.stream().collect(Collectors.toMap(Book::getReleaseYear, Function.identity(),
(o1, o2) -> o1, ConcurrentHashMap::new));
}
계속해서 코드를 테스트한다.
1
2
3
4
@Test
public void whenCreateConcurrentHashMap() {
assertTrue(convertToMap.listToConcurrentMap(bookList) instanceof ConcurrentHashMap);
}
2) Sorted Map
마지막으로 정렬된 지도를 반환하는 방법이다. 이를 위해 TreeMap을 mapSupplier 매개변수로 사용한다.
TreeMap은 기본적으로 키의 자연스러운 순서에 따라 정렬되므로 명시적으로 책을 직접 정렬할 필요가 없다.
1
2
3
4
5
public TreeMap<String, Book> listToSortedMap(List<Book> books) {
return books.stream()
.collect(
Collectors.toMap(Book::getName, Function.identity(), (o1, o2) -> o1, TreeMap::new));
}
따라서 반환된 TreeMap은 책 이름을 기준으로 알파벳 순서로 정렬된다.
1
2
3
4
5
@Test
public void whenMapisSorted() {
assertTrue(convertToMap.listToSortedMap(bookList).firstKey().equals(
"The Fellowship of the Ring"));
}