1

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

 2 years ago
source link: https://blog.51cto.com/u_15724795/5539097
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

这个需求,如果是在数据库中,可以直接使用一个 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=[Staff{name='张三', age=24}, Staff{name='赵六', age=24}], 26=[Staff{name='李四', age=26}], 27=[Staff{name='王五', 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=[Staff{name='张三', age=24}, Staff{name='赵六', age=24}], 26=[Staff{name='李四', age=26}], 27=[Staff{name='王五', 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=[Staff{name='周七', age=-1}], 24=[Staff{name='张三', age=24}, Staff{name='赵六', age=24}], 26=[Staff{name='李四', age=26}], 27=[Staff{name='王五', age=27}]}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK