Java基础、中级、高级、架构面试资料

只因1个null造成10亿美金损失,救赎的Optional 80%的人还没用对

JAVA herman 16浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

只因 1 个 null 造成 10 亿美金损失,救赎的 Optional 80% 的人还用错了。

最近我在 CodeReview 团队里的 Java 代码时,发现不少同事想用 Java 8 提供的 Optional,但是 80% 的人用错了。甚至有些同事告诉我,Optional 并不能解决 NullPointerException。

这其中的细节我就不说了,我相信不少人对 Optional 有极大的误解。但这些我们都不表,我们来看看 null 问题的一些黑历史,以及 Optional 的一些最佳实践吧。

null 的发明者是谁?

我相信程序员都知道 null,但很少有人知道 null 的发明者是谁。说他是功臣吧,他给大家带来了无数的麻烦。说他是祸害吧,他确实解决了程序设计中的一些问题,还荣获了 1980 年的图灵奖。

话说,1965 年,英国计算机科学家托尼·霍尔(Tony Hoare)在设计编程语言 ALGOL W 时引入了 null 引用。他回忆说,引入 null 的主要原因非常简单,因为它实现起来太容易了simply because it was so easy to implement。因为 null 的引入,他在算法(如快速排序)和程序正确性逻辑(Hoare logic)方面做出了杰出贡献。

当时他希望有一种方式来表示“指针不指向任何对象”的状态,作为对尚未初始化或无效引用的占位符。这一设计在语言实现层面确实简化了逻辑。

但是,他没想到 null 后带来的影响这么大。以至于霍尔后来深切地后悔这一决定。他将 null 引用称为我的十亿美元错误my billion-dollar mistake)。原因在于:

  • null 引用导致了无数的运行时错误,尤其是空指针异常(NullPointerException);
  • 这些错误难以在编译期发现,常常在生产环境中引发系统崩溃、安全漏洞或严重故障;
  • 全球软件行业为此付出了巨大的调试、维护和经济损失,估计远超十亿美元。

因此,虽然 null 的初衷是为了简化语言设计,但它在实践中成为了软件可靠性的一大隐患,也促使现代语言(如 Kotlin、Swift、Rust)和 Java 8+ 的 Optional 等机制重新思考“如何更安全地表达缺失值”。

Null 的十亿美元错误

我称之为我的十亿美元错误。—— Tony Hoare(null 引用的发明者)

null 的作者完全低估了 null 带来的影响,有人统计,由 Null 带来的错误,远远不止 10 亿美金。光人工成本都不止,还不说由 null 引发的生产事故造成的经济价值。

所以,程序员或者说是不少程序设计者都试图更好的解决这类问题。比如,Java 8,引入的 Optional 类等。

要知道,在 Java 开发中,NullPointerException 可能是每个开发者最熟悉的“老朋友”。每个 Java 开发者应该都经历过空异常的噩梦。而 Java 中的 NullPointerException 经常会被其它语言从业者用来诋毁,于是官方引入了Optional 类,正是为了优雅地处理这个长期困扰我们的问题。但如何正确使用Optional,却让许多开发者感到困惑。接下来,我们就来说说 Optional 的正确打开方式,告别 NullPointerException 的优雅之道。

Optional 不鸡肋

不少人觉得 Optional 鸡肋,其实是用的不对。

先看下面这个案例一。

// 传统写法
User user = getUserById(id);
if (user != null) {
    String name = user.getName();
    System.out.println(name);
}

// 被吐槽的 Optional 写法
Optional<User> userOpt = getUserById(id);
if (userOpt.isPresent()) { // 这和判空有什么区别??
    // 多了一层拆包,性能还差了
    String name = userOpt.get().getName();
    System.out.println(name);
}

上面这个案例一,代码丑陋不说,完全就没有用处 Optional 的精髓。如果都是这样用,我就理解他们所说的鸡肋了。

案例二,作为方法返回类型,我就比较推荐这种用法。

// 正确:明确表示可能没有结果
public Optional<User> findUserById(Long id) {
    return userRepository.findById(id);
}

// 错误用法,小心让调用的方法喜提报错
public Optional<User> queryById(Long id) {
    return null;
}

// 调用方清晰处理
userService.findUserById(1L)
    .ifPresent(user -> System.out.println(user.getName()));

案例三,错误的用法,用 Optional 作为方法参数。

// 错误:使 API 更复杂,调用方必须包装参数
public void updateUser(Optional<String> name, Optional<Integer> age) {
    // 不推荐!
}

// 正确做法:使用方法重载
public void updateUser(String name, Integer age) {
    // ...
}

public void updateUser(String name) {
    updateUser(name, null);
}

案例四,错误的用法。直接调用 get() 而不检查。

