1. 概述

本文将深入探讨不同Java集合类型对null值的容忍度和限制。Java集合对null值的处理方式各不相同,需要开发者特别注意。

虽然ArrayListHashMap允许存储null值,但在访问时可能触发NullPointerException。相比之下,TreeMap则完全禁止null键。理解这些差异至关重要,尤其是在使用Stream或处理集合时,能有效避免运行时错误。合理的空值检查是构建健壮代码的基础。

2. List中的null值

List可以包含null元素。我们可以安全地添加和检索null值,但检索后必须进行空值检查,否则可能踩坑。

首先定义一个可复用的lambda表达式来统计集合中的null值数量:

Function<Collection<?>, Long> countNulls
    = collection -> collection.stream().filter(Objects::isNull).count();

这个表达式将集合转换为流,通过filter(Objects::isNull)保留空元素,最后用count()统计数量。接下来测试List的null处理:

@Test
void givenList_whenNullValueAdded_doesNotFail() {
    // 添加null值到列表
    Integer[] numberArray = { null, 0, 1, null, 2, 3, null };
    List<Integer> numbers = Arrays.asList(numberArray);
    assertEquals(3, countNulls.apply(numbers));

    // 从列表获取null值
    Integer number = numbers.get(0);
    assertNull(number);

    // 解引用null值会抛出异常
    assertThrows(NullPointerException.class, () -> number.toString());
}

测试验证了添加null值不会报错,统计函数准确计数,获取null元素返回null,但解引用操作会触发NullPointerException

3. Set中的null值

Set要求元素唯一,因此最多只能包含一个null值。 这种特性确保了数据完整性,但并非所有Set实现都允许null值。

HashSet:基于HashMap实现,可以容纳一个null值。
TreeSet:依赖Comparable或Comparator,完全禁止null值(尝试添加会抛出NPE)。

先测试HashSet的null处理:

@Test
void givenHashSet_whenNullValueAdded_doesNotFail() {
    // 添加null值到HashSet
    Integer[] numberArray = { null, 0, 1, null, 2, 3, null };
    Set<Integer> numbers = new HashSet<>(Arrays.asList(numberArray));
    assertEquals(1, countNulls.apply(numbers));

    // 检查null值存在性
    assertTrue(numbers.contains(null));

    // 遍历时解引用null值会抛出异常
    assertThrows(NullPointerException.class, () -> numbers.forEach(Object::toString()));
}

测试证明HashSet会忽略重复的null值,正确保留一个null,但遍历解引用时仍会触发NPE。

再看TreeSet的严格限制:

@Test
void givenTreeSet_whenNullValueAdded_mightFail() {
    // 添加null值到TreeSet
    Integer[] numberArray = { null, 0, 1, null, 2, 3, null };
    assertThrows(NullPointerException.class, () -> new TreeSet<>(Arrays.asList(numberArray)));
}

TreeSet在初始化时直接抛出NPE,因为无法对null进行排序比较。

4. Map中的null值

HashMap允许存储一个null键和多个null值。 null键会被存放在专门的哈希桶中,使用get()方法获取null键关联值时不会报错。但解引用操作仍需谨慎:

@Test
void givenHashMap_whenNullKeyValueAdded_doesNotFail() {
    // 添加null键和null值
    Integer[] numberArray = { null, 0, 1, null, 2, 3, null };
    Map<Integer, Integer> numbers = new HashMap<>();
    Arrays.stream(numberArray)
        .forEach(integer -> numbers.put(integer, integer));
    assertEquals(1, countNulls.apply(numbers.keySet()));
    assertEquals(1, countNulls.apply(numbers.values()));

    // 检查null键值存在性
    assertTrue(numbers.containsKey(null));
    assertTrue(numbers.containsValue(null));
    assertNull(numbers.get(null));

    // 解引用null值会抛出异常
    assertThrows(NullPointerException.class, () -> numbers.get(null)
        .toString());
}

测试验证了HashMap对null键值的完整支持,但解引用操作仍需防御性编程。

TreeMap对null的处理则更严格:

@Test
void givenTreeMap_whenNullKeyAdded_fails() {
    Map<Integer, Integer> numbers = new TreeMap<>();

    // 添加null键(无论值是否为null)都会失败
    assertThrows(NullPointerException.class, () -> numbers.put(null, null));
    assertThrows(NullPointerException.class, () -> numbers.put(null, 1));

    // 非null键可以关联null值
    assertDoesNotThrow(() -> numbers.put(1, null));
    assertDoesNotThrow(() -> numbers.put(1, 1));
}

⚠️ 关键区别

  • ❌ TreeMap禁止null键(排序时无法比较)
  • ✅ 允许非null键关联null

5. 总结

本文系统分析了Java集合对null值的处理差异:

  • 灵活派:ArrayList/HashSet/HashMap 允许null值(需注意解引用风险)
  • 严格派:TreeSet/TreeMap 禁止null键(依赖比较机制)

理解这些特性有助于:

  1. 避免运行时NPE
  2. 选择合适的集合类型
  3. 编写更健壮的数据处理逻辑

所有示例代码可在GitHub仓库获取。


原始标题:Java Collections and null Values: Tolerance and Restrictions | Baeldung