引子
今天测试环境一处代码使用toMap出现了空指针异常,看了下其实很多经常使用lambda表达式进行转换的开发大多遇见过这种问题,本来这个也没什么研究了,现成的解决方案,但是大概就是好久没写东西了,天天忙的焦头烂额的时候突然想写点啥,于是在看到所有文章给出了一个几乎一样的解决方案时,想看看源码是否有其他方案。
问题
问题很简单就是List转换为Map的时候空指针报错了。
大概是一段这样的逻辑,使用toMap的三个参数:键映射、值映射、冲突解决逻辑
public static void main(String[] args) {
List<User> loop = new ArrayList<>();
for (int i = 0; i < 10; i++) {
if (i == 5) {
// 这条数据进入 JavaMain::covertName 回返回空
User user = new User(String.valueOf(5),"");
loop.add(user);
} else {
User user = new User(String.valueOf(i),"name" + String.valueOf(i));
loop.add(user);
}
}
Map<String, String> rest = loop.stream()
.collect(Collectors.toMap(User::getUserId, JavaMain::covertName, (o,n) -> n));
}
public static String covertName(User user) {
if (StringUtils.isEmpty(user.getName())) {
return null;
}
return user.getName() + user.getUserId();
}
在循环过程中需要根据数据进行value
值的设置,某些逻辑下会被设置为null
,然后会无情的抛出了空指针
Connected to the target VM, address: '127.0.0.1:13641', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:13641', transport: 'socket'
Exception in thread "main" java.lang.NullPointerException
at java.util.HashMap.merge(HashMap.java:1225)
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)
at dai.samples.jpa.config.JavaMain.main(JavaMain.java:31)
解决方式
解决方式很多人都给出了方案
HashMap<Object, Object> hashMap = loop.stream().collect(HashMap::new,
(m, v) -> m.put(v.getUserId(), covertName(v)), HashMap::putAll);
虽然系统问题解决了,但是我的问题却出来了,正常情况我们认为HashMap并不会限制我们对键和值设置为null。那既然如此为什么会出现空指针?哪里抛出的空指针?为什么另外一种方式就可以?
哪里出现了问题?
我们看下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方法进行值的设置,而不是我们以为的put。那么在HashMap的merge中,方法的第一行就告诉你,我们不接受null的值。
@Override
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
......
return value;
}
为什么另外一个方法就可以呢?
首先看另外一个方法是什么样的,首先会发现三个参数会被包裹到ReduceOps中
@Override
public final <R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super P_OUT> accumulator,
BiConsumer<R, R> combiner) {
return evaluate(ReduceOps.makeRef(supplier, accumulator, combiner));
}
这个ReduceOps干嘛的?说实话好久没看源码了我也不知道,不管了继续往下看,到这一步就很明显了,原来第一个参数seedFactory
会创建一个Map,然后第二个参数将state和循环对象作为参数传递到accumulator
,而第三个方法在参数注释以及入参的ReducingSink
已经提醒你了这个是用来并发流中合并数据的内容。
public static <T, R> TerminalOp<T, R>
makeRef(Supplier<R> seedFactory,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R,R> reducer) {
Objects.requireNonNull(seedFactory);
Objects.requireNonNull(accumulator);
Objects.requireNonNull(reducer);
class ReducingSink extends Box<R>
implements AccumulatingSink<T, R, ReducingSink> {
@Override
public void begin(long size) {
state = seedFactory.get();
}
@Override
public void accept(T t) {
accumulator.accept(state, t);
}
@Override
public void combine(ReducingSink other) {
reducer.accept(state, other.state);
}
}
return new ReduceOp<T, R, ReducingSink>(StreamShape.REFERENCE) {
@Override
public ReducingSink makeSink() {
return new ReducingSink();
}
};
}
现在明白为什么使用第二个方式可以了,重点在于第二个参数。使用toMap的时候java默认使用Map的merge方法这使得我们没办法去设置null的值,而第二种方式我们可以在第二个参数中自己实现设置值的方式,比如我们可以下面这样,让我们用另外一种方式再错一次,笑:)
HashMap<Object, Object> hashMap =
loop.parallelStream().collect(HashMap::new,
(m, v) -> m.merge(v.getUserId(), Objects.requireNonNull(covertName(v)), (o,n) -> n),
HashMap::putAll);
toMap的另外一个问题
回到这个方法,我们没办法设置值为null,那么如果在最后一个参数(o,n) -> n
中返回null会如何呢?
Map<String, String> rest = loop.stream()
.collect(Collectors.toMap(User::getUserId, JavaMain::covertName, (o,n) -> Objects.equals(n,o)?null:n));
在HashMap的merge中有这么一段逻辑
@Override
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
......
if (old != null) {
V v;
if (old.value != null)
v = remappingFunction.apply(old.value, value);
else
v = value;
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
return v;
}
.....
return value;
}
这个时候会发现如果合并后的值为null的时候并不会将null设置进去而是直接移除掉这个节点。就像下面的例子:
public static void main(String[] args) {
List<User> loop = new ArrayList<>();
for (int i = 0; i < 10; i++) {
if (i == 5 || i == 6) {
User user = new User(String.valueOf(5),"name" + String.valueOf(5));
loop.add(user);
} else {
User user = new User(String.valueOf(i),"name" + String.valueOf(i));
loop.add(user);
}
}
Map<String, String> rest = loop.stream()
.collect(Collectors.toMap(User::getUserId, JavaMain::covertName,(n, o) -> Objects.equals(n,o)?null:n));
System.out.println(JSON.toJSON(rest.keySet()));
}
这个时候返回的key中不仅仅6不见了连5也不见了。
["0","1","2","3","4","7","8","9"]
所以这个时候使用的时候一定要注意否则在后续进行key值进行判断的时候,有可能导致错误的数量。
ps.磨磨唧唧写了一个多小时,emmmm,感觉没啥卵用的知识+1了