본문 바로가기
Programing/Java

Collectors.toMap() 시 IllegalStateException 오류

by Tomining 2017. 12. 26.
Collectors.toMap 사용시 중복 key가 존재하는 경우 IllegalStateException 이 발생한다.
@Test
public void duplicateKeyError() {
  List<Category> categories = Lists.newArrayList(
    Category.builder().categoryId("KO_01").languageCode("ko").build(),
    Category.builder().categoryId("KO_02").languageCode("ko").build()
  );

  Map<String, Category> categoryMap = categories.stream().collect(toMap(Category::getLanguageCode, Function.identity()));

  assertThat(categoryMap.size(), is(1));
}
java.lang.IllegalStateException: Duplicate key Category(categoryId=KO_01, languageCode=ko, categoryName=null)

    at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
    at java.util.HashMap.merge(HashMap.java:1253)
    at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
    at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
    at tomining.java.stream.ToMapExample.duplicateKeyError(ToMapExample.java:28)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
왜 그럴까?
테스트 코드에서 사용한 toMap() 인터페이스를 살펴보자.
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
  Function<? super T, ? extends U> valueMapper) {
  return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
private static <T> BinaryOperator<T> throwingMerger() {
  return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}
mergeFunction에 throwingMerger()를 전달하고 있는 것이 확인된다.
이 부분이 IllegalStateException의 원인이다.
즉, toMap()은 기본적으로 중복이 있는 경우 Exception을 반환하게 된다. HashMap의 Key 충돌을 생각하면 쉽다.
만약 Key 중복시 기준을 제시해 줄 수 있다면 mergeFunction을 명시적으로 만들어 전달할 수도 있다.
@Test
public void mergeFunction() {
  List<Category> categories = Lists.newArrayList(
    Category.builder().categoryId("KO_01").languageCode("ko").build(),
    Category.builder().categoryId("KO_02").languageCode("ko").build()
  );

  Map<String, Category> categoryMap = categories.stream().collect(toMap(Category::getLanguageCode, Function.identity(), firstPriority()));

  assertThat(categoryMap.size(), is(1));
}

private <T> BinaryOperator<T> firstPriority() {
  return (u,v) -> { return u; };
}
위에서 구현된 mergeFunction은 중복 키가 발생한 경우 첫 번째 아이템으로 유지하는 경우이다.
동일한 기조로 Collectors.groupingBy() 라는 것을 활용할 수도 있다.


@Test
public void groupingByExample() {
  List<Category> categories = Lists.newArrayList(
    Category.builder().categoryId("KO_01").languageCode("ko").build(),
    Category.builder().categoryId("KO_02").languageCode("ko").build()
  );

  Map<String, List<Category>> categoryMap = categories.stream().collect(groupingBy(Category::getLanguageCode, toList()));

  assertThat(categoryMap.size(), is(1));
}