Java8实战之(九)用Optional取代null

本章内容

  • null 引用引发的问题,以及为什么要用Null 引用
  • 从null 到Optional,以null安全的方式重写你的域模型
  • 不要使用if else处理NullPointerException
  • 读取Optional中的几种可能的方法

作为Java 程序员曾经遭受过NullPointerException,来看看下面例子

package com.learn.java8.optional.model;

import lombok.Data;

/**
 * @Description:
 * @Author: Zlx
 * @Date: 2021/6/26 11:54 下午
 */
@Data
public class Person {
	private Car car;

	@Data
	static class Car {
		private Insurance insurance;

		@Data
		static class Insurance {
			private String name;
		}
	}
	/**
	 *
	 */
	public String getCarInsuranceName(Person person) {
		return person.getCar().getInsurance().getName();
	}
}

getCarInsuranceName 这个方法看起来没有问题,但是现实中很多人没有车,所以getInsurance的时候出现NullPointerException。
对此在需要的地方添加null校验(同时过于激进的防御式检查甚至会在不太需要的地方添加代码)

	public String getCarInsuranceNameTestNull(Person person) {
		if (person != null) {
			Car car = person.getCar();
			if (car != null) {
				Car.Insurance insurance = car.getInsurance();
				if (insurance != null) {
					return insurance.getName();
				}
			}
		}
		return "Unknown";
	}

这种方式每次引用一个变量都要进行null校验,如果引用链上的某个值为null它就返回一个“Unknown”,但是为什么最后一个insurance的name不进行校验呢?为什么insurance的name为null,可以直接返回?因为insurance必定有一个name,这是跟业务相关的,所以避免了这个检查,那么能不能把对象引用的检查也跟业务绑定一起呢?

我们来看看第二种解决办法

public String getCarInsuranceNameByReturn(Person person) {
		if (person == null) {
			return "Unknown";
		}
		Car car = person.getCar();
		if (car == null) {
			return "Unknown";
		}
		Car.Insurance insurance = car.getInsurance();
		if (insurance == null) {
			return "Unknown";
		}
		return insurance.getName();
	}

这种方式避免深层次递归的if语句块,每次遭遇到null你都返回“Unknown”,以至于Unknown你出现了三次,所以这种写法也不是最佳的方案,下面教你优雅的java8写法,对缺失的变量值进行建模

Optional类入门

Java8 中引入了一个新的类java.util.Optional,这是一个封装的类,举例来说,使用新的类意味着当一个人没有车时,Person内部的car就不应该声明成Car, 而是应该Optional
在这里插入图片描述
如果变量存在,Optional就是对car的简单封装,如果变量不存在,那就是一个空的Optional,返回的是Optional.empty(),我们来看看Optional.empty()是怎么处理的

    private static final Optional<?> EMPTY = new Optional<>();

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

由此看见,对optional的使用不会出现null引用问题的,因为返回就是一个空的Optional,借助Optional我们可以设计建模,表达缺失的变量值是允许的。
重新定义Person 的变量值

package com.learn.java8.optional.model;

import java.util.Optional;

/**
 * @Description:
 * @Author: Zlx
 * @Date: 2021/7/4 10:06 下午
 */
public class PersonOptional {
	private Optional<Car> car;
	public Optional<Car> getCar() { return car; }
	static class Car {
		private Optional<Insurance> insurance;
		public Optional<Insurance> getInsurance() { return insurance; }
	}
	static class Insurance {
		private String name;
		public String getName() { return name; }
	}
}

通过上面的建模,可以给其他程序员很快的知道了person的car是允许不存在的,同样car可能会有insurance,也可能没有。但是insurance肯定有

应用 Optional 的ࠖ几种模式

现在你知道怎么建设你的域模型,也了解这种方式与直接使用null引用表示变量值的缺失的优劣,下面我们就看看Optional 到底怎么用

