三元运算符 + 包装类型 + null = NullPointerException
案例还原:这段代码为什么会 NPE?
protected Long getCurrentUserId() {
return getBaseContextService() == null ? 0 : getBaseContextService().getCurrentUserId();
}
你可能会想:“我不是已经判了 null 吗?怎么还能空指针?”
关键不是 getBaseContextService() 是不是 null,而是 getCurrentUserId() 返回的是 null。
三元运算符的底层机制
编译器在做什么?
Java 编译器会这样处理:
return (getBaseContextService() == null)
? 0 // int
: getBaseContextService().getCurrentUserId(); // Long(可能为 null)
由于 0 是 int,而 getCurrentUserId() 返回 Long,两者类型不一致。
Java 编译器必须统一类型:
- int 会转成 long
- Long 会被自动拆箱成 long
代码演示:三种写法对比
错误写法(容易 NPE):
public Long getCurrentUserId() {
return getBaseContextService() == null ? 0 : getBaseContextService().getCurrentUserId();
}
报错堆栈可能是这样的:
java.lang.NullPointerException
at ...Long.longValue()...
写法一:手动统一类型(推荐)
public Long getCurrentUserId() {
return getBaseContextService() == null ? 0L : getBaseContextService().getCurrentUserId();
}
- 0L 是 Long 类型
- 不触发拆箱,避免空指针
写法二:提前处理,逻辑更清晰
public Long getCurrentUserId() {
BaseContextService ctx = getBaseContextService();
if (ctx == null) return 0L;
Long userId = ctx.getCurrentUserId();
return userId != null ? userId : 0L;
}
写法三:Java 8 Optional 优雅风格
return Optional.ofNullable(getBaseContextService())
.map(BaseContextService::getCurrentUserId)
.orElse(0L);
- 可读性强
- null 安全
Java 源码规范背书(JLS §15.25)
根据《Java Language Specification》第15.25节(三元运算符):
If one operand is of type T and the other is boxed T, then the boxed operand is unboxed to match T.
翻译一下就是:
如果一个分支是基本类型(如 long),另一个是包装类型(如 Long),Java 会自动把 Long 拆箱成 long。
所以下面这个就是编译器的真实行为:
return getBaseContextService() == null
? 0L
: getBaseContextService().getCurrentUserId().longValue(); // 这里拆箱可能抛出 NullPointerException
类型推导表
| 表达式1 | 表达式2 | 推导结果 | 是否会拆箱 | 说明 |
|---|---|---|---|---|
int | Long | long | 是 | int 先提升为 long,Long 拆箱为 long |
0L | Long | long | 是 | 0L 是 long 字面量,Long 会拆箱后比较/运算 |
Long | Long | 视场景而定 | 否/是 | == 比较引用时不拆箱;算术运算时会拆箱 |
long | Long | long | 是 | Long 需要拆箱成 long |
Long(null) | long | NullPointerException | 是 | null 拆箱时抛空指针异常 |
最佳实践总结
| 写法 | 是否安全 | 原因 |
|---|---|---|
ctx == null ? 0 : ctx.getCurrentUserId() | 否 | 可能发生拆箱,若 getCurrentUserId() 返回 null 会报错 |
ctx == null ? 0L : ctx.getCurrentUserId() | 是 | 类型统一为 Long/long 场景,更安全,避免因 0(int)导致额外类型提升问题 |
Optional.ofNullable(...).map(...).orElse(...) | 是 | 天然 null 安全,写法更现代 |
if-else 写法 | 是 | 显式判空,逻辑清晰,适合复杂分支场景 |
总结
很多人误以为三元运算只看第一个判断条件,其实两个分支的类型推导才是根源。
要想避免三元运算的坑,记住一条:包装类型就用包装值(比如 0L),不要和基本类型混用!