【Java杂项】Java 中的 null:空指针、自动拆箱与集合边界详解

【Java杂项】Java 中的 null:空指针、自动拆箱与集合边界详解

    • [一、先把 `null`、空、缺失分开](#一、先把 null、空、缺失分开)
    • [二、最常见的 NPE 来自自动拆箱](#二、最常见的 NPE 来自自动拆箱)
    • [三、`Map.get()` 的 `null` 不一定表示没值](#三、Map.get()null 不一定表示没值)
    • [四、`TreeMap` 为什么默认不接受 `null` key](#四、TreeMap 为什么默认不接受 null key)
    • [五、工程上怎么防 `null`](#五、工程上怎么防 null)
      • [5.1 入参先校验](#5.1 入参先校验)
      • [5.2 返回空集合,不要返回 `null`](#5.2 返回空集合,不要返回 null)
      • [5.3 `Optional` 只负责"可能没有",别把它当装饰品](#5.3 Optional 只负责“可能没有”,别把它当装饰品)
      • [5.4 Stream 里先过滤,再收集](#5.4 Stream 里先过滤,再收集)
      • [5.5 静态分析和注解别省](#5.5 静态分析和注解别省)
    • 六、总结

🎬 博主名称: 超级苦力怕

🔥 个人专栏: 《基本功修炼大全》

🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!


文章元信息:

  • 适合读者: 正在学习 Java 基础、异常处理和集合框架,或准备 Java 面试、排查空指针问题的读者
  • 前置知识: 了解 Java 引用类型、基本类型与包装类、Map 和 List 的基本用法

本文用最小代码讲清 Java 中 null 为什么容易引发空指针,并继续展开自动拆箱、Map.get 返回 null、TreeMap 排序边界和工程防御写法。

一、先把 null、空、缺失分开

null、空集合、业务缺失不是一回事。很多坑都是因为这三者被混着用了。

状态 含义 常见表达
null 没有对象引用 String s = null;
对象存在,但内容为空 new ArrayList<>()
缺失 业务上没有这个值 Optional.empty()

再看常见容器对 null 的态度:

容器 null 规则 说明
ArrayList 可以存多个 null 只是元素值为空,不影响列表结构
LinkedList 可以存多个 null 但作为队列用时要小心语义混淆
HashSet 可以有一个 null 元素 底层依赖 HashMap
HashMap 一个 null key,多个 null value get() 返回 null 不能直接代表"没这个 key"
TreeMap null value 可以,null key 默认不行 需要比较 key 来维持有序树
ConcurrentHashMap 不允许 null key / value 避免并发场景的语义歧义
Hashtable 不允许 null key / value 老实现,规则更严格
ArrayDeque / PriorityQueue 不允许 null 需要用 null 作为空或异常状态的边界信号

一句话记忆: 容器能不能放 null,和它是否要拿 null 当"空信号"是两回事。

二、最常见的 NPE 来自自动拆箱

null 本身不会自动变成 0false 或空字符串。只要把包装类型交给基本类型,编译器就会偷偷拆箱。

示例:包装类型自动拆箱触发 NPE

java 复制代码
Integer count = null;
int total = count; // NPE

Boolean ready = null;
if (ready) {       // NPE
    System.out.println("ok");
}

编译器背后大致会变成:

等价展开:编译器会调用 xxxValue()

java 复制代码
int total = count.intValue();
if (ready.booleanValue()) {
    System.out.println("ok");
}

再看一个更隐蔽的场景:

示例:Map.get() 返回 null 后赋给 int

java 复制代码
Map<String, Integer> scoreMap = new HashMap<>();
int score = scoreMap.get("Tom"); // NPE

这里 get("Tom") 返回的是 null,但左边是 int,于是又发生了拆箱。

写法 触发点 结果
int x = integer; 自动拆箱 Integer.intValue() 调到 null
if (booleanObj) 条件判断拆箱 Boolean.booleanValue() 调到 null
int x = flag ? integer : 0; 三目运算符统一类型 可能先选出 Integer 再拆箱
sum += integer; 运算前拆箱 null 直接炸掉

核心结论: 只要包装类型要参与运算、比较或条件判断,就先想一遍"这里会不会被拆箱"。

三、Map.get()null 不一定表示没值

Map.get() 返回 null,有三种可能:

  1. key 不存在。
  2. key 存在,但 value 本身就是 null
  3. 对于可变 key,key 的哈希身份已经被改坏了。

前两种可以这样区分:

示例:用 containsKey() 区分 key 是否存在

java 复制代码
Map<String, Integer> map = new HashMap<>();
map.put("A", null);

System.out.println(map.get("A"));         // null
System.out.println(map.containsKey("A")); // true

所以,如果业务上允许 null value,get() 的结果就不能单独说明问题,得配合 containsKey()

如果业务上不希望出现这个歧义,更稳的做法是:

  • 不存 null value。
  • 返回空集合而不是 null
  • Optional 表达"可能没有值"。

四、TreeMap 为什么默认不接受 null key

TreeMap 的 key 要参与排序。默认自然排序下,key 需要能比较,null 没法比较,所以会抛 NullPointerException

示例:默认 TreeMap 不接受 null key

java 复制代码
TreeMap<String, Integer> map = new TreeMap<>();
map.put("A", 1);
map.put(null, 2); // NPE

如果确实有业务需求,可以提供能处理 null 的比较器:

示例:自定义比较器显式处理 null

java 复制代码
TreeMap<String, Integer> map = new TreeMap<>((a, b) -> {
    if (a == b) {
        return 0;
    }
    if (a == null) {
        return -1;
    }
    if (b == null) {
        return 1;
    }
    return a.compareTo(b);
});

map.put(null, 2);
map.put("A", 1);
System.out.println(map.get(null));

这里要注意两点:

说明
比较器要稳定 compare(a, b) 不能前后乱跳,否则 put() / get() 会不一致
compare(a, b) == 0 的语义要清楚 不然 TreeMap 可能把两个不同对象当成同一个 key

TreeSet 的规则也一样,因为它底层也是 TreeMap

五、工程上怎么防 null

5.1 入参先校验

公共方法和构造函数里,能在入口挡住的 null 就别放进去。

示例:在构造函数入口校验非空参数

java 复制代码
public User(String name, Integer age) {
    this.name = Objects.requireNonNull(name, "name");
    this.age = Objects.requireNonNull(age, "age");
}

5.2 返回空集合,不要返回 null

示例:返回空集合而不是 null

java 复制代码
public List<String> listTags() {
    return Collections.emptyList();
}

这样调用方就不用先判空再遍历。

5.3 Optional 只负责"可能没有",别把它当装饰品

示例:根据是否确定非空选择 of()ofNullable()

java 复制代码
Optional.of(value);       // 已知非空
Optional.ofNullable(val); // 不确定是否为空

of() 适合已知不为空的值,ofNullable() 适合边界输入。

5.4 Stream 里先过滤,再收集

findFirst()max()min()toMap()groupingBy() 这类操作都要小心 null

API 注意点
findFirst() / max() / min() 流里不要混入 null 元素
Collectors.toMap() value mapper 不要返回 null
Collectors.groupingBy() 分组 key 不要返回 null

常见做法是先过滤:

示例:Stream 收集前先过滤 null 元素

java 复制代码
stream.filter(Objects::nonNull)
      .collect(Collectors.toList());

5.5 静态分析和注解别省

@Nullable@NotNull 这类注解配合静态分析工具,能把很多 NPE 提前拦下来。

六、总结

问题 结论
null 能不能直接参与运算 不能,常见结果是自动拆箱 NPE
Map.get() 返回 null 就一定没值吗 不一定,可能是 value 本身就是 null
TreeMap 为什么默认不收 null key 因为 key 要参与排序,默认自然顺序无法比较 null
哪些容器最容易和 null 打架 MapSetTreeMapQueue、包装类型
工程上最稳的做法 入口校验、空集合返回、Optional、显式判空

核心结论: null 不是一个统一的"空值"。先分清它在当前场景里代表"缺失""空对象"还是"非法输入",后面的代码才不会一路埋雷。


相关推荐
karry_k2 小时前
MyBatis批量insert-select踩坑:useGeneratedKeys=true 可能让PostgreSQL返回大量插入结果
java·后端
karry_k2 小时前
PostgreSQL 在 MyBatis 中执行正常 SQL 失效:一次 DELETE USING 踩坑记录
java·后端
SamDeepThinking6 小时前
从源码到代码:MyBatis-Flex 与 MyBatis-Plus 的逐项对比
java·后端·程序员
她的男孩9 小时前
Spring Boot 接 Flowable 工作流:用 3 个注解搭一个请假审批流程
java·后端·架构
荣码10 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python
plainGeekDev12 小时前
Gson → kotlinx.serialization
android·java·kotlin
小bo波20 小时前
Java Swing 图形用户界面实验 —— 从算术练习到游戏开发的完整实践
java·课程设计·gui·游戏开发·扫雷·swing
咖啡八杯1 天前
GoF设计模式——备忘录模式
java·后端·spring·设计模式