65. 集合中的 Null 值处理

一、Null 值的基本概念

Null 值的定义

Null 值在 Java 中表示一个不存在的对象引用。它是引用类型的默认值,表示变量没有指向任何对象实例。Null 不是基本数据类型(如 int、boolean),而是所有引用类型(如 String、List)的特殊值。

Null 值的特点

  1. 默认值:类成员变量(引用类型)未初始化时,默认值为 null。
  2. 类型无关:所有引用类型变量都可以赋值为 null。
  3. 内存表现:null 不占用堆内存(无对象实例),仅占用栈内存(引用变量空间)。

常见场景

  1. 显式初始化String s = null;
  2. 方法返回:当方法找不到有效对象时返回 null。
  3. 集合元素:允许作为元素存入 List/Map(除非显式限制)。

注意事项

  1. NullPointerException:对 null 调用方法/属性会抛出此异常。
  2. 防御性编程
    if (obj != null) {
        obj.method();
    }
    
  3. 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())。
核心区别
  1. 内存分配

    • null 不占用集合对象的内存。
    • 空集合会分配对象内存(如 ArrayList 的初始容量为 10,但元素数为 0)。
  2. 操作行为

    • 调用 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());
注意事项
  1. 防御性编程

    • 方法返回值优先返回空集合而非 null(避免调用方漏判 null)。
    • 使用 Optional@Nullable 注解明确可能为 null 的场景。
  2. 性能影响

    • 频繁创建空集合可能产生微小开销,可复用 Collections.emptyList() 等不可变空集合。
  3. API 设计原则

    • 如 Google Guava 等库强制约定“集合返回值不为 null”,减少歧义。

Null 在 Java 中的语义

概念定义

Null 在 Java 中表示一个引用变量不指向任何对象。它是所有引用类型的默认值,可以赋值给任何对象引用变量,但不能赋值给基本数据类型(如 int、double 等)。Null 是一个特殊的关键字,表示“无”或“不存在”。

使用场景
  1. 初始化引用变量:当声明一个引用变量但暂时不需要指向具体对象时,可以初始化为 null。
  2. 表示缺失或无效值:例如,数据库查询可能返回 null 表示没有找到记录。
  3. 释放对象引用:将变量设置为 null 可以帮助垃圾回收器回收不再使用的对象。
常见误区与注意事项
  1. NullPointerException:最常见的运行时异常之一,发生在尝试调用 null 对象的方法或访问其属性时。
  2. 与空集合混淆:null 集合表示引用不存在,而空集合(如 new ArrayList())表示存在但内容为空。
  3. 重载方法调用:传递 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]
    
注意事项
  1. 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
      
  2. 排序时的 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]
      
  3. 并发修改

    • 在多线程环境中,ArrayListLinkedList 不是线程安全的,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
    

注意事项

  1. 唯一性:所有 Set 实现类中,null 最多只能存在一个(如多次添加,仅保留一个)。
  2. 线程安全CopyOnWriteArraySetConcurrentSkipListSet 是线程安全的,但后者不支持 null。
  3. 性能影响:在 HashSetLinkedHashSet 中使用 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(因依赖 ComparableComparator 排序,调用 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 的限制
  1. ConcurrentHashMap

    • 键和值均不允许为 null
      原因:ConcurrentHashMap 的设计中,null 可能表示“键不存在”或“值未初始化”,多线程环境下无法区分这两种情况,易引发歧义。
      ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
      map.put("key", null); // 抛出 NullPointerException
      
  2. CopyOnWriteArrayList / CopyOnWriteArraySet

    • 允许 null
      但需注意:频繁插入 null 可能影响可读性和逻辑判断。
  3. BlockingQueue 实现类(如 ArrayBlockingQueue)

    • 多数实现禁止 null
      原因:null 通常用作队列的特殊标记(如“终止信号”),禁止 null 可避免混淆。
      BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
      queue.offer(null); // 抛出 NullPointerException
      
注意事项
  1. 替代方案
    • 若需表示“空值”,可使用 Optional.empty() 或自定义标记对象(如 EMPTY_OBJECT)。
  2. 性能影响
    • null 的检查可能增加少量性能开销,但能提升代码健壮性。
  3. 文档查阅
    • 使用线程安全集合时,务必查阅官方文档确认其对 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)。
  • 原因:依赖 ComparableComparator 进行排序,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
    
