1. 概述
本文将深入探讨不同Java集合类型对null值的容忍度和限制。Java集合对null值的处理方式各不相同,需要开发者特别注意。
虽然ArrayList和HashMap允许存储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键(依赖比较机制)
理解这些特性有助于:
- 避免运行时NPE
- 选择合适的集合类型
- 编写更健壮的数据处理逻辑
所有示例代码可在GitHub仓库获取。