巧用Optional之优雅规避NPE问题

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS AI 助手,专业版
简介: 本文探讨了Java中常见的NullPointerException问题及其解决方案,重点介绍了Optional类的使用。通过实例代码分析,展示了如何用Optional替代传统的空值检查,使代码更简洁、优雅。文章详细讲解了Optional的创建方法(如of、ofNullable、empty)及常用方法(如get、orElse、map、flatMap、filter),并通过实战案例演示了其在实际开发中的应用,帮助开发者有效避免NPE问题,提升代码质量。

避之不及的 NullPointerException

NPE : NullPointerException

空指针异常是最常见的Java异常之一,抛出NPE错误不是用户操作的错误,而是开发人员的错误,应该被避免,那么只能在每个方法中加入非空检查,阅读性和维护性都比较差。

以下是一个常见的嵌套对象:一个用户所拥有的汽车,以及为这个汽车配备的保险。

代码解读

复制代码

public class User {

    private String userName;

    private Car car;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Car getCar() {
        return car;
    }

    public void setCar(Car car) {
        this.car = car;
    }
}

public class Car {

    private String carName;

    private Insurance insurance;

    public String getCarName() {
        return carName;
    }

    public void setCarName(String carName) {
        this.carName = carName;
    }

    public Insurance getInsurance() {
        return insurance;
    }

    public void setInsurance(Insurance insurance) {
        this.insurance = insurance;
    }
}

public class Insurance {

    private String insuranceName;

    public String getInsuranceName() {
        return insuranceName;
    }

    public void setInsuranceName(String insuranceName) {
        this.insuranceName = insuranceName;
    }
}

如果我们此时,需要获取一个用户对应的汽车保险名称,我们可能会写出来以下的代码

代码解读

复制代码

private String getInsuranceName(User user) {
    return user.getCar().getInsurance().getInsuranceName();
}

避免NullPointerException的方法

显然上面的程序是存在诸多NullPointerException隐患的,为了保证程序的健壮性,我们需要尽量避免出现空指针NullPointerException,那么通常我们会有以下两种写法。

深层质疑

代码解读

复制代码

private String getInsuranceName(User user) {
        if (user != null) {
            Car car = user.getCar();
            if (car != null) {
                Insurance insurance = car.getInsurance();
                if (insurance != null) {
                    return insurance.getInsuranceName();
                }
            }
        }
        return "not found";
    }

及时退出

代码解读

复制代码

    private String getInsuranceName(User user) {
        if (user == null) {
            return "not found";
        }
        Car car = user.getCar();
        if (car == null) {
            return "not found";
        }
        Insurance insurance = car.getInsurance();
        if (insurance == null) {
            return "not found";
        }
        return insurance.getInsuranceName();
    }

为了避免出现空指针,我们通常会采用以上两种写法,但是它们复杂又冗余,为了鼓励程序员写更干净的代码,代码设计变得更加的优雅。JAVA8提供了Optional类来优化这种写法。

Optional

Optional入门

Java 8中引入了一个新的类java.util.Optional。这是一个封装Optional值的类。举例来说,使用新的类意味着,如果你知道一个人可能有也可能没有车,那么User类内部的car变量就不应该声明为Car, 遇某人没有车时把null引用值给它,而是应该如下图所示直接将其声明为Optional类型。

变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空” 的Optional对象,由方法Optional.empty()返回。它返回Optional类的特定单一实例。

null引用和Optional.empty() 有什么本质的区别吗?

从语义上,你可以把它们当作一回事儿,但是实际中它们之间的差别非常大:如果你尝试直接引用一个null,一定会触发NullPointerException,不过使用 Optional.empty()就完全没事儿,它是Optional类的一个有效对象。

使用Optional而不是null的一个非常重要而又实际的语义区别是,第一个例子中,我们在声明变量时使用的是Optional类型,而不是Car类型,这句声明非常清楚地表明了这里发生变量缺失是允许的。与此相反,使用Car这样的类型,可能将变量赋值为null,你只能依赖你对业务模型的理解,判断一个null是否属于该变量的有效值又或是异常情况。

代码解读

复制代码

public class User {

    private String userName;

    private Optional<Car> car;

    public String getUserName() {
        return userName;
    }
    public Optional<Car> getCar() {
        return car;
    }
}
public class Car {
    private String carName;
    private Optional<Insurance> insurance;
    public String getCarName() {
        return carName;
    }
    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}
public class Insurance {
    private String insuranceName;
    public String getInsuranceName() {
        return insuranceName;
    }
}

发现Optional是如何 富你模型的语义了吧。代码中user引用的是Optional, 而car引用的是Optional,这种方式非常清晰地表达了你的模型中一个user 可能有也可能没有car的情形,同样,car可能进行了保险,也可能没有保险。

与此同时,我们看到insurance的名称insuranceName被声明成String类型,而不是Optional,这非常清楚地表明声明为insurance的类中的名称字段insuranceName是必须存在的。

