天天看點

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=[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.eval(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.eval(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);
}
           
{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}]}