// 错误:和直接使用null没区别
Optional<User> user = findUserById(1L);
// 可能抛出NoSuchElementException
String name = user.get().getName(); 

// 正确:安全获取
String name = user.map(User::getName).orElse("默认名称业余草");

案例五,优雅的链式操作。链式调用与值转换。

// 正确:优雅的链式操作
public String getUserCityName(Long userId) {
    return userService.findUserById(userId)
        .map(User::getAddress)
        .map(Address::getCity)
        .map(City::getName)
        .orElse("未知城市");
}

案例六,安全地获取值。提供默认值。

// 正确:安全地获取值
String userName = optionalUser
    .map(User::getName)
    .orElse("匿名用户");

// 或延迟计算默认值
String userName = optionalUser
    .map(User::getName)
    .orElseGet(() -> generateDefaultName());

案例七,条件执行。只在值存在时执行操作。

// 正确:只在值存在时执行操作
optionalUser.ifPresent(user -> {
    sendWelcomeEmail(user.getEmail());
    updateLastLogin(user.getId());
});

案例八,错误用法。在集合中使用 Optional。

// 错误:增加不必要的复杂度
List<Optional<User>> users = new ArrayList<>();

// 正确:直接使用空集合或过滤
List<User> activeUsers = allUsers.stream()
    .filter(User::isActive)
    .collect(Collectors.toList());

还有人在 dto、po、do 等实体类属性上使用 Optional,另类。

案例九,过滤特定条件。

// 正确:结合过滤条件
Optional<User> activeUser = optionalUser
    .filter(user -> user.isActive() && user.hasVerifiedEmail());

案例十,错误用法。过度使用isPresent()get()模式。

// 错误:函数式风格的倒退
if (optionalUser.isPresent()) {
    User user = optionalUser.get();
    // 处理user
}

// 正确:使用函数式风格
optionalUser.ifPresent(user -> {
    // 处理user
});

案例十一,错误用法。创建包含 null 的 Optional。

// 错误:违背了 Optional 的设计初衷
Optional<User> user = Optional.ofNullable(someMethod());
// 如果someMethod()返回null,这没问题
// 但不要这样做:
Optional<User> user = Optional.of(null);
// 直接抛出 NullPointerException

案例十二,用户服务重构。

// 重构前:传统的null检查
public String getUserProfile(Long userId) {
    User user = userDao.findById(userId);
    if (user != null) {
        Address address = user.getAddress();
        if (address != null) {
            City city = address.getCity();
            if (city != null) {
                return city.getName();
            }
        }
    }
    return "未知";
}

// 重构后:Optional 的优雅实现
public String getUserProfile(Long userId) {
    return userDao.findOptionalById(userId)
        .flatMap(user -> Optional.ofNullable(user.getAddress()))
        .flatMap(address -> Optional.ofNullable(address.getCity()))
        .map(City::getName)
        .orElse("业余草");
}

// 更简洁的写法(如果getter方法也返回Optional)
public String getUserProfile(Long userId) {
    return userDao.findOptionalById(userId)
        .flatMap(User::getAddressAsOptional)
        .flatMap(Address::getCityAsOptional)
        .map(City::getName)
        .orElse("业余草");
}

最佳实践

简单来说,Optional 是一个容器,代表该容器内的实例或对象也好,可能为空。但最不该的是,连 Optional 这个容器,有些人都能返回 null,连容器都是 null,那还玩个屁呀!!!

直接看下面的最佳实践吧。

设计原则

  • Optional 主要用作返回类型
  • 不要用它作为字段类型或方法参数
  • 集合中直接使用空集合而不是 Optional

编码规范

  • 优先使用orElseGet()而不是orElse()(避免不必要的计算)
  • 使用map()flatMap()filter()进行链式操作
  • 避免isPresent()、get()模式

性能考虑

  • Optional 本身是一个包装对象,有轻微的内存开销
  • 在性能关键的场景要谨慎使用
  • 简单场景下,传统的 null 检查可能更高效

结语

Optional 是一个容器,让我们拥抱函数式思维。它不仅仅是一个避免 NPE 的工具,它更代表着一种编程思维的转变,从命令式的 null 检查转向声明式的值处理。正确使用 Optional 能够让我们的代码:更清晰地表达意图、减少运行时异常、提高代码可读性、鼓励函数式编程风格。

切记,Optional 不是用来消灭 null 的银弹,而是为我们提供了一种更安全、更优雅处理可能缺失值的工具。掌握它的正确用法,让我们的代码变得更加健壮和优雅。

抱怨 Optional 的时候,可以先怀疑一下是不是自己用的不对。弱者抱怨,强者自渡,圣者改变。有的人没有工具,他一无是处;有了工具,他还是一无是处🤣。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » 只因1个null造成10亿美金损失,救赎的Optional 80%的人还没用对