使用这种方式, 一旦通过引用insurance获取insuranceName时发生NullPointerException,你就能非常确定地知道出错的原因,不再需要为其添加null的检查查,因为null的检查查只会掩盖问题,并未真正地修复问题。

insurance必须有个名字,所以,如果你遇到一个insurance没有名称,你需要调查你的数据出了什么问题,而不应该再添加一段代码,将这个问题隐藏。

Optional的方法介绍

创建Optional

of(T value)

如果构造参数是一个null,这段代码会立即 出一个NullPointerException,而不是等到你  图访问car的属性值时才返回一个错误。

代码解读

复制代码

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

ofNullable(T value)

创建一个允许null值的Optional对象

代码解读

复制代码

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

empty()

创建一个空的Optional对象

代码解读

复制代码

    public static<T> Optional<T> empty() {
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }

常用方法

  • get()是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常。所以,除非你非常确定Optional变量一定包含值,否则最好不要使用这个方法。
  • orElse(T other),它允许你在 Optional对象不包含值时提供一个默认值。
  • orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier方法只有在Optional对象不含值时才执行调用。
  • orElseThrow(Supplier<? extends X>    exceptionSupplier)和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型。
  • ifPresent(Consumer<? super T>)让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作。

注意:orElse(T other)和orElseGet(Supplier<? extends T> other)的区别

这两个函数的区别:当value值不为null时,orElse函数依然会执行返回T的方法,而orElseGet函数并不会执行返回T的方法。

用map从Optional中提取和转换值

map(Function<? super T, ? extends U> mapper)

可以把Optional对象看成一种特殊的集合数据,它至多包含一个元素。如果Optional包含一个值,那函数就将该值作为参数传递给map,对该值进行转换。如果Optional为空,就什么也不做。

reasonml

代码解读

复制代码

String optionMap = Optional.ofNullable("abc").map(value -> value.toUpperCase()).get();

使用flatMap链接Optional对象

flatMap(Function<? super T, Optional> mapper)

将两层的optional合并为一个

reasonml

代码解读

复制代码

String optionFlatMap = Optional.ofNullable("abc").flatMap(value -> Optional.of((value + "flat-map").toUpperCase())).get();

用filter剔除特定的值

filter(Predicate<? super T> predicate)

filter方法接受一个谓词作为参数。如果Optional对象的值存在,并且它符合谓词的条件, filter方法就返回其值;否则它就返回一个空的Optional对象。

vbnet

代码解读

复制代码

Optional<String> filterOptional = Optional.ofNullable("abc").filter(value -> Objects.equals(value, "abc"));

实战

尝试获取用户的用户名称,不存在则返回默认值

reasonml

代码解读

复制代码

String userName = Optional.ofNullable(userOfNull).orElse(new User()).getUserName();

尝试获取用户的carName,不存在则返回null

reasonml

代码解读

复制代码

String carName = Optional.ofNullable(userOfNull).map(u -> u.getCar()).map(c -> c.getCarName()).orElse(null);

用户名存在的时候转为大写

reasonml

代码解读

复制代码

Optional.ofNullable(user).map(u -> u.getUserName()).ifPresent(userName -> System.out.println(userName.toUpperCase()));

过滤出来用户名称是张三的用户

reasonml

代码解读

复制代码

Optional.ofNullable(user).filter(u -> Objects.equals(u.getUserName(),"张三")).map(u -> u.getUserName()).ifPresent(userName -> System.out.println(userName + "实战Test"));

将张三的用户名称更改为李四

代码解读

复制代码

Optional.ofNullable(user).ifPresent(x -> {
            if (Objects.equals(user.getUserName(),"张三")){
                user.setUserName("李四");
            }
        });

Optional.ofNullable(user).filter(u -> Objects.equals(user.getUserName(),"张三")).ifPresent(x -> user.setUserName("李四"));


转载来源:https://juejin.cn/post/6844903958142517255

