瞧瞧別人家的判空,那叫一個優(yōu)雅!
一、傳統(tǒng)判空的血淚史
某互聯(lián)網(wǎng)金融平臺因費用計算層級的空指針異常,導(dǎo)致凌晨產(chǎn)生9800筆錯誤交易。
DEBUG日志顯示問題出現(xiàn)在如下代碼段:
// 錯誤示例
BigDecimal amount = user.getWallet().getBalance().add(new BigDecimal("100"));
此類鏈?zhǔn)秸{(diào)用若中間環(huán)節(jié)出現(xiàn)null值,必定導(dǎo)致NPE。
初級階段開發(fā)者通常寫出多層嵌套式判斷:
if(user != null){
Wallet wallet = user.getWallet();
if(wallet != null){
BigDecimal balance = wallet.getBalance();
if(balance != null){
// 實際業(yè)務(wù)邏輯
}
}
}
這種寫法既不優(yōu)雅又影響代碼可讀性。
那么,我們該如何優(yōu)化呢?
二、Java 8+時代的判空革命
Java8之后,新增了Optional類,它是用來專門判空的。
能夠幫你寫出更加優(yōu)雅的代碼。
1. Optional黃金三板斧
// 重構(gòu)后的鏈?zhǔn)秸{(diào)用
BigDecimal result = Optional.ofNullable(user)
.map(User::getWallet)
.map(Wallet::getBalance)
.map(balance -> balance.add(new BigDecimal("100")))
.orElse(BigDecimal.ZERO);
高級用法:條件過濾
Optional.ofNullable(user)
.filter(u -> u.getVipLevel() > 3)
.ifPresent(u -> sendCoupon(u)); // VIP用戶發(fā)券
2. Optional拋出業(yè)務(wù)異常
BigDecimal balance = Optional.ofNullable(user)
.map(User::getWallet)
.map(Wallet::getBalance)
.orElseThrow(() -> new BusinessException("用戶錢包數(shù)據(jù)異常"));
3. 封裝通用工具類
public class NullSafe {
// 安全獲取對象屬性
public static <T, R> R get(T target, Function<T, R> mapper, R defaultValue) {
return target != null ? mapper.apply(target) : defaultValue;
}
// 鏈?zhǔn)桨踩僮? public static <T> T execute(T root, Consumer<T> consumer) {
if (root != null) {
consumer.accept(root);
}
return root;
}
}
// 使用示例
NullSafe.execute(user, u -> {
u.getWallet().charge(new BigDecimal("50"));
logger.info("用戶{}已充值", u.getId());
});
三、現(xiàn)代化框架的判空銀彈
4. Spring實戰(zhàn)技巧
Spring中自帶了一些好用的工具類,比如:CollectionUtils、StringUtils等,可以非常有效的進(jìn)行判空。
具體代碼如下:
// 集合判空工具
List<Order> orders = getPendingOrders();
if (CollectionUtils.isEmpty(orders)) {
return Result.error("無待處理訂單");
}
// 字符串檢查
String input = request.getParam("token");
if (StringUtils.hasText(input)) {
validateToken(input);
}
5. Lombok保駕護(hù)航
我們在日常開發(fā)中的entity對象,一般會使用Lombok框架中的注解,來實現(xiàn)getter/setter方法。
其實,這個框架中也提供了@NonNull等判空的注解。
比如:
@Getter
@Setter
public class User {
@NonNull // 編譯時生成null檢查代碼
private String name;
private Wallet wallet;
}
// 使用構(gòu)造時自動判空
User user = new User(@NonNull "張三", wallet);
四、工程級解決方案
6. 空對象模式
public interface Notification {
void send(String message);
}
// 真實實現(xiàn)
public class EmailNotification implements Notification {
@Override
public void send(String message) {
// 發(fā)送郵件邏輯
}
}
// 空對象實現(xiàn)
public class NullNotification implements Notification {
@Override
public void send(String message) {
// 默認(rèn)處理
}
}
// 使用示例
Notification notifier = getNotifier();
notifier.send("系統(tǒng)提醒"); // 無需判空
7. Guava的Optional增強
其實Guava工具包中,給我們提供了Optional增強的功能。
比如:
import com.google.common.base.Optional;
// 創(chuàng)建攜帶缺省值的Optional
Optional<User> userOpt = Optional.fromNullable(user).or(defaultUser);
// 鏈?zhǔn)讲僮髋浜螰unction
Optional<BigDecimal> amount = userOpt.transform(u -> u.getWallet())
.transform(w -> w.getBalance());
Guava工具包中的Optional類已經(jīng)封裝好了,我們可以直接使用。
五、防御式編程進(jìn)階
8. Assert斷言式攔截
其實有些Assert斷言類中,已經(jīng)做好了判空的工作,參數(shù)為空則會拋出異常。
這樣我們就可以直接調(diào)用這個斷言類。
例如下面的ValidateUtils類中的requireNonNull方法,由于它內(nèi)容已經(jīng)判空了,因此,在其他地方調(diào)用requireNonNull方法時,如果為空,則會直接拋異常。
我們在業(yè)務(wù)代碼中,直接調(diào)用requireNonNull即可,不用寫額外的判空邏輯。
例如:
public class ValidateUtils {
public static <T> T requireNonNull(T obj, String message) {
if (obj == null) {
throw new ServiceException(message);
}
return obj;
}
}
// 使用姿勢
User currentUser = ValidateUtils.requireNonNull(
userDao.findById(userId),
"用戶不存在-ID:" + userId
);
9. 全局AOP攔截
我們在一些特殊的業(yè)務(wù)場景種,可以通過自定義注解 + 全局AOP攔截器的方式,來實現(xiàn)實體或者字段的判空。
例如:
@Aspect
@Component
public class NullCheckAspect {
@Around("@annotation(com.xxx.NullCheck)")
public Object checkNull(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg == null) {
throw new IllegalArgumentException("參數(shù)不可為空");
}
}
return joinPoint.proceed();
}
}
// 注解使用
public void updateUser(@NullCheck User user) {
// 方法實現(xiàn)
}
六、實戰(zhàn)場景對比分析
場景1:深層次對象取值
// 舊代碼(4層嵌套判斷)
if (order != null) {
User user = order.getUser();
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String city = address.getCity();
// 使用city
}
}
}
// 重構(gòu)后(流暢鏈?zhǔn)剑?String city = Optional.ofNullable(order)
.map(Order::getUser)
.map(User::getAddress)
.map(Address::getCity)
.orElse("未知城市");
場景2:批量數(shù)據(jù)處理
List<User> users = userService.listUsers();
// 傳統(tǒng)寫法(顯式迭代判斷)
List<String> names = new ArrayList<>();
for (User user : users) {
if (user != null && user.getName() != null) {
names.add(user.getName());
}
}
// Stream優(yōu)化版
List<String> nameList = users.stream()
.filter(Objects::nonNull)
.map(User::getName)
.filter(Objects::nonNull)
.collect(Collectors.toList());
七、性能與安全的平衡藝術(shù)
上面介紹的這些方案都可以使用,但除了代碼的可讀性之外,我們還需要考慮一下性能因素。
下面列出了上面的幾種在CPU消耗、內(nèi)存只用和代碼可讀性的對比:
方案 | CPU消耗 | 內(nèi)存占用 | 代碼可讀性 | 適用場景 |
多層if嵌套 | 低 | 低 | ★☆☆☆☆ | 簡單層級調(diào)用 |
Java Optional | 中 | 中 | ★★★★☆ | 中等復(fù)雜度業(yè)務(wù)流 |
空對象模式 | 高 | 高 | ★★★★★ | 高頻調(diào)用的基礎(chǔ)服務(wù) |
AOP全局?jǐn)r截 | 中 | 低 | ★★★☆☆ | 接口參數(shù)非空驗證 |
黃金法則
- Web層入口強制參數(shù)校驗
- Service層使用Optional鏈?zhǔn)教幚?/li>
- 核心領(lǐng)域模型采用空對象模式
八、擴(kuò)展技術(shù)
除了,上面介紹的常規(guī)判空之外,下面再給大家介紹兩種擴(kuò)展的技術(shù)。
Kotlin的空安全設(shè)計
雖然Java開發(fā)者無法直接使用,但可借鑒其設(shè)計哲學(xué):
val city = order?.user?.address?.city ?: "default"
JDK 14新特性預(yù)覽
// 模式匹配語法嘗鮮
if (user instanceof User u && u.getName() != null) {
System.out.println(u.getName().toUpperCase());
}
總之,優(yōu)雅判空不僅是代碼之美,更是生產(chǎn)安全底線。
本文分享了代碼判空的10種方案,希望能夠幫助你編寫出既優(yōu)雅又健壯的Java代碼。