学而实习之 不亦乐乎

Java:Optional 最佳实践

2021-12-18 22:59:41

一、引子

Optional隐藏了可能存在空指针的不确定性,比如:

List<String> numbers= ImmutableList.of("ONE", "TWO", "THREE");

return numbers.stream()
 .filter(number -> "FOUR".equals(number))
 .findAny()
 .toLoweCase();

结果会导致NullPointerExceptions空指针错误,而使用if else进行判断是一种切割器cutter味道:

List<String> numbers= ImmutableList.of("ONE", "TWO", "THREE");
String numberThatImLookingFour = 
numbers.stream()
 .filter(number -> "FOUR".equals(number))
 .findAny();
if(numberThatImLookingFour != null){
    return numberThatImLookingFour.toLowerCase();
}else{
    return "not found";
}

二、理解 Optional

什么是 Optional?
它是 box 类型,保持对另一个对象的引用。
是不可变的,不可序列化的
没有公共构造函数
只能是present 或absent
通过of(), ofNullable(), empty() 静态方法创建。

从这个盒子box中如何获取值?
get()
orElse()
orElseGet()
orElseThrow()
有一种诱惑是调用get()来获取其中的值。我们都知道普通JavaBean的getter / setters :)。并且期望如果我们调用get ...()我们就会得到一些东西。当调用普通bean的getter时,你永远不会得到任何抛出的异常。但是,如果调用在optional上调用get方法,并且该选项内部为空时,则会抛出异常NoSuchElementException。

三、最佳实践

规则1:不要将null赋给Optional

规则2:避免使用Optional.get()。

如果你不能证明存在可选项,那么永远不要调用get()。使用 orElse(), orElseGet(), orElseThrow() 获得你的结果。

可以重构以下代码:

String variable = fetchSomeVaraible();
if(variable == null){
     1. throw new IllegalStateException("No such variable");
     2. return createVariable();
     3. return "new variable";
} else { 
     ... 
     100 lines of code
     ...
}

重构到:

1. 
Optional<String> variableOpt = fetchOptionalSomeVaraible();
String variable = variableOpt.orElseThrow(() -> new Exeption("")) 
... 100 lines of code ...
2.
Optional<String> variableOpt = fetchOptionalSomeVaraible();
String variable = variableOpt.orElseGet(() -> createVariable()) 
... 100 lines of code ...
3.
Optional<String> variableOpt = fetchOptionalSomeVaraible();
String variable = variableOpt.orElse("new variable") 
... 100 lines of code ...

注意,orElse(..)是急切计算,意味着下面代码:

Optional<Dog> optionalDog = fetchOptionalDog();
optionalDog
 .map(this::printUserAndReturnUser)
 .orElse(this::printVoidAndReturnUser)

如果值存在则将执行两个方法,如果值不存在,则仅执行最后一个方法。为了处理这些情况,我们可以使用方法orElseGet(),它将supplier 作为参数,并且是惰性计算的。

规则3:不要在字段,方法参数,集合中使用 Optional。

下面是将thatField直接赋值给了类字段:

public void setThatField(Optional <ThatFieldType> thatField){ 
  this.thatField = thatField; 
} 

改为:

setThatField(Optional.ofNullable(thatField));

规则4:只有每当结果不确定时,使用 Optional 作为返回类型。

说实话,这是使用Optional的唯一好地方。其目的是为库方法的返回类型提供一种有限的机制,其中需要一种明确的方式来表示“无结果”,并且对于这样的方法使用 null 绝对可能导致错误。

规则5:不要害怕使用map和filter。

有一些值得遵循的一般开发实践称为:硬件抽象层(Single Layer of Abstraction)。下面是需要被重构代码:

Dog dog = fetchSomeVaraible();
String dogString = dogToString(dog);
public String dogToString(Dog dog){
    if(dog == null){
        return "DOG'd name is : " + dog.getName();
    } else { 
        return "CAT";
    }
}

重构到:

Optional<Dog> dog = fetchDogIfExists();
String dogsName = dog
 .map(this::convertToDog)
 .orElseGet(this::convertToCat)
 
public void convertToDog(Dog dog){
   return "DOG'd name is : " + dog.getName();
}
public void convertToCat(){
   return "CAT";
}

Filter是有用的折叠语法:

Dog dog = fetchDog();
if(optionalDog != null && optionalDog.isBigDog()){
  doBlaBlaBla(optionalDog);
}

上面代码可以被重构为:

Optional<Dog> optionalDog = fetchOptionalDog();
optionalDog
 .filter(Dog::isBigDog)
 .ifPresent(this::doBlaBlaBla)

规则6:不要为了链方法而使用 optional 。

使用 optional  时要注意的一件事是链式方法的诱惑。当我们像构建器模式一样链接方法时,事情可能看起来很漂亮,但并不总是等于更具可读性。所以不要这样做:

Optional
    .ofNullable(someVariable)
    .ifPresent(this::blablabla)

它对性能不利,对可读性也不好。我们应尽可能避免使用null引用。

规则7:使所有表达式成为单行 lambda

这是更普遍的规则,我认为也应该应用于流。但这篇文章是关于optional 。使用Optional 重要点是记住等式左边和右边一样重要:

Optional
 .ofNullable(someVariable)
 .map(variable -> {
   try{
      return someREpozitory.findById(variable.getIdOfOtherObject());
   } catch (IOException e){
     LOGGER.error(e); 
     throw new RuntimeException(e); 
   }})
 .filter(variable -> { 
   if(variable.getSomeField1() != null){
     return true;
   } else if(variable.getSomeField2() != null){
     return false;   
   } else { 
     return true;
   }
  })
 .map((variable -> {
   try{
      return jsonMapper.toJson(variable);
   } catch (IOException e){
     LOGGER.error(e); 
     throw new RuntimeException(e); 
   }}))
 .map(String::trim)
 .orElseThrow(() -> new RuntimeException("something went horribly wrong."))

上面那么冗长代码块可以使用方法替代:

Optional
 .ofNullable(someVariable)
 .map(this::findOtherObject)
 .filter(this::isThisOtherObjectStale)
 .map(this::convertToJson)
 .map(String::trim)
 .orElseThrow(() -> new RuntimeException("something went horribly wrong."));