读书笔记:《Java实战(第2版)》第11章 用Optional取代null
第一次接触Optional
印象中,我第一次接触Optional是在写某个DAO层Repository时。我发现findByXXX(…)方法返回的不是具体的POJO,而是一个 Optional
下面是spring framework中的CrudRepository接口的代码,我们可以看到findById(…)方法的函数签名就是:Optional
package org.springframework.data.repository;
import java.util.Optional;
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S var1);
<S extends T> Iterable<S> saveAll(Iterable<S> var1);
Optional<T> findById(ID var1);
boolean existsById(ID var1);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> var1);
long count();
void deleteById(ID var1);
void delete(T var1);
void deleteAll(Iterable<? extends T> var1);
void deleteAll();
}
null的由来
1965年,英国计算机科学家Tony Hoare在设计ALGOL W语言时提出了null引用的想法:用null来为不存在的值建模。多年以后,他为自己的这个设计后悔不已。实际上,null带来的问题远超他想象。因为老的语言有这个设计,新的语言为了兼容不得不采用相同的设计:C,C++, Java, Python, Go等等都是如此。
为数不多的几个例外有:Haskell,ML等函数式语言。
恼人的NullPointerException
现实中,当我们不确定一个对象是否存在时,我们不得不增加额外的代码来保证程序运行过程中不会出现NullPointerException。常见的防御性方式有两种:
- 深层质疑–使用if多层嵌套
private int getGpuUsedNumOfV1Container(V1Container container) {
int usedNum = 0;
V1ResourceRequirements resources = container.getResources();
if (null != resources) {
Map<String, Quantity> limits = resources.getLimits();
if (null != limits) {
Quantity quantity = limits.get("gpu");
if(null != quantity){
if(null != quantity.getNumber()){
usedNum = quantity.getNumber().intValue();
}
}
}
}
return usedNum;
}
- 使用return提前返回–过多的退出语句
private int getGpuUsedNumOfV1Container(V1Container container) {
int usedNum = 0;
V1ResourceRequirements resources = container.getResources();
if (null == resources) {
return 0;
}
Map<String, Quantity> limits = resources.getLimits();
if (null == limits) {
return 0;
}
Quantity quantity = limits.get("gpu");
if(null == quantity){
return 0;
}
if(null == quantity.getNumber()){
return 0;
}
return quantity.getNumber().intValue();
}
有没有感觉到一丝丝代码的坏味道,但我们好像也没有更好的解决方案。
总结一下null带来的问题:
- 因为静态语言针对不存在的值设计的缺陷,java不得不继承了这一缺陷,从而违反了java设计的初衷:不向开发者暴露指针。但现在暴露了唯一的指针:null指针。
- 代码量膨胀。
- null自身没有任何语义,而且任何类型都可以被赋值为null。这就绕开了类型系统,当向接收一个第三方系统传来的变量是null时,可能我们无法知道最初它代表的是什么类型。
Optional的解决方案
针对上面null所带来的问题,Java 8正式引入了java.util.Optional类。
初识Optional
这个类设计不复杂,我们简单来看一看:
public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>();
private final T value;
private Optional() {
this.value = null;
}
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
public boolean isPresent() {
return value != null;
}
/**
* If a value is not present, returns {@code true}, otherwise
* {@code false}.
*
* @return {@code true} if a value is not present, otherwise {@code false}
* @since 11
*/
public boolean isEmpty() {
return value == null;
}
...
...
public T orElse(T other) {
return value != null ? value : other;
}
...
...
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
...
...
简单总结一下:
- 该类使用final进行了修饰,也就是说它不能被继承,不想被人改变自身的逻辑。
- 类中就一个属性:value,用来存放原始对象的值。
- 定义了一个不变的对象:EMPTY,用来表示空值。
- 提供了静态方法of(T value)、ofNullable(T value)进行实例化,前者校验原始对象的值不能为空,后者不会。
- 提供了isPresent()用来判断原始对象的值是否存在,并在Java 11引入了判断空的isEmpty()。
- orElse(T other) :如果原始对象值为空,返回给定的默认值,否则返回原值,这个方法也经常使用。
- orElseThrow(…) 与上面的orElse(…)相似,只是原始对象为空时,会抛出一个异常。
其他还有类似于流相关的操作方法,如map、flatmap、filter等等,可以自行查看源码了解。
实战
理论的东西终归是停留在纸上,我们动动手,通过几个例子来看看Optional是如何规避NullPointerException的。
这里我们依然使用《Java实战(第2版)》中的例子来说明。
先是领域对象,Person可能有一辆car,但一定有年龄age;Car可能买了保险insurance;Insurance保险一定有名字。代码如下:
/**
* Person类
*/
public class Person {
// private Optional<Car> car;
private Car car;
private int age;
public Person(Car car, int age) {
this.car = car;
this.age = age;
}
// 注意返回的是:Optional<Car>
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{" +
"car=" + car +
", age=" + age +
'}';
}
}
/**
* Car类
*/
public class Car {
// private Optional<Insurance> insurance;
private Insurance insurance;
public Car(Insurance insurance) {
this.insurance = insurance;
}
// 注意返回的是:Optional<Insurance>
public Optional<Insurance> getInsuranceAsOptional() {
return Optional.ofNullable(insurance);
}
@Override
public String toString() {
return "Car{" +
"insurance=" + insurance +
'}';
}
}
/**
* Insurance类
*/
public class Insurance {
private String name;
public Insurance(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Insurance{" +
"name='" + name + '\'' +
'}';
}
}
- 使用Optional
作为类的字段?
《Java实战(第2版)》中给我们提供了一种思路,即定义属性时,直接将属性定义为Optional
public class Person2 implements Serializable {
private Optional<Car> car;
private int age;
public Person2(Optional<Car> car, int age) {
this.car = car;
this.age = age;
}
public Optional<Car> getCar() {
return car;
}
public void setCar(Optional<Car> car) {
this.car = car;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person2{" +
"car=" + car +
", age=" + age +
'}';
}
}
@Slf4j
public class OptionalTest{
@Test
public void testOptionalSerialize() {
Car car = new Car(new Insurance("太平洋保险"));
Person2 person2 = new Person2(Optional.ofNullable(car), 22);
log.info("person2: {}", person2);
Gson gson = new Gson();
String person2Str = gson.toJson(person2);
log.info("person2Str: {}", person2Str);
log.info("person2Bytes: {}", toBytes(person2));
}
byte[] toBytes(Person2 person2) {
try (ByteArrayOutputStream bstream = new ByteArrayOutputStream();
ObjectOutputStream ostream = new ObjectOutputStream(bstream)) {
ostream.writeObject(person2);
return bstream.toByteArray();
} catch (IOException ioException) {
ioException.printStackTrace();
}
return null;
}
}
打印输出如下:
12:32:45.864 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - person2: Person2{car=Optional[Car{insurance=Insurance{name='太平洋保险'}}], age=22}
12:32:46.055 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - person2Str: {"car":{"value":{"insurance":{"name":"太平洋保险"}}},"age":22}
java.io.NotSerializableException: java.util.Optional
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1553)
at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1510)
at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1433)
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1179)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:349)
at com.practice.learn.moderninjava.chapter11.OptionalTest.toBytes(OptionalTest.java:224)
at com.practice.learn.moderninjava.chapter11.OptionalTest.testOptionalSerialize(OptionalTest.java:218)
...
...
12:32:46.087 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - person2Bytes: null
- Optional.map():获取或转换对象的值,返回一个Optional
@Slf4j
public class OptionalTest {
@Test
public void testGetObjectPropertyValue() {
IntStream.rangeClosed(1, 3).forEach(idx -> {
Insurance insurance = generateInsurance();
String name = Optional.ofNullable(insurance).map(Insurance::getName).orElse("unKnown");
log.info("insurance.name: {}", name);
});
}
private Insurance generateInsurance() {
Random random = new Random();
int val = random.nextInt(100);
if (val > 75) {
return new Insurance("太平洋保险");
} else if (val > 25) {
return new Insurance("国元保险");
} else {
return null;
}
}
}
输出:
{
06:34:23.481 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:34:23.485 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
06:34:23.485 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
}
可以看到,使用map()和orElse()方法配合,让我们不再写if…else…这样繁冗的代码了。
- Optional.flatMap(): 可以用于链接Optional对象
@Slf4j
public class OptionalTest {
@Test
public void testFlatmap() {
for (int i = 0; i < 10; i++) {
Optional<Person> person = Optional.ofNullable(generatePerson());
String insuranceName = person.flatMap(Person::getCarAsOptional)
.flatMap(Car::getInsuranceAsOptional)
.map(Insurance::getName)
.orElse("unKnown");
log.info("insurance.name: {}", insuranceName);
}
}
private Car generateCar() {
Random random = new Random();
int val = random.nextInt(100);
if (val < 51) {
return new Car(generateInsurance());
} else {
return null;
}
}
private Person generatePerson() {
Person person = null;
Random random = new Random();
int val = random.nextInt(100);
if (val > 51) {
int age = ThreadLocalRandom.current().nextInt(18, 60);
person = new Person(generateCar(), age);
}
return person;
}
}
输出:
06:43:49.967 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.972 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.972 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
06:43:49.972 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
这里需要注意flatMap与map的区别,map是对获取的值在外面包装一层Optional,而flatMap平铺则是将传入的mapper函数返回的Optional直接返回,外面不再包装一层Optional,如果再包装就变成了Optional<Optional
public final class Optional<T> {
...
...
/**
* If a value is present, returns the result of applying the given
* {@code Optional}-bearing mapping function to the value, otherwise returns
* an empty {@code Optional}.
* ...
* ...
*/
public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent()) {
return empty();
} else {
@SuppressWarnings("unchecked")
Optional<U> r = (Optional<U>) mapper.apply(value);
return Objects.requireNonNull(r);
}
}
...
/**
* If a value is present, returns an {@code Optional} describing (as if by
* {@link #ofNullable}) the result of applying the given mapping function to
* the value, otherwise returns an empty {@code Optional}.
* ...
* ...
*/
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent()) {
return empty();
} else {
return Optional.ofNullable(mapper.apply(value));
}
}
...
}
- Optional.stream():Java 9 引入的新方法,可以将Optional的值转换成一个流
/**
* 获取personList中拥有一辆车的人所使用的保险公司的名字列表
*/
@Test
public void testOptionalStream() {
List<Person> personList = IntStream.rangeClosed(1, 20)
.mapToObj(i -> generatePerson())
.collect(Collectors.toList());
// 常规做法,在一个map里面将其转换(不剔除重复)
Collection<String> nameList = personList.stream()
.filter(Objects::nonNull)
.map(person -> person.getCarAsOptional()
.flatMap(Car::getInsuranceAsOptional)
.map(Insurance::getName)
.orElse(null))
// .filter(Objects::nonNull)
.collect(Collectors.toList());
// .collect(Collectors.toSet());
log.info("1 names: {}", nameList);
// 通过stream的方式获取name(剔除重复)
Set<String> insuranceNames = personList.stream()
.filter(Objects::nonNull)
.map(Person::getCarAsOptional)
.map(optCar -> optCar.flatMap(Car::getInsuranceAsOptional))
.map(optIns -> optIns.map(Insurance::getName))
.flatMap(Optional::stream)
.collect(Collectors.toSet());
log.info("2 names: {}", insuranceNames);
}
输出:
07:15:16.132 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - 1 names: [国元保险, null, 国元保险, null, 太平洋保险, null, 太平洋保险, null, null, 国元保险, null, null]
07:15:16.139 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - 2 names: [太平洋保险, 国元保险]
如果没有使用IntelliJ IDEA 编辑器的话,是可以看到每一步的输出格式的,如下截图所示:
- 两个Optional对象的组合:flatMap与map的完美配合
/**
* 测试用例:通过Person和Car,找到最便宜的保险,如果入参中person或car为空,则返回空,否则一定可以找到合适的保险公司
*/
@Test
public void testOptionalCompose() {
Optional<Person> optPerson = Optional.ofNullable(generatePerson());
Optional<Car> optCar = Optional.ofNullable(generateCar());
Optional<Insurance> optInsurance1 = nullSafeFindCheapestInsurance1(optPerson, optCar);
Optional<Insurance> optInsurance2 = nullSafeFindCheapestInsurance2(optPerson, optCar);
log.info("optInsurance1: {}", optInsurance1.orElse(null));
log.info("optInsurance2: {}", optInsurance2.orElse(null));
}
/**
* 方案1:常规做法
*/
private Optional<Insurance> nullSafeFindCheapestInsurance1(Optional<Person> optPerson, Optional<Car> optCar) {
if (optPerson.isPresent() && optCar.isPresent()) {
return Optional.of(findCheapestInsurance(optPerson.get(), optCar.get()));
} else {
return Optional.empty();
}
}
/**
* 方案2: 使用Optional组合:flatMap, map
*/
private Optional<Insurance> nullSafeFindCheapestInsurance2(Optional<Person> optPerson, Optional<Car> optCar) {
return optPerson.flatMap(person -> optCar.map(car -> findCheapestInsurance(person, car)));
}
private Insurance findCheapestInsurance(Person person, Car car) {
// 查询各种保险公司数据,这里省略
// 比对数据,这里省略
return new Insurance("太平洋保险");
}
输出:
07:26:12.650 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optInsurance1: Insurance{name='太平洋保险'}
07:26:12.683 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optInsurance2: Insurance{name='太平洋保险'}
这里的方案2,就是组合使用Optional的典型,因为传入的两个值都有可能为空,常规的做法就是将他们依次做判断(即方案1)。而使用flatMap及map,它们内部会先进行空值判断,如果为空,就直接返回Optional.empty(),不会走后续的操作了。
- Optional.filter(Predicate<? super T> predicate):根据给定的Predicate进行过滤
/**
* 测试用例:找出年龄大于或等于给定参数minAge的Person所对应的保险公司名称
*/
@Test
public void testOptionalFilter() {
for (int i = 0; i < 5; i++) {
Optional<Person> optPerson = Optional.ofNullable(generatePerson());
String insuranceName = getCarInsuranceName(optPerson, 30);
log.info("optPerson: {}, insuranceName: {}", optPerson.orElse(null), insuranceName);
}
}
private String getCarInsuranceName(Optional<Person> optPerson, int minAge) {
return optPerson.filter(person -> person.getAge() >= minAge)
.flatMap(Person::getCarAsOptional)
.flatMap(Car::getInsuranceAsOptional)
.map(Insurance::getName)
.orElse("UnKnown");
}
输出:
07:35:38.245 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: null, insuranceName: UnKnown
07:35:38.248 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: null, insuranceName: UnKnown
07:35:38.249 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: Person{car=null, age=48}, insuranceName: UnKnown
07:35:38.278 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: null, insuranceName: UnKnown
07:35:38.278 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: null, insuranceName: UnKnown
- 使用Optional对异常进行封装
比如我们需要将String类型转换为int,而这可能会抛出NumberFormatException,为了将繁冗的try…catch…通用代码规避掉,我们可以将其包装为一个工具类,如下:
@Slf4j
public class OptionalUtil {
public static Optional<Integer> stringToInt(String str) {
try {
return Optional.of(Integer.parseInt(str));
} catch (NumberFormatException ex) {
log.error("NumberFormatException: {}", ex.getMessage());
}
return Optional.empty();
}
}
小Tips
- 不建议使用基础类型的Optional对象(如:OptionalInt, OptionalLong, OptionalDouble, etc),一是因为Optional只封装了一个值,基础类型的自动装箱和拆箱成本并不高,二是因为它不支持flatMap, map, filter等重要的操作。
- 使用Optional可以帮助我们设计更好的API,因为阅读方法签名,我们就了解了这个参数值可能为空,迫使我们对Optional对象进行解引用,从而有效遏制NullPointerException的出现。