跳至主要內容

Optional

Jin...大约 17 分钟

Optional

随着时间的推移,曾经我们觉得重要的东西,可能在今天看来是如此的浅薄和无知,同理,今天我们放不下,想不开,觉得重要的东西,多年后我们可能也会觉得也就那样,所以,今天的的所有烦恼,忧愁,想不开,其实我们都没必要过于在意,因为有些东西随着时间的冲刷,也就那样了。

​ ——Jin

1、前言

ItBackendJava8提供了Optional接口,Optional接口能够是我们的代码变得更加的优雅,可读性更高,同时能够很好的避免空指针,因为空指针是一个很让人头疼的问题,特别对于调用第三方接口,如果不知道对象的规约的时候,我们在取值的时候无法直到那些值能为空,那些不能为空,所以容易出现空指针,如果我们谨慎一点,可能会对每一个值进行判空处理,但是将会充斥着大量的if语句,甚是不雅观。

2、API

2.1、empty()

返回一个空的Optional对象 Optional.empty

Optional<Object> empty = Optional.empty();

2.2、of(T value)

参数传入一个对象,返回一个Option对象,value不能为空,如果为null,将抛出空指针异常

    /**
     * 演示Optional.of方法如果传null会抛异常
     */
    @Test
    public void testCreateOptional2() {
        // 使用Optional.of方法时,需要确保入参不为null,否则会空指针
        System.out.println("-----下面会有空指针----");
        try {
            System.out.println(Optional.of(null));
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("------上面会有空指针------");
    }

打印结果:

image-20220722141008314
image-20220722141008314

2.3、ofNullable(T value)

​ 参数传入一个对象,可以为空,如果为空,将返回一个空的Optional对象,就等于Optional.empty(),输出的值为Optional.empty,如果不为空,返回一个不为空的Optional对象。

    @Test
    public void testCreateOptional() {
        // 使用Optional.of构造出具体对象的封装Optional对象
        System.out.println(Optional.of(new Content("111", "Jin")));
        // 使用Optional.empty构造一个不代表任何对象的空Optional值
        System.out.println(Optional.empty());
        System.out.println(Optional.ofNullable(null));
        System.out.println(Optional.ofNullable(new Content("222", "Jin")));
    }

打印结果:

image-20220722141650924
image-20220722141650924

2.4、get()

获取Optional中的值,这个值也就是我们的值,Optional相当于就是一个外壳。

    @Test
    public void testCallOptional() {
        Optional<Content> optional = getContent();
        System.out.println("-------下面代码会报异常--------");
        try {
            // 【错误用法】直接从Optional对象中get()实际参数,这种效果与返回null对象然后直接调用是一样的效果
            Content content = optional.get();
            System.out.println(content);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("-------上面代码会报异常--------");
    }

    private Optional<Content> getContent() {
        return Optional.ofNullable(null);
    }

打印结果:

image-20220722141830705
image-20220722141830705

2.5、isPresent()

判断Optional对象中是否有值,如果有值,返回true,没值返回false。

    @Test
    public void testIsPresent() {
        User user = new User();
        Optional<User> optionalUser = Optional.ofNullable(user);
        System.out.println(optionalUser.isPresent());//true

        user = null;
        Optional<User> optionalUser1 = Optional.ofNullable(user);
        System.out.println(optionalUser1.isPresent());//false
    }

2.6、ifPresent(Consumer<? super T> consumer)

ifPresent参数是一个函数式接口,无返回值,会将Optional中的值作为参数传递到ifPresent()中

@Test
    public void testIfPresent() {
        User user = new User();
        Optional<User> optional = Optional.ofNullable(user);
        optional.ifPresent(s -> System.out.println(s));
    }
 //打印结果:User(id=null, userName=null)

2.7、filter(Predicate<? super T> predicate)

​ 是一个Predicate函数接口,会将Optional中的值作为参数传入,如果符合规则,那么返回一个Optional对象,否则返回一个空的Optional 对象(Optional.empty)

    @Test
    public void testFilter() {
        Optional<String> stringOptional = Optional.of("zhangsan");
        System.out.println(stringOptional.filter(e -> e.length() > 5).orElse("王五"));//zhangsan
        stringOptional = Optional.empty();
        System.out.println(stringOptional.filter(e -> e.length() > 5).orElse("lisi"));//lisi
    }

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

​ 参数是一个Function函数式接口,会将Optional中的值作为参数传递到map中,如果传入的值为空,则返回一空的Optional对象,相当于Optional.empty(), 如果不为空,我们可以返回一个可以描述描述结果的返回值(Optional中的值,这个值可以重新赋值)

    @Test
    public void testMap() {
        User user = new User();
        user.setId("1");
        user.setUserName("Jin");
        Optional<String> optional = Optional.ofNullable(user).map(OptionalService::getUserMap);
        System.out.println(optional);//Optional[Jin]

        user = null;
        Optional<String> optional1 = Optional.ofNullable(user).map(OptionalService::getUserMap);
        System.out.println(optional1);//Optional.empty
    }

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

​ 如果Optional中值存在,那么返回一个基于Optional的值(如Optional),如果Optional中的值不存在,则返回一空的Optional对象,相当于Optional.empty(),与map不同, map返回的是一个值,而flatMap返回一个基于Optional的值

@Test
    public void testMapAndFlatMap() {
        Optional<User> userOptional = getUser();
        Optional<Employee> employeeOptional = userOptional.map(user -> {
            Employee employee = new Employee();
            employee.setEmployeeName(user.getUserName());
            // map与flatMap的区别点:此处return的是具体对象类型
            return employee;
        });
        System.out.println(employeeOptional);

        Optional<Employee> employeeOptional2 = userOptional.flatMap(user -> {
            Employee employee = new Employee();
            employee.setEmployeeName(user.getUserName());
            // map与flatMap的区别点:此处return的是具体对象的Optional封装类型
            return Optional.of(employee);
        });
        System.out.println(employeeOptional2);
    }

    private Optional<User> getUser() {
        User user = new User();
        user.setId("111");
        user.setUserName("Jin");
        return Optional.of(user);
    }

2.10、orElse(T other)

如果Optional中的值不为空,则返回Optional中的值,如果为空,则返回other值,

Optional<String> stringOptional = Optional.of("张三");
System.out.println(stringOptional.orElse("zhangsan"));//打印结果:张三
 
Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.orElse("李四"));//打印结果:李四

​ 一般与of或者ofNullable配合使用,但需要注意的是,orElse不管前面的of或者ofNullable是否满足条件,都会进行执行,如果要走类似 if..else..的逻辑,需要使用orElseGet来替代orElse。

@Test
    public void testOrElse() {
        User user = new User();
        user.setId("1");
        user.setUserName("Jin");
        user = Optional.ofNullable(user).orElse(addUser());//User(id=1, userName=Jin)
        System.out.println(user);

        User user2 = null;
        user2 = Optional.ofNullable(user2).orElse(addUser());
        System.out.println(user2);//User(id=2, userName=JinX)
    }

    private static User addUser() {
        User user = new User();
        user.setId("2");
        user.setUserName("JinX");
        return user;
    }

如果创建的Optional中有值存在,则返回此值,否则返回一个默认值

image-20220722150724004
image-20220722150724004

2.11、orElseGet(Supplier<? extends T> other)

如果Optional中存在值,则返回值,否则返回other调用的结果

Optional中存在值

    @Test
    public void testOrElseGet() {
        User user = new User();
        user.setId("1");
        user.setUserName("Jin");
        user = Optional.ofNullable(user).orElse(addUser());//User(id=1, userName=Jin)
        System.out.println(user);

        User user2 = null;
        user2 = Optional.ofNullable(user2).orElseGet(() -> addUser());
        System.out.println(user2);//User(id=2, userName=JinX)
    }

    private static User addUser() {
        User user = new User();
        user.setId("2");
        user.setUserName("JinX");
        return user;
    }

2.12、orElseThrow(Supplier<? extends X> exceptionSupplier)

如果Optional中的值存在,则返回值,值不存在,则抛出异常函数Supplier中的异常

    @Test
    public void testOrElseThrow() {
        String value = null;
        String orElse = Optional.ofNullable(value).orElseThrow(() -> new RuntimeException("不存在值"));
        System.out.println(orElse);//java.lang.RuntimeException: 不存在值
    }

3、优雅地和NullPointException说再见

NullPointException应该算是每一个码农都很熟悉的家伙了吧?谁的代码不曾抛过几个空指针异常呢...

​ 运行程序,你可能就等不到你需要的结果,而是要喜提NullPointException了...

​ 作为JAVA开发中最典型的异常类型,甚至可能是很多程序员入行之后收到的第一份异常大礼包类型,NullPointException也似乎成为了一种魔咒,迫使程序员在敲出的每一行代码的时候都需要去思考下是否需要去做一下判空操作,久而久之,代码中便充斥着大量的null检查逻辑。

于是呢,上面的代码会变成下面这样:

public void getCompanyFromEmployee() {
    Employee employee = getEmployee();
    if (employee == null) {
        // do something here...
        return;
    }
    Team team = employee.getTeam();
    if (team == null) {
        // do something here...
        return;
    }
    Department department = team.getDepartment();
    if (department == null) {
        // do something here...
        return;
    }
    Company company = department.getCompany();
    System.out.println(company);
}

​ 是不是大家的项目中都有见过这种写法的?每行代码中都流露着对NullPointException的恐惧有木有?是不是像极了一颗被深深伤害过的心在小心翼翼的保护着自己?

image-20220722154236548
image-20220722154236548

3.1、null的困扰

通过上面代码示例,我们可以发现使用null可能会带来的一系列困扰:

  • 空指针异常,导致代码运行时变得不可靠,稍不留神可能就崩了
  • 使代码膨胀,导致代码中充斥大量的null检查与保护,使代码可读性降低
image-20220722154320401
image-20220722154320401

此外,null还有一个明显的弊端:

  • 含义不明确,比如一个方法返回了null,调用方不清楚到底是因为逻辑有问题导致为null,还是说null其实也是一种可以接受的正常返回值类型?

所以说,一个比较好的编码习惯,是尽量避免在程序中使用null,可以按照具体的场景分开区别对待:

image-20220722154353091
image-20220722154353091
  • 确定是因为代码或者逻辑层面处理错误导致的无值,通过throw异常的方式,强制调用方感知并进行处理对待
  • 如果null代表业务上的一种正常可选值,可以考虑返回Optional来替代。

​ 当然咯,有时候即使我们自己的代码不返回null,也难免会遇到调用别人的接口返回null的情况,这种时候我们真的就只能不停的去判空来保护自己吗?有没有更优雅的应对策略来避免自己掉坑呢?下面呢,我们一起探讨下null的一些优雅应对策略。

3.2、Optional应对null处理

3.2.1、Optional一定比return null安全吗

前面我们提到了说使用Optional来替代null,减少调用端的判空操作压力,防止调用端出现空指针异常。

那么,使用返回Optional对象就一定会比return null更靠谱吗?

答案是:也不一定,关键要看怎么用!

比如:下面的代码,getContent()方法返回了个Optional对象,然后testCallOptional()方法作为调用方,获取到返回值后的操作方式:

public void testCallOptional() {
    Optional<Content> optional = getContent();
    System.out.println("-------下面代码会报异常--------");
    try {
        // 【错误用法】直接从Optional对象中get()实际参数,这种效果与返回null对象然后直接调用是一样的效果
        Content content = optional.get();
        System.out.println(content);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println("-------上面代码会报异常--------");
}

private Optional<Content> getContent() {
    return Optional.ofNullable(null);
}

上述代码运行之后会发现报错了:

-------下面代码会报异常--------
java.util.NoSuchElementException: No value present
	at java.util.Optional.get(Optional.java:135)
	at com.veezean.skills.optional.OptionalService.testCallOptional(OptionalService.java:47)
	at com.veezean.skills.optional.OptionalService.main(OptionalService.java:58)
-------上面代码会报异常--------

既然直接调用Optional.get()报错,那就是调用前加个判断就好咯?

public void testCallOptional2() {
    Optional<Content> optional = getContent();
    // 使用前先判断下元素是否存在
    if (optional.isPresent()) {
        Content content = optional.get();
        System.out.println(content);
    }
}

执行一下,果然不报错了。但是,这样真的就是解决方法吗?这样跟直接返回null然后使用前判空(下面的写法)其实也没啥区别,也并不会让调用方使用起来更加的优雅与靠谱:

public void testNullReturn2() {
    Content content = getContent2();
    if (content != null) {
        System.out.println(content.getValue());
    }
}

3.3、全面认识下Optional

3.3.1、创建Optional对象

Optional<T>对象,可以用来表示一个T类型对象的封装,或者也可以表示不是任何对象。Optional类提供了几个静态方法供对象的构建:

方法名功能含义描述
empty()构造一个无任何实际对象值的空Optional对象(可以理解为业务层面的null
of(T t)根据给定的对象,构造一个此对象的封装Optional对象,注意入参t不能为null,否则会空指针
ofNullable(T t)根据传入的入参t的值构造Optional封装对象,如果传入的t为null,则等同于调用empty()方法,如果t不为null,则等同于调用of(T t)方法

在项目中,我们可以选择使用上面的方法,实现Optional对象的封装:

    /**
     * 演示创建Optional的方法
     * <p>of<p/>
     * <p>empty<p/>
     * <p>ofNullable<p/>
     */
    @Test
    public void testCreateOptional() {
        // 使用Optional.of构造出具体对象的封装Optional对象
        System.out.println(Optional.of(new Content("111", "Jin")));
        // 使用Optional.empty构造一个不代表任何对象的空Optional值
        System.out.println(Optional.empty());
        System.out.println(Optional.ofNullable(null));
        System.out.println(Optional.ofNullable(new Content("222", "Jin")));
    }

输出结果:

Optional[Content{id='111', value='Jin'}]
Optional.empty
Optional.empty
Optional[Content{id='222', value='Jin'}]

​ 这里需要注意下of方法如果传入null会抛空指针异常,所以比较建议大家使用ofNullable方法,可以省去调用前的额外判空操作,也可以避免无意中触发空指针问题:

底层代码:

    /**
     * Returns an {@code Optional} describing the specified value, if non-null,
     * otherwise returns an empty {@code Optional}.
     *
     * @param <T> the class of the value
     * @param value the possibly-null value to describe
     * @return an {@code Optional} with a present value if the specified value
     * is non-null, otherwise an empty {@code Optional}
     */
    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

3.3.2、Optional常用方法理解

在具体讨论应该如何正确使用Optional的方法前,先来了解下Optional提供的一些方法:

方法名含义说明
isPresent如果Optional实际有具体对象值,则返回true,否则返回false。
ifPresent这是一个函数式编程风格的API接口,入参是一个函数,即如果Optional对象有实际对象值,则会执行传入的入参函数逻辑,如果不存在实际对象值,则不会执行传入的入参函数逻辑。
get返回Optional封装的实际对象T数据,注意,如果实际对象数据不存在,会抛异常而非返回null
orElseget方法类似,都是获取Optional实际的对象值,区别在于orElse必须传入一个默认值,当Optional没有实际值的时候返回默认值而非抛异常
orElseGet可以理解为orElse方法的升级版,区别在于orElse仅允许传入一个固定的默认值,而orElseGet的入参是一个函数方法,当Optional无实际值时,会执行给定的入参函数,返回动态值
orElseThroworElse类似,区别在于如果没有获取到,会抛出一个指定的异常
filter判定当前Optional的实际对象是否符合入参函数的过滤规则,如果符合则返回当前Optional对象,如果不符合则返回空Optional
map接收一个入参函数,允许将Optional中的实际对象值处理转换为另一实际对象值(这个入参函数的返回值为T),并生成返回此新类型的Optional对象,如果生成的新对象为null,则返回一个空Optional对象
flatMapmap类似,区别点在于入参函数的返回值类型有区别(此处入参函数的返回值为Optional<T>

​ 看到这里的mapflatMap方法,不知道大家会不会联想到Stream流对象操作的时候也有这两个方法的身影呢,的确,它们的作用也是类似的,都是用来将一个对象处理转换为另一个对象类型的:

对于Optional而言,mapflatMap最终的实现效果其实都是一样的,仅仅只是入参的要求不一样,也即两种不同写法,两者区别点可以通过下图来理解:

image-20220722163334904
image-20220722163334904

实际使用的时候,可以根据需要选择使用map或者flatMap

    @Test
    public void testMapAndFlatMap() {
        Optional<User> userOptional = getUser();
        Optional<Employee> employeeOptional = userOptional.map(user -> {
            Employee employee = new Employee();
            employee.setEmployeeName(user.getUserName());
            // map与flatMap的区别点:此处return的是具体对象类型
            return employee;
        });
        System.out.println(employeeOptional);//Optional[Employee(employeeName=Jin, team=null)]

        Optional<Employee> employeeOptional2 = userOptional.flatMap(user -> {
            Employee employee = new Employee();
            employee.setEmployeeName(user.getUserName());
            // map与flatMap的区别点:此处return的是具体对象的Optional封装类型
            return Optional.of(employee);
        });
        System.out.println(employeeOptional2);//Optional[Employee(employeeName=Jin, team=null)]
    }

3.4、Optional使用场景

3.4.1、减少繁琐的判空操作

再回到本篇文章最开始的那段代码例子,如果我们代码里面不去逐个做判空保护的话,我们可以如何来实现呢?看下面的实现思路:

    public void getCompanyFromEmployeeTest() {
        Employee employeeDetail = getEmployee();
        String companyName = Optional.ofNullable(employeeDetail)
                .map(employee -> employee.getTeam())
                .map(team -> team.getDepartment())
                .map(department -> department.getCompany())
                .map(company -> company.getCompanyName())
                .orElse("No Company");
        System.out.println(companyName);//No Company
    }
    private Employee getEmployee() {
        Employee employee = new Employee();
        employee.setEmployeeName("Jin");
        employee.setTeam(new Team("DevTeam"));
        return employee;
    }

​ 先通过map的方式一层一层的去进行类型转换,最后使用orElse去获取Optional中最终处理后的值,并给定了数据缺失场景的默认值。是不是看着比一堆if判空操作要舒服多了?

📢 适用场景: 需要通过某个比较长的调用链路一层一层去调用获取某个值的时候,使用上述方法,可以避免空指针以及减少冗长的判断逻辑。

转载请注明出处。

3.4.2、需要有值兜底的数据获取场景

​ 编码的时候,经常会遇到一些数据获取的场景,需要先通过一些处理逻辑尝试获取一个数据,如果没有获取到需要的数据,还需要返回一个默认值,或者是执行另一处理逻辑继续尝试获取

比如从请求头中获取客户端IP的逻辑,按照常规逻辑,代码会写成下面这样:

public String getClientIp(HttpServletRequest request) {
    String clientIp = request.getHeader("X-Forwarded-For");
    if (!StringUtils.isEmpty(clientIp)) {
        return clientIp;
    }
    clientIp = request.getHeader("X-Real-IP");
    return clientIp;
}

但是借助Optional来实现,可以这样写:

public String getClientIp2(HttpServletRequest request) {
    String clientIp = request.getHeader("X-Forwarded-For");
    return Optional.ofNullable(clientIp).orElseGet(() -> request.getHeader("X-Real-IP"));
}

📢 适用场景: 优先执行某个操作尝试获取数据,如果没获取到则去执行另一逻辑获取,或者返回默认值的场景。

3.4.3、替代可能为null的方法返回值

下面是一段从项目代码中截取的片段:

public FileInfo queryOssFileInfo(String fileId) {
    FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
    if (entity != null) {
        return new FileInfo(entity.getName(), entity.getFilePath(), false);
    }
    FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
    if (hisEntity != null) {
        return new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true);
    }
    return null;
}

​ 可以看到最终的return分支中,有一种可能会返回null,这个方法作为项目中被高频调用的一个方法,意味着所有的调用端都必须要做判空保护。可以使用Optional进行优化处理:

public Optional<FileInfo> queryOssFileInfo(String fileId) {
    FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
    if (entity != null) {
        return Optional.ofNullable(new FileInfo(entity.getName(), entity.getFilePath(), false));
    }
    FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
    if (hisEntity != null) {
        return Optional.ofNullable(new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true));
    }
    return Optional.empty();
}
复制代码

这样的话,就可以有效的防止调用端踩雷啦~

📢 适用场景: 实现某个方法的时候,如果方法的返回值可能会为null,则考虑将方法的返回值改为Optional类型,原先返回null的场景,使用Optional.empty()替代。

3.4.4、包装数据实体中非必须字段

首先明确一下,Optional的意思是可选的,也即用于标识下某个属性可有可无的特性。啥叫可有可无?看下面代码:

public class PostDetail {
    private String title;
    private User postUser;
    private String content;
    private Optional<Date> lastModifyTime = Optional.empty();
    private Optional<Attachment> attachment = Optional.empty();
}

​ 上面是一个帖子详情数据类,对于一个论坛帖子数据而言,帖子的标题、内容、发帖人这些都是属于必须的字段,而帖子的修改时间、帖子的附件其实是属于可选字段(因为不是所有的帖子都会被修改、也不是所有帖子都会带附件),所以针对这种可有可无的字段,就可以声明定义的时候使用Optional进行封装。

image-20220722165508393
image-20220722165508393

使用Optional进行封装之后有两个明显的优势:

  • 强烈的业务属性说明,明确的让人知晓这个是一个可选字段,等同于数据库建表语句里面设置nullable标识一样的效果;
  • 调用端使用的时候也省去了判空操作。

📢 适用场景: 数据实体定义的时候,对于可选参数,采用Optional封装类型替代。

3.5、使用抛异常替代return null

​ 相比于返回一个Optional封装的对象,直接抛异常具有强烈的警醒意味,意在表达此处存在预期之外的不合理情况,要求编码的时候,调用端必须要予以专门处理

public Team getTeamInfo() throws TestException {
    Employee employee = getEmployee();
    Team team = employee.getTeam();
    if (team == null) {
        throw new TestException("team is missing");
    }
    return team;
}

相比直接return null,显然抛异常的含义更加明确。

3.6、JDK与开源框架的实践

JDK提供的很多方法里面,其实都是遵循着本文中描述的这种返回值处理思路的,很少会看到直接返回null的——不止JDK,很多大型的开源框架源码中,也很少会看到直接return null的情况。

​ 比如Spring中org.springframework.data.jpa.repository.support包下面的方法例子:

public Optional<T> findById(ID id) {
	Assert.notNull(id, ID_MUST_NOT_BE_NULL);
	Class<T> domainType = getDomainClass();
	if (metadata == null) {
		return Optional.ofNullable(em.find(domainType, id));
	}
	LockModeType type = metadata.getLockModeType();
	Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();
	return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}
贡献者: Jin
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度