注意事项
  1. 替代方案:若需存储 null,可改用 HashMapArrayList 等普通集合类。
  2. 文档检查:使用特殊集合前,应查阅其文档确认 null 支持情况。
  3. 自定义处理:通过包装类或 Optional 显式处理可能的 null 值。

第三方集合库对 Null 值的限制

概念定义

第三方集合库(如 Guava、Apache Commons Collections 等)通常对 null 值有明确的限制或支持策略。这些库通过设计约束或运行时检查,强制开发者明确处理 null,以提高代码的健壮性和可读性。


常见库的限制策略
1. Google Guava
  • 禁止 null:大多数 Guava 集合(如 ImmutableListImmutableSet)直接禁止 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 抛出异常。
  • 文档模糊:需仔细阅读具体类的文档确认限制。

使用场景
  1. 防御性编程:使用禁止 null 的集合(如 Guava)可减少空指针异常。
  2. 数据清洗:在接收外部数据时,通过 Preconditions.checkNotNull 显式校验。

注意事项
  1. 性能影响null 检查可能增加微小开销。
  2. 序列化兼容性:允许 null 的集合在跨系统传输时需额外处理。
  3. 文档优先:始终查阅第三方库的官方文档确认其 null 策略。

示例:Guava 的 Optional 替代方案
Optional<String> optionalValue = Optional.fromNullable(getNullableInput());
if (optionalValue.isPresent()) {
    System.out.println(optionalValue.get());
}

四、Null 值带来的问题

NullPointerException 风险

概念定义

NullPointerException(NPE)是 Java 中常见的运行时异常,当程序试图访问或操作一个 null 引用时抛出。在集合操作中,NPE 风险主要来源于:

  1. 集合本身为 null
  2. 集合元素为 null
  3. null 元素进行操作(如调用方法)
常见触发场景
  1. 未初始化集合
List<String> list = null;
int size = list.size(); // NPE
  1. 添加 null 元素
List<String> list = new ArrayList<>();
list.add(null);
String s = list.get(0).toUpperCase(); // NPE
  1. Map 的 key/value 为 null
Map<String, String> map = new HashMap<>();
map.put(null, "value"); // 允许但危险
String v = map.get(null).trim(); // 可能NPE
防御性编程方案
  1. 集合判空
if (list != null && !list.isEmpty()) {
    // 安全操作
}
  1. 使用 Optional
Optional.ofNullable(list)
    .orElse(Collections.emptyList())
    .forEach(item -> System.out.println(item));
  1. 使用 Objects.requireNonNull
List<String> safeList = Objects.requireNonNull(list, "List不能为null");
  1. 集合工具类
// Apache Commons
CollectionUtils.emptyIfNull(list); 

// Guava
Iterables.filter(list, Predicates.notNull());
最佳实践
  1. 明确约定集合是否允许 null 元素(如 ConcurrentHashMap 不允许 null 值)
  2. 使用 @NonNull/@Nullable 注解(JSR-305)
  3. 优先返回空集合而非 nullCollections.emptyList()
  4. Java 8+ 推荐使用 Optional 包装可能为 null 的返回值
注意事项
  1. ConcurrentHashMapHashtablekey/value 均不能为 null
  2. TreeSet/TreeMapnull 的检查取决于 Comparator 实现
  3. 使用 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-eachIterator 时直接调用集合的 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)。

数据一致性问题

概念定义

数据一致性指在分布式系统或并发环境中,多个数据副本或事务操作后,数据保持逻辑正确和同步的状态。在集合操作中表现为:当多个线程或操作同时修改集合时,可能导致数据丢失、重复或状态不一致。

使用场景
  1. 多线程环境:如 ArrayList 被多个线程同时修改时可能抛出 ConcurrentModificationException
  2. 分布式缓存:如 Redis 集群中多个节点数据同步。
  3. 数据库事务:如订单和库存的跨表操作需保证原子性。
常见误区
  1. 误用非线程安全集合:如直接使用 HashMap 而非 ConcurrentHashMap
  2. 忽略原子操作:如先 contains()add() 的非原子组合。
  3. 过度同步:滥用 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);
注意事项
  1. 性能权衡ConcurrentHashMap 分段锁优于 Hashtable 的全表锁。
  2. 最终一致性:分布式场景下可能允许短暂不一致(如 CAP 理论)。
  3. 不可变集合:使用 Collections.unmodifiableList() 避免意外修改。

