java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java null空指针

Java 中的 null空指针、自动拆箱与集合边界实例详解

作者:超级苦力怕

这段文章详细解析了Java中null引发的空指针异常的原因,探讨了自动空、空对象与缺失值之间的区别,并强调了自动拆箱、Map.get()返回null、TreeMap排序边界等场景中的注意事项,感兴趣的朋友一起看看吧

本文用最小代码讲清 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 valueget() 返回 null 不能直接代表“没这个 key”
TreeMapnull value 可以,null key 默认不行需要比较 key 来维持有序树
ConcurrentHashMap不允许 null key / value避免并发场景的语义歧义
Hashtable不允许 null key / value老实现,规则更严格
ArrayDeque / PriorityQueue不允许 null需要用 null 作为空或异常状态的边界信号

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

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

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

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

Integer count = null;
int total = count; // NPE

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

编译器背后大致会变成:

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

int total = count.intValue();
if (ready.booleanValue()) {
    System.out.println("ok");
}

再看一个更隐蔽的场景:

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

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 是否存在

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()

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

四、TreeMap为什么默认不接受nullkey

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

示例:默认 TreeMap 不接受 null key

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

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

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

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 就别放进去。

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

public User(String name, Integer age) {
    this.name = Objects.requireNonNull(name, "name");
    this.age = Objects.requireNonNull(age, "age");
}

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

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

public List<String> listTags() {
    return Collections.emptyList();
}

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

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

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

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 元素

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 不是一个统一的“空值”。先分清它在当前场景里代表“缺失”“空对象”还是“非法输入”,后面的代码才不会一路埋雷。

到此这篇关于Java 中的 null空指针、自动拆箱与集合边界实例详解的文章就介绍到这了,更多相关java null空指针内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文