创建Optional对象

  1. 声明一个空的Optional
    前面已经看过源码,创建一个空的得很简单,直接
    Optional<Car> optCar = Optional.empty();
  1. 依据一个非空值创建一个Optional
    Optional<Car> optCar = Optional.of(car);
    如果car 是一个null则立马抛出一个NullPointerException,而不是等到你使用这个optCar
  1. 可接受null的Optional
    Optional<Car> optCar = Optional.ofNullable(car);
    如果car是一个Null 那么你得到的将会是Optional.empty()的一个空的Optional

有了创建,还要有获取,Optional提供了get方法,但是这个get如果获取空的Optional 就会抛出异常,所以你必须确定有值才敢使用,不然你又掉进了if 维护null的噩梦!!,不用get,我们也有其他的方式获取

使用map 从Optional 对象中提取和转换值

String name = null; 
if(insurance != null){ 
 name = insurance.getName(); 
}

为了支持上面这种写法,Optional 提供了map方法 。它的工作方式如下

Optional<Insurance> optInsurance = Optional.ofNullable(insurance); 
Optional<String> name = optInsurance.map(Insurance::getName);

这个特别像之前学到了Stream,map操作应用于流的每一个元素,所以你可以把Optional 也看成是一种特使的集合,这个集合里边只有一个元素,如果有值就操作,没有值就什么都不做。
在这里插入图片描述

这个看起来非常有用,但是怎么重构如下代码呢?

public String getCarInsuranceName(Person person) { 
 return person.getCar().getInsurance().getName(); 
}

为了这个目的,我们不适用Map,而是适用flatMap,如果我们map操作,如下所示

Optional<Person> optPerson = Optional.of(person); 
Optional<String> name = 
 optPerson.map(Person::getCar) 
 .map(Car::getInsurance) 
 .map(Insurance::getName);
 // 但是这段代码确无法编译,异常就是getInsurance,
 // 因为你的getCar 返回的是一个这意味着map操作的结果是一个Optional<Optional<Car>>类型的对象,继续map操作的入参是Optional<Car>类型的对象 而Optional并没有getInsurance方法,所以是非法的

在这里插入图片描述
我们回顾一下flat 的方法,该函数的返回值是一个新的流,这个方法会应用到流中的每一个元素,就流的内容替换成每个新生成的流。换句话说就是
由该方法生成的各个流会被合并或者扁平化一个单一的流,这个跟你想要的结果是一样的。
在这里插入图片描述
在这里插入图片描述

1. 使用Optional 获取car的保险公司名字

	public String getCarInsuranceName(Optional<PersonOptional> person) {
		return person.flatMap(PersonOptional::getCar)
				.flatMap(Car::getInsurance)
				.map(Insurance::getName)
				.orElse("Unknown");
	}

在这里插入图片描述

2. 使用Optional解引用串接的Person/Car/Insurance对象
由Optional < Person >我们可以结合flatMap和map方法,从Person中解引用出Car,从Car中解引用Insurance,再从Insurance出解引用字符串,下图对这种流水线做出了说明
在这里插入图片描述

  1. 从Optional 封装的persion入手,对其调用flatmap(Persion::getCar()),某个Function作为参数被传递给Optional的persion对象,对其进行转换。这里Function具体变现是调用Persion::getCar(),返回的是Optional < Car >,结果流水线里就是两层的Optional 对象,最后flatmap将这两层的对象扁平化成一个新的流,还是Optional对象。
    注意::如果对象是空的,那么就返回一个空的Optional,对空的Optional 操作,不会有任何的改变,还是返回一个空的Optional。如果对象不是空的,经过Funciton的实现就可以继续转成Optional对象
  2. 与第一步大同小异,Optional< Car >转成 Optional< Insurance >
  3. 第三步,Optional< Insurance > 转成Optional< String >,因为Insurance::getName()就是返回了字符串,并且后续不需要继续流水线,那就直接使用map。
    在这里插入图片描述

3. 域模型代码中使用Optional不能被序列化
Optional在设计之初就没想过把它当成字段使用,所以它不能实现序列化的接口。但是所以你的域模型使用了要求序列化的库或者框架,所以建议你如下设计域模型。

