在使用java8 中Collectors.toMap的时候出现了异常,具体异常如下:
Exception in thread "main" java.lang.IllegalStateException: Duplicate key
该异常字面意思是有重复的key,但是使用 Collectors.toMap 表示将数据集合转换为map,map的key如果出现hash冲突则会覆盖,不明所以写了一个main方法测试。
@Data
public static class Student{
private Integer id;
private String name;
}
public static void main(String[] rags){
//2个student对象 有id,name属性。相同ID 不同的name,看是否会出现重复keyY异常。
List<Student> studentList = new ArrayList<>();
Student student1 = new Student();
student1.setId(2);//重复ID
student1.setName("a");
Student student2 = new Student();
student2.setId(2);//重复ID
student2.setName("b");
studentList.add(student1);
studentList.add(student2);
Map<Integer, Student> map1 = studentList.stream().collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(map1);
}
执行结果:
Exception in thread "main" java.lang.IllegalStateException: Duplicate key a
at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
at java.util.HashMap.merge(HashMap.java:1254)
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:1382)
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)
果然出现了重复key冲突的异常,进入源码跟踪。
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);
}
toMap2个入参一个keyMapper,一个valueMapper,但是出来一个throwingMerger()?先不管继续进入toMap方法查看。
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}
进入toMap方法没有太多逻辑执行的是map的merge方法。继续进入查看。
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);//非空判断 外面肯定传入了
Objects.requireNonNull(value);//value 也不能为空?
V oldValue = get(key);//查找有这个key是否已经有值,返回值声明为oldValue
//关键点,如果oldValue==null使用新value,有值则直接执行传入的函数
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if(newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
如代码注释中描述,
1.查找有这个key是否已经有值,返回值声明为oldValue
2. 如果oldValue==null使用新value,有值则直接执行传入的函数
3.value不能为空,否则抛出空指针异常。
传入的函数就是刚开始toMap的时候看到的throwingMerger()方法,所以现在进入看下是干什么的。
private static <T> BinaryOperator<T> throwingMerger() {
return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}
结合该函数调用apply的时候remappingFunction.apply(oldValue, value),可以理解为
就是抛出一个异常并且告诉你那个key重复了。。。。
一时间让我对map可以重复key有所误解还是java8对map有什么误解有所怀疑。
再回到最初toMap的方法上查看注释
* <p>If the mapped keys contains duplicates (according to
* {@link Object#equals(Object)}), an {@code IllegalStateException} is
* thrown when the collection operation is performed. If the mapped keys
* may have duplicates, use {@link #toMap(Function, Function, BinaryOperator)}
* instead.
//如果映射key包含重复的,则会抛出IllegalStateException,
* @apiNote
* It is common for either the key or the value to be the input elements.
* In this case, the utility method
* {@link java.util.function.Function#identity()} may be helpful.
* For example, the following produces a {@code Map} mapping
* students to their grade point average:
* <pre>{@code
* Map<Student, Double> studentToGPA
* students.stream().collect(toMap(Functions.identity(),
* student -> computeGPA(student)));
* }</pre>
* And the following produces a {@code Map} mapping a unique identifier to
* students:
* <pre>{@code
* Map<String, Student> studentIdToStudent
* students.stream().collect(toMap(Student::getId,
* Functions.identity());
* }</pre>
*
翻一下大致为如果映射key包含重复的,则会抛出IllegalStateException,通过函数编程。
而紧接着又给出了该情况下的解决办法,可以我们传入一个对比函数进行处理发生merge的情况。
所以代码变更为:
Map<Integer, String> map1 = studentList.stream().collect(Collectors.toMap(Student::getId,Student::getName ,(s,s2)->s2));
System.out.println(map1);//{2=b}
所以在出现重复key的时候对应mergeFunction 就是我们toMap中第三个参数的函数。
查了下openJDK,这个问题之前有人提过bug,并且在JDK1.9修复了。
总结: 1.使用toMap的时候如果会有重复key的情况要自己指定处理方式不然会报错。 2.toMap不允许空value. 使用任何新功能前还是要先看文档呀。
关注我的公众号 LearnMoreDoMore 可以及时看到更多技术分享文章(不会该死的卖课广告)。