五、处理集合中 Null 值的方法

Objects.requireNonNull() 方法

方法定义

Objects.requireNonNull() 是 Java 7 引入的一个实用方法,用于显式检查对象引用是否为 null。如果为 null,则抛出 NullPointerException;否则返回该对象引用。

主要重载方法
  1. requireNonNull(T obj)
    • 基本形式,仅检查对象是否为 null
  2. requireNonNull(T obj, String message)
    • 可指定自定义异常消息
  3. requireNonNull(T obj, Supplier<String> messageSupplier)
    • 延迟构造异常消息(Java 8+)
使用场景
  1. 构造函数参数校验

    public class Person {
        private final String name;
        
        public Person(String name) {
            this.name = Objects.requireNonNull(name, "Name cannot be null");
        }
    }
    
  2. 方法参数校验

    public void process(List<String> items) {
        Objects.requireNonNull(items, "Item list cannot be null");
        // 处理逻辑...
    }
    
  3. 返回前校验

    public String getNonNullName() {
        String name = fetchName();
        return Objects.requireNonNull(name);
    }
    
优势
  1. 早期失败:在问题源头快速暴露 null 值问题
  2. 代码清晰:比手动 if-null-throw 更简洁
  3. 可读性强:明确表达"此参数不能为 null"的设计意图
注意事项
  1. 性能考虑:在极端性能敏感场景慎用(每次调用都有方法栈开销)
  2. 消息设计:自定义消息应具体说明哪个参数不能为 null
  3. 不要过度使用:仅用于必须非 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 的发生。

主要方法
  1. of(T value)
    创建一个非空的 Optional 对象,若 valuenull 则抛出 NullPointerException
  2. ofNullable(T value)
    创建一个 Optional 对象,允许 valuenull
  3. empty()
    返回一个空的 Optional 对象(表示值为 null)。
  4. isPresent()
    检查值是否存在(非 null)。
  5. ifPresent(Consumer<T> action)
    若值存在,执行指定的操作。
  6. orElse(T other)
    若值不存在,返回默认值 other
  7. orElseGet(Supplier<T> other)
    若值不存在,通过 Supplier 动态生成默认值。
  8. orElseThrow(Supplier<X> exceptionSupplier)
    若值不存在,抛出指定的异常。
使用场景
  1. 方法返回值
    明确表示方法可能返回 null,调用方需处理空值情况。
    public Optional<String> findUserById(int id) {
        // 模拟查询可能返回 null
        return Optional.ofNullable(database.get(id));
    }
    
  2. 链式调用避免空指针
    安全地访问嵌套对象的属性。
    Optional.ofNullable(user)
            .map(User::getAddress)
            .map(Address::getCity)
            .orElse("Unknown");
    
  3. 替代 if (obj != null)
    通过 ifPresentorElse 简化代码。
    Optional.ofNullable(name).ifPresent(System.out::println);
    
常见误区
  1. 滥用 Optional
    • 不要用于类字段、方法参数或集合元素,它设计初衷是返回值。
    • 避免直接调用 get()(需先检查 isPresent()),推荐使用 orElse 等安全方法。
  2. 性能开销
    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引用。该对象提供与真实对象相同的接口,但方法实现为空或默认行为。

使用场景
  1. 当集合中可能出现null元素时
  2. 需要避免频繁的null检查时
  3. 希望提供默认行为而不是抛出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
}
优势
  1. 消除null检查代码
  2. 提供一致的接口行为
  3. 减少运行时异常
  4. 代码更清晰可读
注意事项
  1. 空对象的行为应该明确且无害
  2. 不适合需要区分"空"和"不存在"的场景
  3. 可能掩盖真正的逻辑错误
集合中的典型应用
// 传统方式
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 值。这包括:

  1. 集合对象本身是否为 Null
  2. 集合初始化时包含的元素是否为 Null