public class Person { 
 private Car car; 
 public Optional<Car> getCarAsOptional() { 
 return Optional.ofNullable(car); 
 } 
}

默认行为以及解引用Optional对象

前面我们看到使用了orElse方法读取这个变量的值,使用这种方式,遭遇到空的optional就会返回这个默认值,此外,Optional还提供了其他读取值得方法

  1. get()
    是获取值最简单但是又是最不安全的办法,因为空的optional调用get()会抛出空指针异常,如果你非常确定你的optional对象不是空的,那么你尽管用,不然又将遭遇噩梦般的空指针校验
  1. orElse(T other)
    前面提到过,如果空的optional就会返回other当做默认值
  1. orElseGet(Supplier<? extends T> other)
    是orElse的延迟版,当遭遇到空的optional会执行Supplier方法,如果创建对象是耗时的工作,那么可以考虑这种方法,或者当你需要在这个对象为空时调用,也可以考虑,不过这种情况有严格的限制。
  1. orElseThrow(Supplier<? extends X> exceptionSupplier)
    和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以抛出自己定义的异常。
  1. ifPresent(Consumer<? super T>)
    让你能在变量值存在时执行一个作为参数传入的
    方法,否则就不进行任何操作。

Optional类和Stream接口的相似之处,远不止map和flatMap这两个方法。还有第三个方
法filter,它的行为在两种类型之间也极其相似~~

两个Optional对象的组合

现在假设有这么一个方法,返回最便宜的保险公司

public PersonOptional.Insurance getCharaestInsurance(Person person, Car car) {
		// 处理复杂的业务逻辑
		PersonOptional.Insurance cheapestCompany = new PersonOptional.Insurance();
		return cheapestCompany;
	}

现在我们要求提供一个安全的null方法,接收两个Optional对象作为参数,返回值是一个Optional< Insurance > 如果入参为空,那么返回值就是空的Optional

作为传统的Java 程序员,你可能会写出如下代码

public Optional<PersonOptional.Insurance> nullSafeFindCheapestInsurance(Optional<Person> person,
		Optional<Car> car) {
		if (person.isPresent() || car.isPresent()) {
			return Optional.empty();
		}
		return Optional.ofNullable(this.getCharaestInsurance(person.get(), car.get()));

	}

仔细这么一想是不是跟if else null 校验的思路很像
但是这么写,Optional 优点就完全没有被体现出来
来看看Optional 流是怎么写的

	public Optional<PersonOptional.Insurance> nullSafeFindCheapestInsurance(Optional<Car> car,
		Optional<Person> person) {
		return person.flatMap(optPerson -> car.map(optCar -> this.getCharaestInsurance(optPerson, optCar)));
	}
	

解释一下上述的代码

