Java NullPointerException 根本不是空指针问题,而是契约缺失
1. 项目概述:NullPointerException 不是“空指针”,而是你代码里没写完的半句话
Java 里最常被程序员挂在嘴边、又最常被面试官拿来当开场白的问题,就是NullPointerException。它不像 OutOfMemoryError 那样吓人,也不像 StackOverflowError 那样难复现,但它像厨房里那把钝刀——不流血,但切菜费劲、剁骨费力、每次用都让你心里一紧。很多人说“NPE 就是对象为 null 调了方法”,这没错,但就像说“车祸就是车撞了”一样,漏掉了所有关键细节:谁没系安全带?红灯闯了几次?刹车片是不是上个月就该换了?
我带过二十多个 Java 项目,从银行核心账务系统到 IoT 设备管理平台,NPE 出现频率排前三,但真正因“真 null”导致的不到 15%。剩下 85%,全是逻辑断点没补全、契约没声明、边界没兜底、测试没覆盖留下的技术债。比如一个User user = userService.findById(id)返回 null,你直接user.getName()—— 这不是 Java 的错,是你没回答一个问题:“如果查不到用户,业务上该怎么走?” 是抛异常?返回默认用户?还是跳转到注册页?Java 只负责报错,不替你做决策。
这个标题里的三个动词——Detect(检测)、Fix(修复)、Best Practices(最佳实践)——不是线性流程,而是一个闭环:检测是为了定位“谁在说半句话”,修复是补上后半句,最佳实践则是让团队以后少说半句话。它不只关乎 try-catch 写几行,更关乎 API 设计规范怎么定、单元测试覆盖率怎么设、IDE 提示怎么开、甚至 Code Review checklist 里要不要加一条“所有外部输入必须校验非空”。
如果你正被 NPE 困扰:线上日志里满屏红字、本地调试半天找不到 null 来源、或者面试时被问“如何避免 NPE”只能答出“加 if 判断”——这篇内容就是为你写的。它不讲教科书定义,不堆概念,只讲我在真实项目里踩过的坑、压测时翻车的现场、Code Review 中被揪出的低级错误,以及最终沉淀下来的、能直接抄进自己项目的检查清单和配置模板。
关键词里 “java” 和 “java面试题” 高频出现,说明大量开发者是在求职压力下才开始正视 NPE。但我要说句实在话:把 NPE 当成面试八股文背,不如花 20 分钟配好 IDE 的空值检查,再花 1 小时写个@NonNull+@Nullable的全局约定。因为面试官问的从来不是“NPE 是什么”,而是“你怎么让团队不再为它加班”。
2. 核心思路拆解:为什么“加 if 判断”是最差的修复方式?
2.1 检测 ≠ 日志里找 stack trace,而是把问题拦在编译期和运行前
很多团队的 NPE 处理流程是这样的:线上报警 → 查日志 → 翻 stack trace → 定位某行xxx.getName()→ 加个if (xxx != null)→ 发版。这套流程看似闭环,实则在纵容漏洞。它把本该在设计阶段解决的契约问题,拖到了生产环境靠“救火”来补。
真正的 Detect,分三层,缺一不可:
编译期检测:靠注解(如
@NonNull)+ Lombok + 编译器插件(如 ErrorProne),让 null 调用在敲下.的瞬间就被 IDE 标红。这不是可选项,是基建。我见过最狠的案例:某支付中台强制所有 DTO 字段加@NonNull,CI 流程里跑 ErrorProne 插件,只要检测到潜在 NPE,构建直接失败。上线三年,NPE 相关故障归零。运行时检测:不是等它崩,而是主动“试毒”。比如 Spring Boot 的
@Valid+@NotNull组合,对 Controller 入参做前置校验;或自定义 AOP 切面,在 Service 方法入口统一拦截 null 参数并转成IllegalArgumentException。这比 catch NPE 后 log.warn 更早、更准、更可控。测试期检测:单元测试里必须包含 null 输入用例。不是随便写个
testNullUserShouldThrowException()就完事,而是用junit-jupiter-params配合@NullSource、@EmptySource自动生成边界数据。我们团队规定:所有 public 方法的单元测试覆盖率中,“null 参数路径”必须单独打勾,否则 MR 不通过。
提示:别迷信 “Optional 能消灭 NPE”。它只是把 null 包装成一个对象,但
Optional.empty().get()依然抛 NPE。真正起作用的是 Optional 强制你思考“值不存在时怎么办”,而不是把它当 null 的马甲。
2.2 Fix ≠ 补一行 if,而是重构调用链的契约关系
Fix 这个词最容易误导人。看到 NPE 就想“修掉它”,但 NPE 是症状,不是病根。比如这段典型代码:
public String getUserName(Long userId) { User user = userRepository.findById(userId); // 可能返回 null return user.getName(); // NPE 在这里 }最差的 Fix:
if (user != null) { return user.getName(); } else { return "Unknown"; }问题在哪?
- 责任错位:
userRepository.findById()声明返回User,却可能返回 null,违反了“方法签名即契约”的原则; - 语义丢失:“Unknown” 是业务兜底,但没说明为什么未知——是 ID 不存在?数据库连不上?还是缓存穿透?
- 扩散风险:下游调用方看到返回 “Unknown”,无法区分是正常兜底还是异常状态,可能掩盖更严重的问题。
正确的 Fix 路径有三条,按优先级排序:
- 改上游契约:让
findById()明确返回Optional<User>,强制调用方处理空值。这是最彻底的,但需全链路改造,适合新项目或大版本迭代。 - 改当前方法语义:把
getUserName()改成findUserNameById(),返回Optional<String>,把空值处理权交给调用方。 - 加业务级异常:
throw new UserNotFoundException("User not found for id: " + userId),由全局异常处理器统一转 HTTP 404 或业务码。
选哪条?看上下文。如果是内部服务间调用,选 1 或 2;如果是面向前端的 API,选 3。没有银弹,只有权衡。
2.3 Best Practices 不是“写文档”,而是嵌入开发流水线的硬规则
很多团队写了一堆《Java 空值处理规范》,结果没人看。最佳实践要落地,必须变成“不做就不让过”的卡点。我们团队的硬规则有四条:
- IDE 强制:所有开发机安装 IntelliJ 的 “Nullability Annotations” 插件,
@NonNull默认开启,@Nullable必须显式标注。新建类时,Lombok 的@RequiredArgsConstructor自动忽略@Nullable字段,避免构造时传 null。 - CI 卡点:Maven 构建时集成
maven-checkstyle-plugin,规则里有一条:if (obj == null)必须紧跟throw new IllegalArgumentException(...)或return,禁止出现if (obj == null) { /* do nothing */ }。 - MR 检查项:Code Review checklist 第一条:“本次修改是否引入新的 null 解引用?如有,是否已通过
@NonNull/Optional/ 异常明确声明?” - 日志规范:所有捕获的 NPE,log.error 必须带上下文参数,格式为
"NPE in [method] for [key=value], args=[...]",禁止只写"NPE occurred"。
这些规则不是为了增加负担,而是把“写防御性代码”变成肌肉记忆。就像开车系安全带,一开始觉得麻烦,习惯后反而不系不舒服。
3. 核心细节解析与实操要点:从 IDE 配置到注解实战
3.1 IntelliJ IDEA 零配置启动空值检查(JDK 11+)
很多人以为空值检查要装一堆插件、配复杂规则,其实 JDK 11+ 的 IntelliJ 已内置足够强的能力。关键不是“能不能”,而是“敢不敢开”。
第一步:启用编译器空值分析Settings → Build → Compiler → Java Compiler → Additional command line parameters
添加:
-Xlint:unchecked -Xlint:deprecation -Xlint:cast -Xlint:empty -Xlint:fallthrough -Xlint:finally -Xlint:path -Xlint:serial -Xlint:try -Xlint:all其中-Xlint:nullable(JDK 15+)或-Xlint:all(JDK 11-14)会触发空值警告。但光有编译器不够,IDE 需要感知。
第二步:激活注解驱动检查Settings → Editor → Inspections → Java → Probable bugs → Nullability problems
勾选全部子项,尤其:
@NotNull/@Nullable problems(检测注解冲突)Dereferenced expression is potentially null(标红潜在 NPE)Redundant null check(删掉没用的 if)
第三步:设置默认注解策略(最关键)Settings → Editor → Inspections → Java → Nullability problems → Configure annotations
点击+添加:
@NonNull→org.jetbrains.annotations.NotNull(推荐,IntelliJ 原生支持)@Nullable→org.jetbrains.annotations.Nullable
然后勾选Configure default annotation for method return values和for parameters,设为@NotNull。
这意味着:只要你没显式写@Nullable,IDE 就默认所有参数和返回值非空。一旦你写了User user = null;,紧接着user.getName(),IDE 立刻标黄警告:“Method call may produce 'NullPointerException'”。这不是猜测,是基于注解的静态分析。
注意:不要用
javax.annotation.Nullable!它在 JDK 9+ 被移除,且部分工具链不兼容。org.jetbrains.annotations是 IntelliJ 官方维护,稳定可靠。
3.2 Lombok + @NonNull 实战:让构造器自动拒绝 null
Lombok 常被误用为“偷懒工具”,但它配合@NonNull能实现强契约。看这个例子:
@Data @RequiredArgsConstructor public class Order { private final Long id; @NonNull private final String status; @NonNull private final BigDecimal amount; private final String remark; // 不加 @NonNull,允许 null }@RequiredArgsConstructor会为所有final且@NonNull字段生成构造参数,并在构造时自动插入 null 检查:
public Order(Long id, String status, BigDecimal amount) { if (status == null) { throw new NullPointerException("status is marked non-null but is null"); } if (amount == null) { throw new NullPointerException("amount is marked non-null but is null"); } this.id = id; this.status = status; this.amount = amount; }这比手写Objects.requireNonNull(status, "status")更简洁,且 IDE 能在调用处提前预警。更重要的是,它把空值检查从“运行时”提前到了“构造时”,避免对象创建后处于非法状态。
实操心得:
- 对于 DTO、VO、Entity 等数据载体类,所有必填字段必须
@NonNull+final。可选字段留空,用@Nullable显式标注。 - 不要用
@Data代替@RequiredArgsConstructor+@Getter+@Setter。@Data会生成@NonNull字段的 setter,破坏不可变性。 - 如果字段类型是
List或Map,用@NonNull修饰的是容器引用本身,不是容器内元素。要保证元素非空,需额外校验或用Collections.unmodifiableList()封装。
3.3 Spring Boot 中的空值防御三板斧
Spring 生态提供了天然的空值防护层,不用白不用。
第一板斧:Controller 层 @Valid + @NotNull
@PostMapping("/orders") public ResponseEntity<Order> createOrder(@Valid @RequestBody OrderRequest request) { return ResponseEntity.ok(orderService.create(request)); }OrderRequest类:
public class OrderRequest { @NotNull(message = "userId cannot be null") private Long userId; @NotBlank(message = "productCode cannot be blank") private String productCode; @NotNull @Min(value = 1, message = "quantity must be at least 1") private Integer quantity; }Spring Validation 会在请求体反序列化后、进入 Controller 方法前,自动校验所有约束。失败时抛MethodArgumentNotValidException,由@ControllerAdvice统一处理为 400 Bad Request。这比在方法里手动if (request.getUserId() == null)干净十倍。
第二板斧:Service 层 @Validated + 分组校验
@Service @Validated public class OrderService { public Order create(@Validated(Create.class) OrderRequest request) { // ... } }分组校验解决“同一对象在不同场景下校验规则不同”的问题。比如创建订单要校验userId,更新订单时userId不可改,但status必须校验。
第三板斧:Repository 层 Optional 化
Spring Data JPA 2.0+ 默认将findById()等查询方法返回Optional<T>。但很多人仍写:
Optional<User> optionalUser = userRepository.findById(id); if (optionalUser.isPresent()) { return optionalUser.get().getName(); }这是对 Optional 的侮辱。正确写法:
return userRepository.findById(id) .map(User::getName) .orElseThrow(() -> new UserNotFoundException("User not found: " + id));map()和orElseThrow()的组合,把空值处理逻辑压缩成一行,且语义清晰:取名字,取不到就抛业务异常。
注意:
Optional不应作为 DTO 字段或数据库字段类型。它不是为持久化设计的,JPA 不支持。只用于方法返回值,表示“可能无结果”。
4. 实操过程与核心环节实现:从零搭建 NPE 防御体系
4.1 项目初始化:Maven 依赖与插件配置(Spring Boot 3.x)
一个能自动拦截 NPE 的项目,起步配置比想象中简单。以下是pom.xml关键片段(基于 Spring Boot 3.2 + JDK 17):
<properties> <java.version>17</java.version> <lombok.version>1.18.30</lombok.version> <errorprone.version>2.23.0</errorprone.version> </properties> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Lombok(必须 scope=provided) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- JetBrains 注解(编译期使用) --> <dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>24.0.1</version> <scope>compile</scope> </dependency> <!-- Spring Validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> <build> <plugins> <!-- Maven Compiler Plugin:启用 JDK 17 空值检查 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> <encoding>UTF-8</encoding> <compilerArgs> <arg>-Xlint:all</arg> <arg>-Xlint:-options</arg> <arg>-Xlint:-processing</arg> </compilerArgs> </configuration> </plugin> <!-- ErrorProne:编译期捕获 NPE --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> <annotationProcessorPaths> <path> <groupId>com.google.errorprone</groupId> <artifactId>errorprone-core</artifactId> <version>${errorprone.version}</version> </path> </annotationProcessorPaths> <compilerArgs> <arg>-Xplugin:ErrorProne</arg> <arg>-Xep:NullAway:ERROR</arg> <arg>-XepOpt:NullAway:AnnotatedPackages=com.yourpackage</arg> </compilerArgs> </configuration> </plugin> </plugins> </build>关键点说明:
errorprone-core是 Google 开发的静态分析工具,NullAway是其子规则,能精准识别未标注@Nullable的潜在空值路径。-XepOpt:NullAway:AnnotatedPackages指定需要检查的包名,避免扫描第三方库。maven-compiler-plugin配置了两次?是的。第一次是基础编译,第二次是集成 ErrorProne。Maven 允许插件重复声明,后声明的会覆盖前声明的配置。
验证是否生效:
写一个故意触发 NPE 的测试类:
public class NpeTest { @NonNull private String name; public void badMethod() { System.out.println(name.length()); // name 未初始化,此处应报错 } }执行mvn compile,控制台会输出:
[ERROR] ... NpeTest.java:[8,31] error: [NullAway] dereferenced expression name is @Nullable说明编译期检查已生效。
4.2 全局异常处理器:把 NPE 转成可读业务响应
Spring Boot 的@ControllerAdvice是处理 NPE 的最后一道防线,但绝不能让它成为主防线。它的作用是兜底,不是主力。
@ControllerAdvice @Slf4j public class GlobalExceptionHandler { // 捕获所有未处理的 NPE,转成 500 Internal Server Error @ExceptionHandler(NullPointerException.class) public ResponseEntity<ErrorResponse> handleNpe(NullPointerException e, HttpServletRequest request) { log.error("NPE occurred in {} {}, params={}", request.getMethod(), request.getRequestURL(), request.getParameterMap(), e); ErrorResponse error = new ErrorResponse( "INTERNAL_ERROR", "System error, please contact admin", System.currentTimeMillis() ); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); } // 捕获业务异常,转成 400/404 @ExceptionHandler(UserNotFoundException.class) public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) { log.warn("User not found: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ErrorResponse("USER_NOT_FOUND", e.getMessage(), System.currentTimeMillis())); } }ErrorResponse类:
@Data @AllArgsConstructor public class ErrorResponse { private String code; // 业务码,如 USER_NOT_FOUND private String message; // 用户友好提示 private long timestamp; // 时间戳,方便排查 }为什么先捕获 NPE,再捕获业务异常?
因为UserNotFoundException是RuntimeException的子类,而NullPointerException也是。Spring 按@ExceptionHandler方法参数类型的继承关系匹配,父类(NPE)的 handler 会匹配所有子类异常,所以必须把更具体的异常(UserNotFoundException)放在前面,否则全被 NPE handler 拦截了。
实操心得:线上环境必须关闭
spring.devtools.restart.enabled=true。开发时热部署方便,但重启时类加载器可能残留旧字节码,导致@NonNull检查失效,NPE 在本地不报,上线就崩。
4.3 单元测试全覆盖:用 JUnit 5 ParameterizedTest 打爆 null 边界
NPE 最爱藏在边界条件里。手工写testNullUserId()、testEmptyProductCode()太累,用@ParameterizedTest自动生成。
@ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private UserRepository userRepository; @InjectMocks private OrderService orderService; @ParameterizedTest @NullSource @EmptySource @ValueSource(strings = {" ", "\t", "\n"}) void shouldThrowExceptionWhenProductCodeIsInvalid(String productCode) { // given OrderRequest request = new OrderRequest(); request.setProductCode(productCode); // when & then assertThatThrownBy(() -> orderService.create(request)) .isInstanceOf(MethodArgumentNotValidException.class); } @Test void shouldThrowUserNotFoundExceptionWhenUserNotFound() { // given when(userRepository.findById(123L)).thenReturn(Optional.empty()); // when & then assertThatThrownBy(() -> orderService.create(validOrderRequest())) .isInstanceOf(UserNotFoundException.class) .hasMessage("User not found for id: 123"); } }@NullSource生成null,@EmptySource生成"",@ValueSource生成指定字符串。JUnit 5 会为每个值运行一次测试,确保所有空值路径都被覆盖。
覆盖率目标:
@NotNull字段的构造器 null 检查,必须有对应测试;Optional.map().orElseThrow()路径,必须有Optional.empty()的测试;@Valid校验,必须有@NullSource和@EmptySource的测试。
我们团队的 Jacoco 覆盖率红线是:@NotNull相关分支覆盖率 100%,Optional的isPresent()/isEmpty()分支各 100%。没达标,CI 直接失败。
5. 常见问题与排查技巧实录:那些年我们踩过的 NPE 坑
5.1 诡异 NPE:IDE 不报错,运行时报,但变量明明不为 null
现象:
User user = userRepository.findById(1L).orElseThrow(); String name = user.getName(); // 这里报 NPEDebug 时user不为 null,user.getClass()是User,但user.getName()就崩。
原因:
这是 Hibernate/JPA 的经典代理坑。userRepository.findById()返回的不是真实User对象,而是User$HibernateProxy$abc123代理对象。getName()调用时,代理会去数据库查name字段,但如果数据库里name是 NULL,代理层就会抛 NPE,而不是返回 null。
解决方案:
- 数据库层面:
name字段设为NOT NULL,从源头杜绝; - 实体类层面:
@Column(nullable = false)+@NotNull双重约束; - 查询层面:用
@Query自定义 SQL,明确SELECT u.name FROM user u WHERE u.id = ?1,避免代理延迟加载。
提示:在
application.properties加spring.jpa.properties.hibernate.jdbc.batch_size=20和spring.jpa.open-in-view=false,能减少代理滥用。
5.2 隐形 NPE:JSON 反序列化时字段为 null,但没加 @Nullable
现象:
前端传{ "status": "PENDING" },后端OrderRequest类:
public class OrderRequest { private String status; private BigDecimal amount; // 前端没传,反序列化后为 null }orderRequest.getAmount().doubleValue()报 NPE。
原因:
Jackson 默认把缺失字段反序列化为 null,但amount字段没加@Nullable,IDE 认为它非空,不警告。运行时amount就是 null。
解决方案:
- 方案一(推荐):所有可能缺失的字段,显式加
@Nullable,并配 Jackson 注解:@Nullable @JsonProperty(required = false) private BigDecimal amount; - 方案二:用
@JsonInclude(JsonInclude.Include.NON_NULL)在类上,让 Jackson 忽略 null 字段,但需配合@NotNull校验; - 方案三:用
@DefaultValue("0.00")(需自定义反序列化器),但语义不清,不推荐。
5.3 多线程 NPE:ConcurrentHashMap 的 computeIfAbsent 返回 null
现象:
private final Map<String, List<String>> cache = new ConcurrentHashMap<>(); public List<String> getTags(String key) { return cache.computeIfAbsent(key, k -> loadFromDB(k)); // loadFromDB 可能返回 null }loadFromDB(k)返回 null,computeIfAbsent就把 null 存进 map,下次cache.get(key)就是 null,调用.size()崩。
原因:computeIfAbsent的 JavaDoc 写得很清楚:“If the specified key is not already associated with a value (or is mapped to null), attempts to compute its value using the given mapping function…” 它接受 null 作为计算结果。
解决方案:
- 永远不在
computeIfAbsent的 mapping function 里返回 null; - 改用
compute:
或更安全的:return cache.compute(key, (k, v) -> v == null ? loadFromDB(k) : v);return cache.computeIfAbsent(key, k -> { List<String> result = loadFromDB(k); return result == null ? Collections.emptyList() : result; });
5.4 NPE 排查速查表
| 现象 | 可能原因 | 快速验证方法 | 修复方案 |
|---|---|---|---|
| IDE 不标红,但运行时报 NPE | @NonNull注解未生效,或用了错误的包 | 检查import org.jetbrains.annotations.NotNull;是否存在;执行mvn compile -X看 ErrorProne 是否加载 | 替换为org.jetbrains.annotations,确认 Maven 插件配置 |
| Controller 层 @Valid 不生效 | @RequestBody参数没加@Valid,或spring-boot-starter-validation依赖缺失 | Postman 发送{"status":null},看是否返回 400 | 检查依赖、注解位置、@Validated是否在类上 |
| Optional.get() 报 NPE | Optional是空的,但调用了get() | 在get()前加log.debug("Optional present: {}", optional.isPresent()) | 改用map().orElse()或orElseThrow() |
| MyBatis 查询返回 null,但字段没加 @Nullable | XML 中<result column="name" property="name"/>对应数据库 NULL | Debug 看实体对象字段值;查数据库该字段是否允许 NULL | 实体字段加@Nullable;数据库字段设 NOT NULL |
| Lombok @Data 生成的 setter 允许 null | @Data会为所有字段生成 setter,包括@NonNull字段 | 写测试new Order().setStatus(null),看是否编译通过 | 改用@RequiredArgsConstructor+@NonNull,禁用@Data |
实操心得:线上 NPE 故障,第一反应不是看代码,而是看日志时间戳和请求 ID。我们团队的日志格式是
[TRACE_ID] [LEVEL] [CLASS] [METHOD] [MSG],用 ELK 搜索NPE+TRACE_ID,5 秒定位到具体请求和参数,比翻代码快十倍。
6. 个人经验总结:NPE 消灭战的本质是团队认知对齐
写完这五千多字,我想说点掏心窝的话。NPE 不是技术问题,是协作问题。我见过太多团队,后端写@NonNull,前端传null,测试写用例时照着 Swagger 文档填默认值,结果上线就崩。为什么?因为没人坐下来一起定义:“这个字段,业务上到底允不允许为空?如果为空,前端展示什么?后端返回什么状态码?日志怎么记?”
所以,我最后分享三个不写进代码,但比任何注解都管用的经验:
第一,把 NPE 写进需求文档。
PRD 里不能只写“用户可以输入姓名”,要写“姓名为必填项,长度 2-20 字,仅支持中文、英文、数字、空格,为空时前端高亮红色边框,后端返回 400 错误码 USER_NAME_REQUIRED”。把空值当成一个独立需求点,而不是开发时拍脑袋决定。
第二,Code Review 时,每人必须问一句:“这个变量,什么时候会是 null?”
不是问“会不会 null”,是问“什么时候会”。如果回答是“永远不会”,那就加@NonNull;如果回答是“数据库查不到时”,那就加Optional或业务异常;如果回答是“我不知道”,那就打回重写。
第三,把 NPE 故障当成最高优先级事故,复盘时只问事实,不追责。
复盘会记录:
- 哪个环节漏掉了空值校验?(是设计?编码?测试?)
- 对应的 CheckList 是否缺失?(是没写,还是写了但没执行?)
- 下次如何自动化拦截?(加 ErrorProne 规则?改 CI 脚本?)
不提“张三没写 if”,只提“我们的 CheckList 缺少‘所有外部输入必须校验’这一条”。改变的是流程,不是人。
NPE 不会消失,但可以变得稀有。就像交通事故不会归零,但安全带、ABS、自动刹车能让它不再致命。你不需要成为 Java 大神,只需要在每次写.的时候,多问半秒钟:“它真的不为 null 吗?” —— 这半秒,就是专业和业余的分水岭。
