65. 集合中的 Null 值处理
一、Null 值的基本概念
Null 值的定义
Null 值在 Java 中表示一个不存在的对象引用。它是引用类型的默认值,表示变量没有指向任何对象实例。Null 不是基本数据类型(如 int、boolean),而是所有引用类型(如 String、List)的特殊值。
Null 值的特点
- 默认值:类成员变量(引用类型)未初始化时,默认值为 null。
- 类型无关:所有引用类型变量都可以赋值为 null。
- 内存表现:null 不占用堆内存(无对象实例),仅占用栈内存(引用变量空间)。
常见场景
-
显式初始化:
String s = null;
- 方法返回:当方法找不到有效对象时返回 null。
- 集合元素:允许作为元素存入 List/Map(除非显式限制)。
注意事项
- NullPointerException:对 null 调用方法/属性会抛出此异常。
-
防御性编程:
if (obj != null) { obj.method(); }
-
Optional 类:Java 8+ 推荐用
Optional<T>
显式处理可能为 null 的情况。
示例代码
List<String> list = new ArrayList<>();
list.add(null); // 允许存入null
System.out.println(list.get(0).length()); // 抛出NullPointerException
Null 与空集合的区别
概念定义
-
Null:表示一个引用变量没有指向任何对象,即该变量未初始化或显式赋值为
null
。 -
空集合:表示集合对象已初始化,但其中不包含任何元素(如
new ArrayList<>()
或Collections.emptyList()
)。
核心区别
-
内存分配:
-
null
不占用集合对象的内存。 - 空集合会分配对象内存(如
ArrayList
的初始容量为 10,但元素数为 0)。
-
-
操作行为:
- 调用
null
集合的方法(如size()
)会抛出NullPointerException
。 - 空集合可以安全调用方法(如
isEmpty()
返回true
)。
- 调用
使用场景
-
Null:通常表示“未初始化”或“无意义值”(如查询数据库无结果时可能返回
null
)。 -
空集合:表示“存在但无数据”(如用户权限列表为空时返回
Collections.emptyList()
)。
示例代码
List<String> nullList = null;
List<String> emptyList = new ArrayList<>();
// 操作对比
System.out.println(nullList == null); // 输出 true
System.out.println(emptyList.isEmpty()); // 输出 true
// 危险操作(抛出 NullPointerException)
// System.out.println(nullList.size());
注意事项
-
防御性编程:
- 方法返回值优先返回空集合而非
null
(避免调用方漏判null
)。 - 使用
Optional
或@Nullable
注解明确可能为null
的场景。
- 方法返回值优先返回空集合而非
-
性能影响:
- 频繁创建空集合可能产生微小开销,可复用
Collections.emptyList()
等不可变空集合。
- 频繁创建空集合可能产生微小开销,可复用
-
API 设计原则:
- 如 Google Guava 等库强制约定“集合返回值不为
null
”,减少歧义。
- 如 Google Guava 等库强制约定“集合返回值不为
Null 在 Java 中的语义
概念定义
Null 在 Java 中表示一个引用变量不指向任何对象。它是所有引用类型的默认值,可以赋值给任何对象引用变量,但不能赋值给基本数据类型(如 int、double 等)。Null 是一个特殊的关键字,表示“无”或“不存在”。
使用场景
- 初始化引用变量:当声明一个引用变量但暂时不需要指向具体对象时,可以初始化为 null。
- 表示缺失或无效值:例如,数据库查询可能返回 null 表示没有找到记录。
- 释放对象引用:将变量设置为 null 可以帮助垃圾回收器回收不再使用的对象。
常见误区与注意事项
- NullPointerException:最常见的运行时异常之一,发生在尝试调用 null 对象的方法或访问其属性时。
-
与空集合混淆:null 集合表示引用不存在,而空集合(如
new ArrayList()
)表示存在但内容为空。 - 重载方法调用:传递 null 参数时,编译器可能无法确定调用哪个重载方法,导致编译错误。
示例代码
String str = null; // 合法,str 不指向任何对象
if (str == null) {
System.out.println("str is null");
}
// 以下代码会抛出 NullPointerException
try {
System.out.println(str.length());
} catch (NullPointerException e) {
System.out.println("Cannot call methods on null");
}
二、集合中允许 Null 值的情况
List 接口实现类对 Null 的支持
概念定义
List 接口的实现类在 Java 中用于存储有序的元素集合。不同的实现类对 null
值的处理方式有所不同,主要体现在是否允许存储 null
值以及如何处理 null
值。
主要实现类对 Null 的支持
ArrayList
-
支持
null
值:允许存储多个null
值。 -
示例代码:
List<String> list = new ArrayList<>(); list.add(null); // 允许 list.add("Hello"); list.add(null); // 允许 System.out.println(list); // 输出: [null, Hello, null]
LinkedList
-
支持
null
值:允许存储多个null
值。 -
示例代码:
List<String> list = new LinkedList<>(); list.add(null); // 允许 list.add("World"); list.add(null); // 允许 System.out.println(list); // 输出: [null, World, null]
Vector
-
支持
null
值:允许存储多个null
值。 -
示例代码:
List<String> list = new Vector<>(); list.add(null); // 允许 list.add("Vector"); list.add(null); // 允许 System.out.println(list); // 输出: [null, Vector, null]
CopyOnWriteArrayList
-
支持
null
值:允许存储多个null
值。 -
示例代码:
List<String> list = new CopyOnWriteArrayList<>(); list.add(null); // 允许 list.add("CopyOnWrite"); list.add(null); // 允许 System.out.println(list); // 输出: [null, CopyOnWrite, null]
Arrays.asList() 返回的 List
-
支持
null
值:允许存储null
值,但需要注意该 List 是固定大小的。 -
示例代码:
List<String> list = Arrays.asList("A", null, "B"); System.out.println(list); // 输出: [A, null, B]
注意事项
-
null
值的比较:- 在使用
contains()
、indexOf()
等方法时,可以传入null
进行查找。 - 示例:
List<String> list = new ArrayList<>(); list.add(null); System.out.println(list.contains(null)); // 输出: true System.out.println(list.indexOf(null)); // 输出: 0
- 在使用
-
排序时的
null
值:- 使用
Collections.sort()
时,如果列表包含null
值,会抛出NullPointerException
。 - 解决方法:使用自定义
Comparator
处理null
值。 - 示例:
List<String> list = new ArrayList<>(); list.add("A"); list.add(null); list.add("B"); Collections.sort(list, Comparator.nullsFirst(Comparator.naturalOrder())); System.out.println(list); // 输出: [null, A, B]
- 使用
-
并发修改:
- 在多线程环境中,
ArrayList
和LinkedList
不是线程安全的,null
值的操作可能导致并发问题。
- 在多线程环境中,
总结
大多数 List
实现类允许存储 null
值,但在使用时需要注意 null
值的比较、排序和并发问题。
Set 接口实现类对 Null 的支持
Set 接口的实现类在 Java 中用于存储不重复的元素。不同的实现类对 null 值的支持有所不同,以下是常见 Set 实现类对 null 值的处理方式:
HashSet
- 支持 null 值:允许存储一个 null 元素。
- 原因:基于哈希表实现,null 有特殊的哈希值(0)。
-
示例代码:
Set<String> set = new HashSet<>(); set.add(null); // 允许 System.out.println(set.contains(null)); // 输出 true
LinkedHashSet
- 支持 null 值:允许存储一个 null 元素。
- 原因:继承自 HashSet,行为一致。
-
示例代码:
Set<String> set = new LinkedHashSet<>(); set.add(null); // 允许
TreeSet
-
不支持 null 值:添加 null 会抛出
NullPointerException
。 -
原因:基于红黑树实现,需要元素可比较(实现
Comparable
或提供Comparator
)。 -
示例代码:
Set<String> set = new TreeSet<>(); set.add(null); // 抛出 NullPointerException
CopyOnWriteArraySet
- 支持 null 值:允许存储一个 null 元素。
- 原因:基于数组实现,不依赖哈希或比较。
-
示例代码:
Set<String> set = new CopyOnWriteArraySet<>(); set.add(null); // 允许
ConcurrentSkipListSet
-
不支持 null 值:添加 null 会抛出
NullPointerException
。 - 原因:基于跳表实现,需要元素可比较。
-
示例代码:
Set<String> set = new ConcurrentSkipListSet<>(); set.add(null); // 抛出 NullPointerException
注意事项
- 唯一性:所有 Set 实现类中,null 最多只能存在一个(如多次添加,仅保留一个)。
-
线程安全:
CopyOnWriteArraySet
和ConcurrentSkipListSet
是线程安全的,但后者不支持 null。 -
性能影响:在
HashSet
或LinkedHashSet
中使用 null 不会显著影响性能,但需注意逻辑处理。
示例代码(综合对比)
public class SetNullExample {
public static void main(String[] args) {
testSet(new HashSet<>(), "HashSet");
testSet(new LinkedHashSet<>(), "LinkedHashSet");
testSet(new TreeSet<>(), "TreeSet");
testSet(new CopyOnWriteArraySet<>(), "CopyOnWriteArraySet");
testSet(new ConcurrentSkipListSet<>(), "ConcurrentSkipListSet");
}
static void testSet(Set<String> set, String setName) {
try {
set.add(null);
System.out.println(setName + " supports null: " + set.contains(null));
} catch (Exception e) {
System.out.println(setName + " throws: " + e.getClass().getSimpleName());
}
}
}
Map 接口实现类对 Null 的支持
HashMap
-
键值支持:允许
null
作为键和值。 -
注意事项:
- 只能有一个
null
键(键唯一性)。 - 多次插入
null
键会覆盖旧值。
- 只能有一个
-
示例代码:
Map<String, String> map = new HashMap<>(); map.put(null, "value1"); // 允许 map.put("key", null); // 允许
LinkedHashMap
-
行为继承:与
HashMap
一致,支持null
键和值。 -
特性:保留插入顺序,但对
null
的处理逻辑与HashMap
相同。
TreeMap
-
键值限制:
-
键:不允许
null
(因依赖Comparable
或Comparator
排序,调用compareTo()
会抛出NullPointerException
)。 -
值:允许
null
。
-
键:不允许
-
示例代码:
Map<String, String> treeMap = new TreeMap<>(); treeMap.put("key", null); // 允许 // treeMap.put(null, "value"); // 抛出 NullPointerException
ConcurrentHashMap
-
线程安全限制:
-
键和值:均不允许
null
(避免并发场景下的歧义)。
-
键和值:均不允许
-
原因:
get(key)
返回null
时无法区分“键不存在”还是“键映射到null
”。
Hashtable
-
历史遗留限制:
-
键和值:均不允许
null
(早期设计约束)。
-
键和值:均不允许
-
对比:与
ConcurrentHashMap
类似,但出于不同原因(非并发设计)。
其他实现(如 EnumMap)
-
键限制:枚举类型键不允许
null
(编译时检查)。 -
值支持:允许
null
。
总结表格
实现类 | 允许 null 键 |
允许 null 值 |
原因/备注 |
---|---|---|---|
HashMap |
✅ | ✅ | 哈希计算特殊处理 null 键 |
LinkedHashMap |
✅ | ✅ | 继承 HashMap 行为 |
TreeMap |
❌ | ✅ | 排序依赖非 null 键 |
ConcurrentHashMap |
❌ | ❌ | 避免并发歧义 |
Hashtable |
❌ | ❌ | 早期设计约束 |
EnumMap |
❌ | ✅ | 键为枚举类型(编译时检查) |
三、集合中不允许 Null 值的情况
线程安全集合对 Null 的限制
概念定义
线程安全集合是 Java 并发编程中用于多线程环境下安全操作数据的集合类。部分线程安全集合对 null
值有明确限制,禁止存储 null
值或 null
键,以避免潜在的并发问题和歧义。
常见线程安全集合对 Null 的限制
-
ConcurrentHashMap
-
键和值均不允许为
null
原因:ConcurrentHashMap
的设计中,null
可能表示“键不存在”或“值未初始化”,多线程环境下无法区分这两种情况,易引发歧义。ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); map.put("key", null); // 抛出 NullPointerException
-
键和值均不允许为
-
CopyOnWriteArrayList / CopyOnWriteArraySet
-
允许
null
值
但需注意:频繁插入null
可能影响可读性和逻辑判断。
-
允许
-
BlockingQueue 实现类(如 ArrayBlockingQueue)
-
多数实现禁止
null
值
原因:null
通常用作队列的特殊标记(如“终止信号”),禁止null
可避免混淆。BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); queue.offer(null); // 抛出 NullPointerException
-
多数实现禁止
注意事项
-
替代方案
- 若需表示“空值”,可使用
Optional.empty()
或自定义标记对象(如EMPTY_OBJECT
)。
- 若需表示“空值”,可使用
-
性能影响
- 对
null
的检查可能增加少量性能开销,但能提升代码健壮性。
- 对
-
文档查阅
- 使用线程安全集合时,务必查阅官方文档确认其对
null
的支持情况。
- 使用线程安全集合时,务必查阅官方文档确认其对
特殊集合类对 Null 的限制
在 Java 集合框架中,某些特殊集合类对 null
值的处理有明确的限制,了解这些限制可以避免运行时异常和逻辑错误。
1. ConcurrentHashMap
-
限制:不允许
null
键或null
值。 -
原因:并发环境下,
null
可能引发歧义(例如,get(key)
返回null
时无法区分是键不存在还是值为null
)。 -
示例代码:
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); map.put("key", null); // 抛出 NullPointerException
2. Hashtable
-
限制:不允许
null
键或null
值。 -
原因:设计早期出于线程安全的考虑(与
ConcurrentHashMap
类似)。 -
示例代码:
Hashtable<String, String> table = new Hashtable<>(); table.put(null, "value"); // 抛出 NullPointerException
3. TreeSet
/TreeMap
-
限制:不允许
null
键(若使用自然排序),但允许null
值(TreeMap
)。 -
原因:依赖
Comparable
或Comparator
进行排序,null
无法比较。 -
示例代码:
TreeSet<String> set = new TreeSet<>(); set.add(null); // 抛出 NullPointerException(自然排序时)
4. ArrayDeque
-
限制:不允许
null
元素。 -
原因:
null
被用作内部操作的标记值。 -
示例代码:
ArrayDeque<String> deque = new ArrayDeque<>(); deque.add(null); // 抛出 NullPointerException
5. 单元素集合工具方法
-
限制:
Collections.singletonList()
、Collections.singletonMap()
等不允许null
值。 -
原因:设计为不可变集合,初始化时需明确非
null
值。 -
示例代码:
List<String> list = Collections.singletonList(null); // 抛出 NullPointerException
注意事项
-
替代方案:若需存储
null
,可改用HashMap
、ArrayList
等普通集合类。 -
文档检查:使用特殊集合前,应查阅其文档确认
null
支持情况。 -
自定义处理:通过包装类或 Optional 显式处理可能的
null
值。
第三方集合库对 Null 值的限制
概念定义
第三方集合库(如 Guava、Apache Commons Collections 等)通常对 null
值有明确的限制或支持策略。这些库通过设计约束或运行时检查,强制开发者明确处理 null
,以提高代码的健壮性和可读性。
常见库的限制策略
1. Google Guava
-
禁止
null
:大多数 Guava 集合(如ImmutableList
、ImmutableSet
)直接禁止null
值,插入null
会抛出NullPointerException
。 -
明确支持
null
:少数类(如HashMultimap
)允许null
,但需在文档中显式声明。 -
工具方法:提供
Optional<T>
作为null
的安全替代方案。
示例代码:
// 尝试向 ImmutableList 添加 null 会抛出异常
ImmutableList<String> list = ImmutableList.of("a", null); // 抛出 NullPointerException
2. Apache Commons Collections
-
部分支持
null
:如ListUtils
允许null
,但某些操作(如CollectionUtils.filter()
)可能因null
抛出异常。 - 文档模糊:需仔细阅读具体类的文档确认限制。
使用场景
-
防御性编程:使用禁止
null
的集合(如 Guava)可减少空指针异常。 -
数据清洗:在接收外部数据时,通过
Preconditions.checkNotNull
显式校验。
注意事项
-
性能影响:
null
检查可能增加微小开销。 -
序列化兼容性:允许
null
的集合在跨系统传输时需额外处理。 -
文档优先:始终查阅第三方库的官方文档确认其
null
策略。
示例:Guava 的 Optional
替代方案
Optional<String> optionalValue = Optional.fromNullable(getNullableInput());
if (optionalValue.isPresent()) {
System.out.println(optionalValue.get());
}
四、Null 值带来的问题
NullPointerException 风险
概念定义
NullPointerException(NPE)是 Java 中常见的运行时异常,当程序试图访问或操作一个 null
引用时抛出。在集合操作中,NPE 风险主要来源于:
- 集合本身为
null
- 集合元素为
null
- 对
null
元素进行操作(如调用方法)
常见触发场景
- 未初始化集合:
List<String> list = null;
int size = list.size(); // NPE
- 添加 null 元素:
List<String> list = new ArrayList<>();
list.add(null);
String s = list.get(0).toUpperCase(); // NPE
- Map 的 key/value 为 null:
Map<String, String> map = new HashMap<>();
map.put(null, "value"); // 允许但危险
String v = map.get(null).trim(); // 可能NPE
防御性编程方案
- 集合判空:
if (list != null && !list.isEmpty()) {
// 安全操作
}
- 使用 Optional:
Optional.ofNullable(list)
.orElse(Collections.emptyList())
.forEach(item -> System.out.println(item));
- 使用 Objects.requireNonNull:
List<String> safeList = Objects.requireNonNull(list, "List不能为null");
- 集合工具类:
// Apache Commons
CollectionUtils.emptyIfNull(list);
// Guava
Iterables.filter(list, Predicates.notNull());
最佳实践
- 明确约定集合是否允许
null
元素(如ConcurrentHashMap
不允许null
值) - 使用
@NonNull
/@Nullable
注解(JSR-305) - 优先返回空集合而非
null
(Collections.emptyList()
) - Java 8+ 推荐使用
Optional
包装可能为null
的返回值
注意事项
-
ConcurrentHashMap
和Hashtable
的key/value
均不能为null
-
TreeSet
/TreeMap
对null
的检查取决于 Comparator 实现 - 使用
contains(null)
前需确保集合本身非null
集合操作中的异常情况
1. NullPointerException
-
定义:当尝试对
null
集合进行操作时抛出。 -
常见场景:
- 调用
null
集合的add()
、remove()
等方法。 - 使用
for-each
循环遍历null
集合。
- 调用
-
示例代码:
List<String> list = null; list.add("item"); // 抛出 NullPointerException
-
解决方法:
- 初始化集合:
List<String> list = new ArrayList<>();
- 使用
Objects.requireNonNull()
提前校验。
- 初始化集合:
2. ConcurrentModificationException
- 定义:在迭代过程中修改集合结构(如删除元素)时抛出。
-
常见场景:
- 使用
for-each
或Iterator
时直接调用集合的remove()
。
- 使用
-
示例代码:
List<String> list = new ArrayList<>(Arrays.asList("A", "B")); for (String s : list) { list.remove(s); // 抛出 ConcurrentModificationException }
-
解决方法:
- 使用
Iterator.remove()
:Iterator<String> it = list.iterator(); while (it.hasNext()) { it.next(); it.remove(); // 安全删除 }
- 使用
3. UnsupportedOperationException
- 定义:调用集合不支持的操作时抛出(如不可变集合的修改操作)。
-
常见场景:
- 对
Arrays.asList()
或Collections.unmodifiableList()
返回的集合调用add()
。
- 对
-
示例代码:
List<String> list = Arrays.asList("A", "B"); list.add("C"); // 抛出 UnsupportedOperationException
-
解决方法:
- 创建可修改的新集合:
new ArrayList<>(Arrays.asList("A", "B"))
。
- 创建可修改的新集合:
4. IndexOutOfBoundsException
- 定义:访问超出集合范围的索引时抛出。
-
常见场景:
- 对空集合调用
get(0)
。 - 使用无效的下标(如负数或
>= size()
)。
- 对空集合调用
-
示例代码:
List<String> list = new ArrayList<>(); String s = list.get(0); // 抛出 IndexOutOfBoundsException
-
解决方法:
- 检查集合非空:
if (!list.isEmpty()) { ... }
- 校验索引范围:
index >= 0 && index < list.size()
。
- 检查集合非空:
5. ClassCastException
- 定义:类型转换失败时抛出(如泛型集合中混入错误类型)。
-
常见场景:
- 原始类型集合与泛型集合混用。
-
示例代码:
List list = new ArrayList(); list.add(123); List<String> strList = list; // 编译通过,但运行时可能抛出 ClassCastException
-
解决方法:
- 避免使用原始类型,始终声明泛型。
6. IllegalArgumentException
- 定义:参数不合法时抛出(如初始容量为负数)。
-
常见场景:
- 创建集合时传入非法参数:
new ArrayList<>(-1)
。
- 创建集合时传入非法参数:
-
解决方法:
- 校验参数有效性(如容量必须 ≥ 0)。
数据一致性问题
概念定义
数据一致性指在分布式系统或并发环境中,多个数据副本或事务操作后,数据保持逻辑正确和同步的状态。在集合操作中表现为:当多个线程或操作同时修改集合时,可能导致数据丢失、重复或状态不一致。
使用场景
-
多线程环境:如
ArrayList
被多个线程同时修改时可能抛出ConcurrentModificationException
。 - 分布式缓存:如 Redis 集群中多个节点数据同步。
- 数据库事务:如订单和库存的跨表操作需保证原子性。
常见误区
-
误用非线程安全集合:如直接使用
HashMap
而非ConcurrentHashMap
。 -
忽略原子操作:如先
contains()
再add()
的非原子组合。 -
过度同步:滥用
synchronized
导致性能下降。
解决方案示例
线程安全集合
// 非线程安全(错误示例)
List<String> unsafeList = new ArrayList<>();
// 线程安全(推荐)
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> safeMap = new ConcurrentHashMap<>();
显式同步
List<String> list = new ArrayList<>();
// 使用 synchronized 代码块
synchronized(list) {
if (!list.contains("Java")) {
list.add("Java");
}
}
原子操作
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 原子性更新
map.compute("count", (k, v) -> (v == null) ? 1 : v + 1);
注意事项
-
性能权衡:
ConcurrentHashMap
分段锁优于Hashtable
的全表锁。 - 最终一致性:分布式场景下可能允许短暂不一致(如 CAP 理论)。
-
不可变集合:使用
Collections.unmodifiableList()
避免意外修改。
五、处理集合中 Null 值的方法
Objects.requireNonNull() 方法
方法定义
Objects.requireNonNull()
是 Java 7 引入的一个实用方法,用于显式检查对象引用是否为 null
。如果为 null
,则抛出 NullPointerException
;否则返回该对象引用。
主要重载方法
-
requireNonNull(T obj)
- 基本形式,仅检查对象是否为
null
- 基本形式,仅检查对象是否为
-
requireNonNull(T obj, String message)
- 可指定自定义异常消息
-
requireNonNull(T obj, Supplier<String> messageSupplier)
- 延迟构造异常消息(Java 8+)
使用场景
-
构造函数参数校验
public class Person { private final String name; public Person(String name) { this.name = Objects.requireNonNull(name, "Name cannot be null"); } }
-
方法参数校验
public void process(List<String> items) { Objects.requireNonNull(items, "Item list cannot be null"); // 处理逻辑... }
-
返回前校验
public String getNonNullName() { String name = fetchName(); return Objects.requireNonNull(name); }
优势
-
早期失败:在问题源头快速暴露
null
值问题 -
代码清晰:比手动
if-null-throw
更简洁 -
可读性强:明确表达"此参数不能为
null
"的设计意图
注意事项
- 性能考虑:在极端性能敏感场景慎用(每次调用都有方法栈开销)
-
消息设计:自定义消息应具体说明哪个参数不能为
null
-
不要过度使用:仅用于必须非
null
的场景,合理的设计应允许null
时不需要此检查
示例对比
传统方式:
if (param == null) {
throw new NullPointerException("param is null");
}
使用 requireNonNull
:
Objects.requireNonNull(param, "param is null");
Optional 类包装
概念定义
Optional
是 Java 8 引入的一个容器类,用于表示一个值可能存在或不存在(null
)。它的核心目的是强制开发者显式处理可能为 null
的情况,从而减少 NullPointerException
的发生。
主要方法
-
of(T value)
创建一个非空的Optional
对象,若value
为null
则抛出NullPointerException
。 -
ofNullable(T value)
创建一个Optional
对象,允许value
为null
。 -
empty()
返回一个空的Optional
对象(表示值为null
)。 -
isPresent()
检查值是否存在(非null
)。 -
ifPresent(Consumer<T> action)
若值存在,执行指定的操作。 -
orElse(T other)
若值不存在,返回默认值other
。 -
orElseGet(Supplier<T> other)
若值不存在,通过Supplier
动态生成默认值。 -
orElseThrow(Supplier<X> exceptionSupplier)
若值不存在,抛出指定的异常。
使用场景
-
方法返回值
明确表示方法可能返回null
,调用方需处理空值情况。public Optional<String> findUserById(int id) { // 模拟查询可能返回 null return Optional.ofNullable(database.get(id)); }
-
链式调用避免空指针
安全地访问嵌套对象的属性。Optional.ofNullable(user) .map(User::getAddress) .map(Address::getCity) .orElse("Unknown");
-
替代
if (obj != null)
通过ifPresent
或orElse
简化代码。Optional.ofNullable(name).ifPresent(System.out::println);
常见误区
-
滥用
Optional
- 不要用于类字段、方法参数或集合元素,它设计初衷是返回值。
- 避免直接调用
get()
(需先检查isPresent()
),推荐使用orElse
等安全方法。
-
性能开销
Optional
会轻微增加内存和计算开销,但对大多数场景影响可忽略。
示例代码
public class OptionalExample {
public static void main(String[] args) {
// 1. 创建 Optional
Optional<String> nonNullOpt = Optional.of("Hello");
Optional<String> nullableOpt = Optional.ofNullable(null);
// 2. 检查值是否存在
System.out.println(nonNullOpt.isPresent()); // true
System.out.println(nullableOpt.isPresent()); // false
// 3. 安全获取值
String result1 = nonNullOpt.orElse("Default");
String result2 = nullableOpt.orElseGet(() -> "Generated Default");
// 4. 链式调用
Optional.of(new User("Alice"))
.map(User::getName)
.ifPresent(name -> System.out.println("User: " + name));
}
}
class User {
private String name;
public User(String name) { this.name = name; }
public String getName() { return name; }
}
注意事项
-
不要用
Optional
替代所有null
检查,仅在需要明确表达“无结果”时使用。 -
避免嵌套
Optional
(如Optional<Optional<T>>
),通常可通过flatMap
解构。
空对象模式(Null Object Pattern)
概念定义
空对象模式是一种行为设计模式,通过创建一个代表"空"状态的对象来替代null
引用。该对象提供与真实对象相同的接口,但方法实现为空或默认行为。
使用场景
- 当集合中可能出现
null
元素时 - 需要避免频繁的
null
检查时 - 希望提供默认行为而不是抛出
NullPointerException
实现方式
// 1. 定义接口
public interface Animal {
void makeSound();
}
// 2. 创建真实对象
public class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
// 3. 创建空对象
public class NullAnimal implements Animal {
@Override
public void makeSound() {
// 静默处理或默认行为
}
}
// 使用示例
List<Animal> animals = Arrays.asList(new Dog(), new NullAnimal());
for (Animal animal : animals) {
animal.makeSound(); // 不会抛出NPE
}
优势
- 消除
null
检查代码 - 提供一致的接口行为
- 减少运行时异常
- 代码更清晰可读
注意事项
- 空对象的行为应该明确且无害
- 不适合需要区分"空"和"不存在"的场景
- 可能掩盖真正的逻辑错误
集合中的典型应用
// 传统方式
List<String> names = getNames(); // 可能返回null
if(names != null) {
for(String name : names) {
// 处理逻辑
}
}
// 使用空对象模式
public List<String> getNames() {
List<String> names = fetchFromDB();
return names != null ? names : Collections.emptyList(); // 返回不可变空集合
}
六、最佳实践建议
集合初始化时的 Null 处理
概念定义
集合初始化时的 Null 处理是指在创建集合对象时,如何正确处理可能存在的 Null 值。这包括:
- 集合对象本身是否为 Null
- 集合初始化时包含的元素是否为 Null
使用场景
- 从外部数据源(如数据库、API)加载数据到集合时
- 合并多个可能为 Null 的集合时
- 使用工具类方法(如 Arrays.asList())转换数组为集合时
常见处理方式
1. 防止集合对象本身为 Null
// 安全初始化方式
List<String> list = Optional.ofNullable(externalList).orElse(new ArrayList<>());
// 或者使用工具类
List<String> list = CollectionUtils.emptyIfNull(externalList); // Apache Commons
2. 处理集合中的 Null 元素
// 初始化时过滤Null
List<String> filtered = Stream.of("a", null, "b")
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 使用不可变集合时的处理
List<String> immutable = List.of("a", "b"); // Java 9+, 不允许Null元素
注意事项
-
不同集合实现对 Null 的支持不同:
- ArrayList/LinkedList:允许 Null
- HashSet/TreeSet:HashSet允许Null,TreeSet不允许
- HashMap:允许Null键和值
- ConcurrentHashMap:不允许Null键或值
-
使用工具类时的行为差异:
Arrays.asList(null, "a"); // 允许 List.of(null, "a"); // 抛出NullPointerException
-
性能考虑:频繁的Null检查可能影响性能,应根据业务场景权衡
最佳实践
- 明确业务需求,决定是否允许Null
- 在集合初始化时就处理好Null,而不是在使用时处理
- 对不可变集合,应在创建时就确保无Null
- 文档化集合的Null处理策略
集合遍历时的 Null 检查
概念定义
在 Java 中,集合遍历时的 Null 检查是指在迭代集合元素时,对元素是否为 null
进行判断的操作。由于集合可能包含 null
值,直接操作这些元素可能导致 NullPointerException
。
使用场景
-
集合可能包含
null
值:如ArrayList
、LinkedList
等允许存储null
的集合。 -
业务逻辑要求:某些业务场景下需要过滤或特殊处理
null
值。 -
避免异常:防止直接调用
null
元素的方法或属性时抛出异常。
常见误区
-
未检查直接操作:假设集合中不存在
null
,直接调用方法(如element.toString()
)。 -
过度检查:在已知集合不包含
null
时(如Collections.emptyList()
),仍冗余检查。 -
忽略集合本身为
null
:未对集合对象本身判空,导致NullPointerException
。
示例代码
List<String> list = Arrays.asList("a", null, "b");
// 正确方式:遍历时检查 null
for (String item : list) {
if (item != null) {
System.out.println(item.toUpperCase()); // 安全操作
}
}
// 使用 Stream 过滤 null(Java 8+)
list.stream()
.filter(Objects::nonNull)
.forEach(item -> System.out.println(item.toUpperCase()));
注意事项
-
性能影响:频繁的
null
检查可能对性能有轻微影响,但在多数场景下可忽略。 -
明确设计意图:若集合不应包含
null
,建议在添加元素时校验,而非遍历时处理。 -
工具类辅助:使用
Objects.requireNonNull()
或Optional
可提升代码可读性。
集合作为方法参数时的 Null 处理
概念定义
当集合作为方法参数传递时,需要考虑两种主要的 Null 情况:
- 集合引用本身为 null
- 集合中包含 null 元素
常见处理方式
防御性编程
public void processList(List<String> list) {
if (list == null) {
list = Collections.emptyList(); // 或 throw new IllegalArgumentException
}
// 处理逻辑
}
使用 Objects.requireNonNull
public void processList(List<String> list) {
Objects.requireNonNull(list, "List cannot be null");
// 处理逻辑
}
集合元素判空
public void processElements(List<String> list) {
for (String item : list) {
if (item != null) {
// 处理非空元素
}
}
}
最佳实践
- 明确文档:在方法注释中说明是否允许null集合或null元素
- 早期失败:在方法开始处进行null检查
-
不可变集合:返回不可变集合时考虑使用
Collections.unmodifiableList()
注意事项
-
Collections.emptyList()
返回的是不可变集合 - 使用
List.of()
创建的集合不允许null元素 - 某些集合操作(如
contains(null)
)在包含null元素时行为不同
示例:完整处理方法
/**
* @param list 可为null,但null会被转换为空列表
* @return 处理后的非null列表
*/
public List<String> safeProcess(List<String> list) {
List<String> workingList = list != null ? new ArrayList<>(list) : new ArrayList<>();
// 移除所有null元素
workingList.removeIf(Objects::isNull);
// 业务处理逻辑
workingList.replaceAll(String::toUpperCase);
return Collections.unmodifiableList(workingList);
}
七、常见面试问题
避免集合中的 Null 值
1. 使用空集合替代 Null
-
概念:当集合为空时,返回一个空的集合实例(如
Collections.emptyList()
)而非null
,避免调用方需要额外处理null
的情况。 -
示例代码:
public List<String> getNames() { // 假设 names 可能为 null return names != null ? names : Collections.emptyList(); }
- 优点:调用方可以直接遍历或操作集合,无需判空。
2. 初始化时分配默认集合
- 场景:在类成员变量或方法局部变量初始化时,直接分配空集合。
-
示例代码:
private List<String> items = new ArrayList<>(); // 默认非 null
3. 使用 Objects.requireNonNull
校验
-
用途:在方法传入集合参数时,强制校验非
null
。 -
示例代码:
public void processList(List<String> list) { this.list = Objects.requireNonNull(list, "List cannot be null"); }
4. 过滤 Null 值
-
场景:从外部数据源(如数据库、API)获取集合时,主动过滤
null
元素。 -
示例代码(Java 8+):
List<String> filtered = originalList.stream() .filter(Objects::nonNull) .collect(Collectors.toList());
5. 使用 Optional 包装
-
适用场景:方法返回可能为空的集合时,用
Optional
明确提示调用方处理空情况。 -
示例代码:
public Optional<List<String>> findItems() { return Optional.ofNullable(items); }
6. 注意事项
- 性能权衡:空集合会占用少量内存,但通常可忽略不计。
-
第三方库兼容性:如 JPA/Hibernate 可能默认返回
null
,需在查询中明确处理(如@Query
返回空集合)。 -
不可变集合:
Collections.emptyList()
返回的集合不可修改,需根据场景选择。
7. 常见误区
-
误区:认为
null
比空集合更“高效”。实际上,空集合的代码可读性和安全性更高。 -
反例:
// 不推荐:调用方必须判空 if (list != null) { for (String s : list) { ... } }
处理 Null 值的性能考量
概念定义
在 Java 集合中处理 null
值时,性能考量主要涉及内存占用、遍历效率、以及空值检查的开销。null
值虽然不占用额外的对象内存,但会增加逻辑判断的负担。
使用场景
-
频繁查询或遍历的集合:如
ArrayList
或HashMap
中包含大量null
值时,每次操作都需要额外的空值检查。 -
高并发环境:
ConcurrentHashMap
等线程安全集合中,null
值可能导致额外的锁竞争或检查逻辑。 -
序列化与反序列化:
null
值会增加序列化后的数据大小(如 JSON 中的"key": null
)。
常见误区或注意事项
-
内存占用误区:
null
不占用对象内存,但会占用引用空间(通常 4 或 8 字节)。 -
性能陷阱:
contains(null)
或remove(null)
在HashMap
中可能触发额外的哈希计算和链表遍历。 -
Optional 的代价:用
Optional
包装null
会引入额外对象创建开销(适用于业务逻辑,而非性能敏感场景)。
示例代码
// 示例1:ArrayList 遍历时的空值检查开销
List<String> list = Arrays.asList("a", null, "b");
for (String s : list) {
if (s != null) { // 每次迭代都需检查
System.out.println(s.toUpperCase());
}
}
// 示例2:HashMap 的 getOrDefault 性能优化
Map<String, Integer> map = new HashMap<>();
map.put("key1", null);
int value = map.getOrDefault("key1", 0); // 避免显式空值检查
优化建议
-
预过滤:使用
stream().filter(Objects::nonNull)
提前移除null
。 -
默认值替代:如
getOrDefault
或computeIfAbsent
减少分支判断。 -
选择集合类型:
ConcurrentHashMap
禁止null
值以避免并发检查开销。
集合框架中 Null 值的实现原理
基本概念
在 Java 集合框架中,null
是一个特殊的值,表示“无对象”或“空引用”。集合框架允许在某些集合类中存储 null
值,但具体实现因集合类型而异。
主要集合类型的实现方式
List 实现类
-
ArrayList
- 内部使用
Object[]
数组存储元素 -
null
可以存储在任意位置 - 示例代码:
List<String> list = new ArrayList<>(); list.add(null); // 允许
- 内部使用
-
LinkedList
- 通过
Node
节点存储数据 -
Node.item
可以设置为null
- 示例代码:
List<String> list = new LinkedList<>(); list.add(null); // 允许
- 通过
Set 实现类
-
HashSet
- 基于
HashMap
实现 - 使用
PRESENT
对象作为值,null
可以作为键存储 - 示例代码:
Set<String> set = new HashSet<>(); set.add(null); // 允许
- 基于
-
TreeSet
- 基于
TreeMap
实现 - 添加
null
会抛出NullPointerException
- 原因:依赖
Comparable
或Comparator
进行排序,无法比较null
- 基于
Map 实现类
-
HashMap
- 使用
hashCode()
计算存储位置 - 允许一个
null
键和多个null
值 - 实现关键点:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
- 使用
-
TreeMap
- 基于红黑树实现
- 不允许
null
键(与TreeSet
相同原因) - 但允许
null
值
底层实现原理
-
数组存储(如 ArrayList)
-
null
作为普通元素存储在数组中 - 不进行特殊处理,只是不调用对象方法
-
-
哈希表(如 HashMap)
- 对
null
键特殊处理:hashCode()
返回 0 - 存储在哈希表的第一个桶(bucket 0)
- 对
-
树结构(如 TreeMap)
- 比较时会抛出
NullPointerException
- 因为
compareTo()
或compare()
不能处理null
- 比较时会抛出
线程安全集合的特殊情况
-
ConcurrentHashMap
- 不允许
null
键或值 - 设计原因:歧义问题(无法区分“不存在”和“值为 null”)
- 不允许
性能影响
-
查询性能:
contains(null)
需要特殊处理 -
空间占用:
null
不占用额外空间(与普通对象引用相同) -
序列化:
null
会被正常序列化/反序列化
最佳实践
- 明确区分“无值”和“值为 null”的场景
- 使用前检查文档确认集合是否支持
null
- 考虑使用
Optional
作为更安全的替代方案