相关文章
|
9月前
|
SQL 数据挖掘 数据库
第三篇:高级 SQL 查询与多表操作
本文深入讲解高级SQL查询技巧,涵盖多表JOIN操作、聚合函数、分组查询、子查询及视图索引等内容。适合已掌握基础SQL的学习者,通过实例解析INNER/LEFT/RIGHT/FULL JOIN用法,以及COUNT/SUM/AVG等聚合函数的应用。同时探讨复杂WHERE条件、子查询嵌套,并介绍视图简化查询与索引优化性能的方法。最后提供实践建议与学习资源,助你提升SQL技能以应对实际数据处理需求。
684 1
|
10月前
|
消息中间件 Java 开发者
Java中实现异步通知的重试机制
在分布式系统中,异步通知是服务间数据交换的重要方式。然而,网络波动或服务不稳定可能导致通知失败,因此设计一个可靠的重试机制至关重要。本文通过Java教程,基于Spring框架和RabbitMQ,详细讲解异步通知重试机制的实现。内容涵盖重试策略(如最大尝试次数、退避策略等)、环境搭建及代码示例,帮助开发者构建健壮的分布式系统。
326 9
|
10月前
|
SQL 存储 关系型数据库
MySQL选错索引了怎么办?
本文探讨了MySQL中因索引选择不当导致查询性能下降的问题。通过创建包含10万行数据的表并插入数据,分析了一条简单SQL语句在不同场景下的执行情况。实验表明,当数据频繁更新时,MySQL可能因统计信息不准确而选错索引,导致全表扫描。文章深入解析了优化器判断扫描行数的机制,指出基数统计误差是主要原因,并提供了通过`analyze table`重新统计索引信息的解决方法。
284 3
|
10月前
|
SQL 分布式数据库 Apache
网易游戏 x Apache Doris:湖仓一体架构演进之路
网易游戏 Apache Doris 集群超 20 个 ,总节点数百个,已对接内部 200+ 项目,日均查询量超过 1500 万,总存储数据量 PB 级别。
913 3
网易游戏 x Apache Doris:湖仓一体架构演进之路
|
12月前
|
IDE Java 应用服务中间件
spring boot 启动流程
Spring Boot 启动流程简介: 在使用 Spring Boot 之前,启动 Java Web 应用需要配置 Web 容器(如 Tomcat),并将应用打包放入容器目录。而使用 Spring Boot,只需运行 main() 方法即可启动 Web 应用。Spring Boot 的核心启动方法是 SpringApplication.run(),它负责初始化和启动应用上下文。 主要步骤包括: 1. **应用启动计时**:使用 StopWatch 记录启动时间。 2. **打印 Banner**:显示 Spring Boot 的 LOGO。 3. **创建上下文实例**:通过反射创建
704 5
|
10月前
|
Java Spring 容器
两种Spring Boot 项目启动自动执行方法的实现方式
在Spring Boot项目启动后执行特定代码的实际应用场景中,可通过实现`ApplicationRunner`或`CommandLineRunner`接口完成初始化操作,如系统常量或配置加载。两者均支持通过`@Order`注解控制执行顺序,值越小优先级越高。区别在于参数接收方式:`CommandLineRunner`使用字符串数组,而`ApplicationRunner`采用`ApplicationArguments`对象。注意,`@Order`仅影响Bean执行顺序,不影响加载顺序。
791 2
|
11月前
|
监控 Java API
JDK动态代理和CGLIB动态代理
Java动态代理允许在运行时创建代理对象,增强或拦截目标类方法的执行。主要通过两种方式实现:JDK动态代理和CGLIB动态代理。JDK动态代理基于接口,利用`java.lang.reflect.Proxy`类和`InvocationHandler`接口;CGLIB则通过字节码技术生成目标类的子类作为代理,适用于未实现接口的类。两者均用于在方法执行前后添加额外逻辑,如日志记录、权限控制等,广泛应用于AOP框架中。
399 2
|
12月前
|
网络协议 算法 安全
Go语言的网络编程与TCP_UDP
Go语言由Google开发,旨在简单、高效和可扩展。本文深入探讨Go语言的网络编程,涵盖TCP/UDP的基本概念、核心算法(如滑动窗口、流量控制等)、最佳实践及应用场景。通过代码示例展示了TCP和UDP的实现,并讨论了其在HTTP、DNS等协议中的应用。最后,总结了Go语言网络编程的未来发展趋势与挑战,推荐了相关工具和资源。
370 5
|
11月前
|
人工智能 Java 数据库连接
MyBatis Plus 使用 Service 接口进行增删改查
本文介绍了基于 MyBatis-Plus 的数据库操作流程,包括配置、实体类、Service 层及 Mapper 层的创建。通过在 `application.yml` 中配置 SQL 日志打印,确保调试便利。示例中新建了 `UserTableEntity` 实体类映射 `sys_user` 表,并构建了 `UserService` 和 `UserServiceImpl` 处理业务逻辑,同时定义了 `UserTableMapper` 进行数据交互。测试部分展示了查询、插入、删除和更新的操作方法及输出结果,帮助开发者快速上手 MyBatis-Plus 数据持久化框架。
838 0
|
SQL 缓存 Java
【吐血整理】MyBatis从入门到精通
本文介绍了 MyBatis 的使用指南,涵盖开发环境搭建、基础操作实例和进阶特性。首先,详细描述了 JDK 和 IDE 的安装及依赖引入,确保项目顺利运行。接着,通过创建用户表和实体类,演示了 CRUD 操作的全流程,包括查询、插入、更新和删除。最后,深入探讨了动态 SQL 和缓存机制等高级功能,帮助开发者提升数据库交互效率和代码灵活性。掌握这些内容,能显著提高 Java 编程中的数据库操作能力。
1619 4