当前位置: 首页 > news >正文

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 路径有三条,按优先级排序:

  1. 改上游契约:让findById()明确返回Optional<User>,强制调用方处理空值。这是最彻底的,但需全链路改造,适合新项目或大版本迭代。
  2. 改当前方法语义:把getUserName()改成findUserNameById(),返回Optional<String>,把空值处理权交给调用方。
  3. 加业务级异常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
点击+添加:

  • @NonNullorg.jetbrains.annotations.NotNull(推荐,IntelliJ 原生支持)
  • @Nullableorg.jetbrains.annotations.Nullable
    然后勾选Configure default annotation for method return valuesfor 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,破坏不可变性。
  • 如果字段类型是ListMap,用@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,再捕获业务异常?
因为UserNotFoundExceptionRuntimeException的子类,而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%,OptionalisPresent()/isEmpty()分支各 100%。没达标,CI 直接失败。

5. 常见问题与排查技巧实录:那些年我们踩过的 NPE 坑

5.1 诡异 NPE:IDE 不报错,运行时报,但变量明明不为 null

现象

User user = userRepository.findById(1L).orElseThrow(); String name = user.getName(); // 这里报 NPE

Debug 时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.propertiesspring.jpa.properties.hibernate.jdbc.batch_size=20spring.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() 报 NPEOptional是空的,但调用了get()get()前加log.debug("Optional present: {}", optional.isPresent())改用map().orElse()orElseThrow()
MyBatis 查询返回 null,但字段没加 @NullableXML 中<result column="name" property="name"/>对应数据库 NULLDebug 看实体对象字段值;查数据库该字段是否允许 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 吗?” —— 这半秒,就是专业和业余的分水岭。

http://www.gsyq.cn/news/1573952.html

相关文章:

  • 2026年红木家具消费防坑深度解析:6大典型画像横评与避坑指南 - 新闻快传
  • 安顺金宝阁黄金回收实测:2026年6月行情与本地变现全攻略 - 润富黄金回收
  • 5分钟打造专业级音乐播放器:foobar2000终极美化指南
  • 电驭之外:路的永恒与你的前行
  • 2026音频转文字工具保姆级教程:免费付费电脑手机在线软件一站式操作指南 - 办公小帮手
  • 2026杭州首饰线下探店,小众门店真实经营状况曝光 - 逸程
  • 2026保姆级教程:Word文档压缩大小怎么做?压缩图片、另存为瘦身全技巧
  • 5步学会使用OpenCore Configurator:告别黑苹果配置烦恼的图形化工具
  • 终极指南:用AntiMicroX让任何游戏都支持手柄控制 [特殊字符]
  • 画线机专用墨水怎么选?翔隆笔业黄绿光浆体打印墨 Y3(651) - GrowthUME
  • 三元锂电池和磷酸铁锂电池哪个好?全面对比分析 - 锂电池大全
  • 2026年上海板材全屋优质厂家 莫干山兔宝宝板材合作门店 - 企业名录优选推荐
  • Kinetis SDK时钟管理API深度解析:从原理到实战配置
  • 终极Mac鼠标优化指南:让普通鼠标超越苹果触控板的5个专业技巧
  • 2026年贵阳名包回收与二手奢侈品鉴定完全指南|如何避坑正规平台 - 企业名录优选推荐
  • 上海名表回收终极避坑指南!门店真实排名曝光,千万别信线上高价钓鱼套路 - 逸程
  • Loop:为什么这款免费macOS窗口管理工具能让你效率翻倍?
  • 如何在5分钟内快速搭建以太坊DApp开发环境:Scaffold-ETH 2完整指南
  • 跳出大厂微服务陷阱:创业初期的架构“减法”实践
  • 新旧金饰无票也能收,沈阳黄金回收实用白皮书 - 奢侈品交易观察员
  • 第一次去新疆不用盲目做攻略,聊聊我找到的踏实本地领队阿晨,怎么找到新疆靠谱领队,去新疆旅游攻略,新疆适合万几天那个领队靠谱 - 热点速览
  • API安全实战:基于crAPI Workshop模块的漏洞挖掘与修复指南
  • 2026年初效板式过滤器袋式过滤器V型过滤器空气过滤器生产厂家值得关注的几家推荐 - 栗子测评
  • 2026年三亚回收陈年收藏老酒靠谱商家推荐:全维度实力解析 - 热点速览
  • Flutter面试题
  • 盘点汕头靠谱成考自考机构!2026 综合实力排名,大牛教育赢在哪? - 一直爱学习的小花猫
  • 2026年最新会议纪要神器亲测:多语言多方言长录音准确性高 - 小智凌凌漆
  • Claude Code省钱攻略
  • 佛山极简大宅适配全屋定制品牌2026年度排名 - 十大品牌排行榜
  • 2026 北京黄金回收梯队排名揭晓 合扬问鼎榜首成行业标杆楷模 - 奢侈品交易观察员