NullPointerException在运行时才抛出,因为编译器只检查类型合法性而不校验null值,null是唯一可赋给任意引用类型的合法值,导致s.length()等调用在执行时崩溃。

为什么NullPointerException总在运行时才爆,编译器却不管?
因为null是Java中唯一能赋给任意引用类型的“合法值”,编译器只校验类型匹配,不校验是否为null。它让String s = null;完全合法,但s.length()一执行就崩——这不是语法错,是语义错。
真正危险的是那些“看起来没问题”的链式调用:user.getAddress().getCity().toUpperCase()。只要中间任一环节返回null(比如getAddress()查不到地址),整个链就立刻触发NPE,而堆栈只报最后一行,你得倒推哪一环空了。
- 别信“这个DAO方法肯定有结果”——数据库没数据、缓存穿透、SQL写错都可能导致返回
null - 别忽略JSON反序列化:字段缺失或类型不匹配时,Jackson/Gson常静默设为
null,尤其对包装类如Integer age - Spring
@Autowired字段为null?大概率是该类没被IoC容器管理(比如new出来的、或@Component漏写了)
Objects.requireNonNull()不是摆设,是第一道防线
它比手写if (obj == null) throw new NullPointerException()更简洁,关键是能带明确定制消息,且JVM在某些版本(Java 14+启用-XX:+ShowCodeDetailsInExceptionMessages)下会直接标出哪个变量为空。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 所有public方法入口,对必填参数强制校验:
Objects.requireNonNull(userId, "userId must not be null") - 不要只在校验参数上用——构造函数、Builder的
build()方法里也加,把问题拦在对象创建阶段 - 避免滥用:内部工具方法若明确允许
null(如“兼容旧逻辑”),就别硬加,否则反而掩盖真实空路径
集合和数组的null陷阱:不只是“集合本身为null”
很多人只记得判list == null,却忘了list.get(0)可能返回null,或者stream().findFirst().orElse(null)的返回值直接进下一行调用——这和对象未初始化一样致命。
典型翻车现场:
-
List—— 若users = dao.findUsers(); for (User u : users) { System.out.println(u.getName()); } users为null,增强for直接NPE;若users非空但含null元素,循环到那个位置仍NPE -
String[] arr = config.getStringArray("roles"); if (arr.length > 0) {...}——arr为null时.length就挂了 - Map.get()返回
null不等于键不存在——可能是键存在但值就是null,要用containsKey()或getOrDefault()区分
自动拆箱和equals():两个最隐蔽的NPE温床
包装类自动转基本类型(拆箱)时,如果对象是null,会立即抛NPE,而且错误行号容易误导人:
Integer count = null; int c = count; // 看似简单赋值,实际触发NPE
equals()也是重灾区——str.equals("abc")在str为null时必崩,但"abc".equals(str)安全,因为字面量字符串不可能为null。
- 永远用常量.equals(变量),而不是变量.equals(常量)
- 涉及比较的场景,优先用
Objects.equals(a, b),它内部已处理双null、单null等全部情况 - 数据库映射字段用
Integer而非int是对的,但后续业务逻辑中若需算术运算,先用Objects.nonNull()或Optional.ofNullable()兜底
最易被忽略的点:NPE从来不是孤立的bug,而是信号——它暴露了调用契约模糊、输入边界未定义、或模块间缺乏空值约定。与其在线上抓堆栈,不如在接口设计时就决定:这个方法到底返null还是空集合?这个DTO字段可空是否需要文档标注?这些决策比写十行判空代码影响更深远。











