您的位置 首页 java

Java 8中Collectors.groupingBy方法空指针异常源码分析

现在有这样的一个需求: 老板让把所有的员工按年龄进行分组,然后统计各个年龄的人数 。

这个需求,如果是在数据库中,可以直接使用一个 group by 语句进行统计即可,那么在 Java 中的话,可以借助于 java 8 中 Collectors 类提供的 groupingBy() 方法来实现, groupingBy() 方法返回的是一个 Map<key, value> 集合,如果通过 groupingBy() 分组的属性 key 值为null,就会抛出空指针异常。

1、分组示例代码

首先来定义一个员工类 Staff

 <pre class="prettyprint hljs java" style=" padding : 0.5em; font-family: Menlo, Monaco, Consolas, " Courier  New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block;  margin : 0px 0px 1.5em; font-size: 14px;  line-height : 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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>> ,即按年龄将员工进行分组。

 <pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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);
    }
}
  

运行程序,输出信息如下:

 <pre class="prettyprint hljs xquery" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">{24=[Staff{name='张三', age=24}, Staff{name='赵六', age=24}], 26=[Staff{name='李四', age=26}], 27=[Staff{name='王五', age=27}]}<
  

如果只需要统计各个年龄的员工数量,那么可以直接使用 Collectors.counting() 方法进行统计,代码如下:

 <pre class="prettyprint hljs  gradle " style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">Map<Integer, Long> staffCountMap = staffs.stream().collect(Collectors.groupingBy(Staff::getAge, Collectors.counting()));
System.out.println(staffCountMap);
  

运行后,输出信息如下:

 <pre class="hljs clojure" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 0.75em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">{24=2, 26=1, 27=1}
  

此时再向员工表中添加一个周七,但是不设置年龄,如下:

 <pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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);
}
  

再次运行,此时就会抛出空指针异常,错误信息如下:

 <pre class="prettyprint hljs vim" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;"> 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.forEa CH Remaining(ArrayList.java:1374)
    at java.util.stream.AbstractPipeline. copy Into(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() 这个方法的源码了。

 <pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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() ,其定义如下:

 <pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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() 方法,但是在这个方法中,也并没有看到具体的实现,而是继续调用了另一个重载方法,其定义如下:

 <pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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);
    };
    // 创建一个合并器
    Binary Operator <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 值为空,则会直接抛出异常了,此方法定义如下:

 <pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">public static <T> T requireNonNull(T obj, String message) {
    if (obj == null)
        throw new NullPointerException(message);
    return obj;
}
  

3、异常解决方法

对于这种空指针异常,该如何处理呢?一般有两种方式:

  • 排除掉空值,空值本身没有任何含义,可以去掉空值数据再进行分组;
  • 将空值替换为一个默认值,再进行分组;

下面分别使用上面的两种方式改写代码:

3.1 排除掉空值

 <pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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 的数据,运行程序,输出结果如下:

 <pre class="prettyprint hljs xquery" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">{24=[Staff{name='张三', age=24}, Staff{name='赵六', age=24}], 26=[Staff{name='李四', age=26}], 27=[Staff{name='王五', age=27}]}
  

3.2 将空值替换为一个默认值

 <pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto;">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);
}
  

运行程序,输出结果如下:

 <pre class="prettyprint hljs xquery" style="padding: 0.5em; font-family: Menlo, Monaco,   

文章来源:智云一二三科技

文章标题:Java 8中Collectors.groupingBy方法空指针异常源码分析

文章地址:https://www.zhihuclub.com/191076.shtml

关于作者: 智云科技

热门文章

网站地图