使用场景
  1. 从外部数据源(如数据库、API)加载数据到集合时
  2. 合并多个可能为 Null 的集合时
  3. 使用工具类方法(如 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元素
注意事项
  1. 不同集合实现对 Null 的支持不同:

    • ArrayList/LinkedList:允许 Null
    • HashSet/TreeSet:HashSet允许Null,TreeSet不允许
    • HashMap:允许Null键和值
    • ConcurrentHashMap:不允许Null键或值
  2. 使用工具类时的行为差异:

    Arrays.asList(null, "a");  // 允许
    List.of(null, "a");       // 抛出NullPointerException
    
  3. 性能考虑:频繁的Null检查可能影响性能,应根据业务场景权衡

最佳实践
  1. 明确业务需求,决定是否允许Null
  2. 在集合初始化时就处理好Null,而不是在使用时处理
  3. 对不可变集合,应在创建时就确保无Null
  4. 文档化集合的Null处理策略

集合遍历时的 Null 检查

概念定义

在 Java 中,集合遍历时的 Null 检查是指在迭代集合元素时,对元素是否为 null 进行判断的操作。由于集合可能包含 null 值,直接操作这些元素可能导致 NullPointerException

使用场景
  1. 集合可能包含 null:如 ArrayListLinkedList 等允许存储 null 的集合。
  2. 业务逻辑要求:某些业务场景下需要过滤或特殊处理 null 值。
  3. 避免异常:防止直接调用 null 元素的方法或属性时抛出异常。
常见误区
  1. 未检查直接操作:假设集合中不存在 null,直接调用方法(如 element.toString())。
  2. 过度检查:在已知集合不包含 null 时(如 Collections.emptyList()),仍冗余检查。
  3. 忽略集合本身为 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()));
注意事项
  1. 性能影响:频繁的 null 检查可能对性能有轻微影响,但在多数场景下可忽略。
  2. 明确设计意图:若集合不应包含 null,建议在添加元素时校验,而非遍历时处理。
  3. 工具类辅助:使用 Objects.requireNonNull()Optional 可提升代码可读性。

集合作为方法参数时的 Null 处理

概念定义

当集合作为方法参数传递时,需要考虑两种主要的 Null 情况:

  1. 集合引用本身为 null
  2. 集合中包含 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) {
            // 处理非空元素
        }
    }
}
最佳实践
  1. 明确文档:在方法注释中说明是否允许null集合或null元素
  2. 早期失败:在方法开始处进行null检查
  3. 不可变集合:返回不可变集合时考虑使用Collections.unmodifiableList()
注意事项
  1. Collections.emptyList()返回的是不可变集合
  2. 使用List.of()创建的集合不允许null元素
  3. 某些集合操作(如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 值虽然不占用额外的对象内存,但会增加逻辑判断的负担。

使用场景
  1. 频繁查询或遍历的集合:如 ArrayListHashMap 中包含大量 null 值时,每次操作都需要额外的空值检查。
  2. 高并发环境ConcurrentHashMap 等线程安全集合中,null 值可能导致额外的锁竞争或检查逻辑。
  3. 序列化与反序列化null 值会增加序列化后的数据大小(如 JSON 中的 "key": null)。
常见误区或注意事项
  1. 内存占用误区null 不占用对象内存,但会占用引用空间(通常 4 或 8 字节)。
  2. 性能陷阱contains(null)remove(null)HashMap 中可能触发额外的哈希计算和链表遍历。
  3. 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); // 避免显式空值检查
优化建议
  1. 预过滤:使用 stream().filter(Objects::nonNull) 提前移除 null
  2. 默认值替代:如 getOrDefaultcomputeIfAbsent 减少分支判断。
  3. 选择集合类型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
    • 原因:依赖 ComparableComparator 进行排序,无法比较 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
底层实现原理
  1. 数组存储(如 ArrayList)

    • null 作为普通元素存储在数组中
    • 不进行特殊处理,只是不调用对象方法
  2. 哈希表(如 HashMap)

    • null 键特殊处理:hashCode() 返回 0
    • 存储在哈希表的第一个桶(bucket 0)
  3. 树结构(如 TreeMap)

    • 比较时会抛出 NullPointerException
    • 因为 compareTo()compare() 不能处理 null
线程安全集合的特殊情况
  • ConcurrentHashMap
    • 不允许 null 键或值
    • 设计原因:歧义问题(无法区分“不存在”和“值为 null”)
性能影响
  • 查询性能contains(null) 需要特殊处理
  • 空间占用null 不占用额外空间(与普通对象引用相同)
  • 序列化null 会被正常序列化/反序列化
最佳实践
  1. 明确区分“无值”和“值为 null”的场景
  2. 使用前检查文档确认集合是否支持 null
  3. 考虑使用 Optional 作为更安全的替代方案