NPE根源是假设引用非空但实际为null,需在设计阶段明确空值契约。常见场景含调用前未判空、自动拆箱、集合元素为null、静态方法误用实例调用、构造器泄露this、多线程共享状态未同步。

调用对象方法前未判空
只要对象引用为 null,直接调用其任何实例方法(包括 toString()、equals()、hashCode())都会触发 NullPointerException。尤其容易在日志打印、参数校验、DTO 转 VO 等环节忽略。
- 常见写法:
user.getName().length()—— 若user为null,立刻崩溃 - 安全写法:先用
Objects.nonNull(user)或user != null判空,再访问;或改用Optional.ofNullable(user).map(User::getName).orElse("") - 注意:
String.valueOf(obj)和Objects.toString(obj)可安全处理null,但它们不解决业务逻辑中对非空的依赖
自动拆箱时包装类为null
Java 在执行 int、boolean 等基本类型运算时,会自动将 Integer、Boolean 等包装类拆箱。若该包装类变量本身是 null,就会抛出 NullPointerException,而非 NumberFormatException 或其他异常。
- 典型场景:
Integer status = getStatusFromDB(); if (status == 1) { ... }—— 若getStatusFromDB()返回null,比较时触发 NPE - 避免方式:改用
Objects.equals(status, 1),或显式判空status != null && status == 1 - 注意:三元表达式如
status != null ? status : 0是安全的;但status ?: 0(Kotlin 风格)在 Java 中不合法
集合或数组元素为null后直接使用
List、Map、数组本身不为 null,不代表其中元素也不为 null。取值后未检查就调用方法或参与计算,是高频 NPE 来源。
- 例如:
list.get(0).trim()——list不空,但第 0 个元素可能是null -
map.get("key").length()——map存在,但 key 对应 value 为null - 建议:用
Optional.ofNullable(list.get(0)).map(String::trim).orElse("");或提前过滤掉null元素(如list.stream().filter(Objects::nonNull).collect(...)) - 特别注意:MyBatis 查询结果映射到 List 时,若数据库字段允许 NULL,对应字段可能被设为
null,而非默认值
静态方法误当成实例方法调用
当把本该通过类名调用的静态方法(如 StringUtils.isEmpty(str)),错误地通过一个可能为 null 的对象引用去调用(如 strUtils.isEmpty(str)),而 strUtils 恰好是 null,就会触发 NPE。
立即学习“Java免费学习笔记(深入)”;
- 本质是「调用空引用的任意方法」,和是否静态无关;JVM 不关心语义,只看接收者是否为
null - 排查重点:IDE 中 Ctrl+Click 进入方法签名,确认是
static;调用处是否用了实例变量而非类名 - 更隐蔽的情况:Lombok 的
@UtilityClass生成的类,所有方法都是 static,但开发者可能误以为它是普通工具类实例
构造函数中过早暴露 this 引用
在构造器执行完成前,就把 this 发送给其他线程或注册到监听器、缓存、单例容器中,此时对象字段尚未初始化完毕,外部代码拿到该引用并调用其方法,极易因字段为 null 报 NPE。
- 典型模式:
new Task().registerToScheduler(this)—— 构造器里调用了外部方法,并传入this - Spring 中常见于
@PostConstruct未生效前就发布事件,或在构造器中调用ApplicationContext.publishEvent() - 根本规避方式:禁止在构造器中泄露
this;改用工厂方法 + 初始化回调,或延迟注册到 Bean 生命周期后期(如afterPropertiesSet)
异步/多线程环境下共享对象状态未同步
多个线程共用一个对象,某个线程将字段设为 null,另一线程未加锁就直接访问该字段,可能读到 null 并调用其方法 —— 表现为偶发 NPE,难以复现。
- 例子:
private Cache cache;被两个线程同时读写,线程 A 执行cache = null,线程 B 同时执行cache.get(key) - 不是简单加
synchronized就能解决:要确保「赋值」和「使用」都在同一锁保护下,且锁对象一致 - 更稳妥做法:用
volatile修饰引用(仅保证可见性,不保证原子性);或改用线程安全容器如ConcurrentHashMap;或彻底避免共享可变状态 - 注意:Spring 默认 singleton Bean 是多线程共享的,字段若可变,必须自行保证线程安全











