Java 8中Collectors.groupingBy方法空指针异常源码分析
Posted 然笑后端
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 8中Collectors.groupingBy方法空指针异常源码分析相关的知识,希望对你有一定的参考价值。
现在有这样的一个需求:老板让把所有的员工按年龄进行分组,然后统计各个年龄的人数。
这个需求,如果是在数据库中,可以直接使用一个 group by
语句进行统计即可,那么在 Java 中的话,可以借助于 Java 8 中 Collectors
类提供的 groupingBy()
方法来实现,groupingBy()
方法返回的是一个 Map<key, value>
集合,如果通过 groupingBy()
分组的属性 key 值为null,就会抛出空指针异常。
1、分组示例代码
首先来定义一个员工类 Staff
。
package com.magic.stream;
public class Staff
private String name;
private Integer age;
public Staff(String name, Integer age)
this.name = name;
this.age = age;
public String getName()
return name;
public void setName(String name)
this.name = name;
public Integer getAge()
return age;
public void setAge(Integer age)
this.age = age;
@Override
public String toString()
return "Staff" +
"name=" + name + \\ +
", age=" + age +
;
再创建一个 Test.java
类,用来验证将 List<Staff>
转换为 Map<String, List<Staff>>
,即按年龄将员工进行分组。
package com.magic.stream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Test
public static void main(String[] args)
List<Staff> staffs = new ArrayList<>();
staffs.add(new Staff("张三", 24));
staffs.add(new Staff("李四", 26));
staffs.add(new Staff("王五", 27));
staffs.add(new Staff("赵六", 24));
Map<Integer, List<Staff>> staffMap = staffs.stream().collect(Collectors.groupingBy(Staff::getAge));
System.out.println(staffMap);
运行程序,输出信息如下:
24=[Staffname=张三, age=24, Staffname=赵六, age=24], 26=[Staffname=李四, age=26], 27=[Staffname=王五, age=27]
如果只需要统计各个年龄的员工数量,那么可以直接使用 Collectors.counting()
方法进行统计,代码如下:
Map<Integer, Long> staffCountMap = staffs.stream().collect(Collectors.groupingBy(Staff::getAge, Collectors.counting()));
System.out.println(staffCountMap);
运行后,输出信息如下:
24=2, 26=1, 27=1
此时再向员工表中添加一个周七,但是不设置年龄,如下:
public static void main(String[] args)
List<Staff> staffs = new ArrayList<>();
staffs.add(new Staff("张三", 24));
staffs.add(new Staff("李四", 26));
staffs.add(new Staff("王五", 27));
staffs.add(new Staff("赵六", 24));
staffs.add(new Staff("周七", null));
Map<Integer, List<Staff>> staffMap = staffs.stream().collect(Collectors.groupingBy(Staff::getAge));
System.out.println(staffMap);
再次运行,此时就会抛出空指针异常,错误信息如下:
Exception in thread "main" java.lang.NullPointerException: element cannot be mapped to a null key
at java.util.Objects.requireNonNull(Objects.java:228)
at java.util.stream.Collectors.lambda$groupingBy$45(Collectors.java:907)
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 com.magic.stream.Test.main(Test.java:21)
2、异常源码分析
这个错误信息是如何报出的呢?下面一起来分析一下 Collectors.groupingBy()
这个方法的源码了。
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier)
return groupingBy(classifier, toList());
该方法的入参是 Function<? super T, ? extends K> classifier
,指分类器,也就是上面示例代码中的 Staff::getAge
,在该方法中,又调用了重载方法 groupingBy()
,其定义如下:
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream)
// 在默认情况下,使用 groupingBy 方法得到的是 HashMap 类型
// 如果希望返回的是 LinkedHashMap 或者 TreeMap,也可以参考下面的方式
return groupingBy(classifier, HashMap::new, downstream);
这个方法有两个参数,一个是 classifier
,另一个是 downstream
,这个 downstream
用于如何对分组后的数据进行归并操作,上一个方法中直接传入了 toList()
方法,但是在这个方法中,也并没有看到具体的实现,而是继续调用了另一个重载方法,其定义如下:
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream)
// 用于存放可变结果的容器
Supplier<A> downstreamSupplier = downstream.supplier();
// 用于将结果值保存到可变容器
BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
BiConsumer<Map<K, A>, T> accumulator = (m, t) ->
// 获取 key 值,对应于上面的示例中,就是调用 Staff 的 getAge() 方法获取员工的年龄
// 此处会对获取的值进行 null 校验
K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key");
A container = m.computeIfAbsent(key, k -> downstreamSupplier.get());
downstreamAccumulator.accept(container, t);
;
// 创建一个合并器
BinaryOperator<Map<K, A>> merger = Collectors.<K, A, Map<K, A>>mapMerger(downstream.combiner());
@SuppressWarnings("unchecked")
Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) mapFactory;
// 判断 finisher 函数是否为恒等函数,如果是则可以忽略,否则需要构建 finisher
if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH))
// 使用 CollectorImpl 类构建 Map 集合
return new CollectorImpl<>(mangledFactory, accumulator, merger, CH_ID);
else
@SuppressWarnings("unchecked")
Function<A, A> downstreamFinisher = (Function<A, A>) downstream.finisher();
Function<Map<K, A>, M> finisher = intermediate ->
intermediate.replaceAll((k, v) -> downstreamFinisher.apply(v));
@SuppressWarnings("unchecked")
M castResult = (M) intermediate;
return castResult;
;
return new CollectorImpl<>(mangledFactory, accumulator, merger, finisher, CH_NOID);
这个方法就是 groupingBy()
的最终实现,从上面的代码分析可以看出,在具体的分组过程中,会使用 Objects.requireNonNull()
方法对 key
值进行校验,如果 key
值为空,则会直接抛出异常了,此方法定义如下:
public static <T> T requireNonNull(T obj, String message)
if (obj == null)
throw new NullPointerException(message);
return obj;
3、异常解决方法
对于这种空指针异常,该如何处理呢?一般有两种方式:
- 排除掉空值,空值本身没有任何含义,可以去掉空值数据再进行分组;
- 将空值替换为一个默认值,再进行分组;
下面分别使用上面的两种方式改写代码:
3.1 排除掉空值
public static void main(String[] args)
List<Staff> staffs = new ArrayList<>();
staffs.add(new Staff("张三", 24));
staffs.add(new Staff("李四", 26));
staffs.add(new Staff("王五", 27));
staffs.add(new Staff("赵六", 24));
staffs.add(new Staff("周七", null));
Map<Integer, List<Staff>> staffMap = staffs.stream().filter(s -> Objects.nonNull(s.getAge())).collect(Collectors.groupingBy(Staff::getAge));
System.out.println(staffMap);
上面使用了 Objects.nonNull()
方法过滤掉了 age
字段为 null
的数据,运行程序,输出结果如下:
24=[Staffname=张三, age=24, Staffname=赵六, age=24], 26=[Staffname=李四, age=26], 27=[Staffname=王五, age=27]
3.2 将空值替换为一个默认值
public static void main(String[] args)
List<Staff> staffs = new ArrayList<>();
staffs.add(new Staff("张三", 24));
staffs.add(new Staff("李四", 26));
staffs.add(new Staff("王五", 27));
staffs.add(new Staff("赵六", 24));
staffs.add(new Staff("周七", null));
// 如果年龄为 null ,则赋值 -1,表示异常数据
staffs.stream().filter(s -> Objects.isNull(s.getAge())).forEach(s -> s.setAge(-1));
Map<Integer, List<Staff>> staffMap = staffs.stream().collect(Collectors.groupingBy(Staff::getAge));
System.out.println(staffMap);
运行程序,输出结果如下:
-1=[Staffname=周七, age=-1], 24=[Staffname=张三, age=24, Staffname=赵六, age=24], 26=[Staffname=李四, age=26], 27=[Staffname=王五, age=27]
以上是关于Java 8中Collectors.groupingBy方法空指针异常源码分析的主要内容,如果未能解决你的问题,请参考以下文章