你对第一个person调用flatMap方法,如果是空的直接就返回空的,否则就car.map(c -> findCheapestInsurance(p, c)作为方法传入流中,注意这里如果car为空,也是直接返回空的对象,那么flatMap的返回也是空的,如果car不是空的就,就执行getCharaestInsurance方法,返回Optional<PersonOptional.Insurance> 。完成期望操作

使用filter 剔除特定的值

你可能会遇到这种常见的操作,判断某个对象的值是否为指定的,如果是则继续,不然就打断。
你可能会这么写


	public String getName(Car.Insurance insurance) {
		if(insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
			System.out.println("ok");
		}
		return null;
	}

我们可以使用类似Stream 的操作

	public void getName(Car.Insurance insurance, Integer type) {
		Optional<Car.Insurance> optInsurance = Optional.ofNullable(insurance);
		optInsurance.filter(optrInsurance ->
				"CambridgeInsurance".equals(optrInsurance.getName()))
				.ifPresent(x -> System.out.println("ok"));
	}

filter 接收一个谓词作为参数,如果Optional 的值存在,并且复合谓词的条件,filter 方法就返回其值,否则就将值过滤掉,返回空的optional

下面来测试假设Person有一个年龄的属性,你需要找出年龄大于某个值得Insurance

public String getCarInsuranceName(Optional<Person> person, int minAge)
public String getCarInsuranceName(Optional<Person> person, int minAge)
	public String getCarInsuranceName(Optional<PersonOptional> person, int minAge){
		return person.filter(personOptional -> personOptional.getAge() > minAge)
				.flatMap(personOptional -> personOptional.getCar())
				.flatMap(car -> car.getInsurance())
				.map(insurance -> insurance.getName())
				.orElse("Unkonw");
	}

总结Optional类方法

方法描述
flaMap如果值存在,就对该值执行提供的mapping 函数调用,返回一个optional类型的值,否则就返回一个空的optional对象
get如果该值存在,将该值经过Optional 封装返回,如果不存在,抛出空指针异常
ifPresent如果该值存在就调用使用该值得方法,否则什么都不做
isPresent如果该值存在就返回true
map如果该值存在就对该值进行mapping函数调用
of将指定值用Optional封装以后,如果该值是Null,则抛出空指针
ofNullable将指定值用Optional封装以后,如果该值是Null,返回一个空的optional对象
orElse如果有值就返回其值,不然就返回默认值
orElseGet如果有值就返回,不然就调用Supplier接口生成的值
orElseThrow如果有值就返回,不然就Supplier接口生成的异常

使用Optional的实战示例

实际上,如果你写的代码在创建之初,很多API设计编写就会有很大的不同。为了保持向后的兼容性,我们很难对老的Java API进行改动,让他们也使用Optional,但是这并不是不能改变,你可以整合一个工具类,让其他人的代码也享受Optional 带来的威力,下面我们看看几个例子

用Optional 封装可能为null的值

比如你要从map.get某个key,如果key对应的值为null,那你对这个值得操作就很有可能出现空指针异常

Object o = map.get("key")
o...

如果你学了optional 还是用if-else 处理,那么就很不应该了
所以你可以用这种方式

Optional.ofNullable(map.get("key"))

这种方式永远都是安全的(除非你不去调用get())

异常与Optional 的对比

由于某种原因,函数无法返回某个值,这时候除了返回null ,你写的API的传统做法就是抛出异常。比如典型使用Integer.paraseInt(String) 如果入参不是一个数字,那就无法转换,这时候你可以借用try-catch联合Optional封装工具类
代码如下

	public static Optional<Integer> stringToInt(String param) {
		try {
			return Optional.of(Integer.parseInt(param));
		} catch (Exception ex) {
			return Optional.empty();
		}
	}

基础类型的Optional 不建议使用
以前学Stream的时候就知道Stream也有基础类型,比如LongStream,IntStream,Optional 也提供了基础类型,比如OptionalInt,如果前面的Optional< Integer >可以返回一个OptionalInt,
但是基础类型的Optional 不建议使用,因为它丢失了我们经常使用的map,flatMap 方法

所有内容整合

我们来完成一个小需求,比如你需要从一个对象中读取某个属性,如果属性没有值就返回0,有的话还要判断是否是正整数,如果不是就返回0.

	public Integer readDuraion(Map<String, String> map, String name) {
		return Optional.ofNullable(map.get(name))
				.flatMap(Person::stringToInt)
				.filter(integer -> integer > 0)
				.orElse(0);
	}

注意到使用Optional和Stream时的那些通用模式了吗?它们都是对数据库查询过程的反思,查询时,多种操作会被串接在一起执行。

小结

这一章中,你学到了以下的内容。

  • null引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。
  • Java 8中引入了一个新的类java.util.Optional< T >,对存在或缺失的变量值进行建模。
  • 你可以使用静态工厂方法Optional.empty、Optional.of以及Optional.ofNullable创建Optional对象。
  • Optional类支持多种方法,比如map、flatMap、filter,它们在概念上与Stream类中对应的方法十分相似。
  • 使用Optional会迫使你更积极地解引用Optional对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。
  • 使用Optional能帮助你设计更好的API,用户如阅读方法签名,就能了解该方法是否接受一个Optional类型的值。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值