【Java 8】优雅的解决空指针NullPointerException —— Optional API的详解与使用示例

一、Java 8 为什么引入Optional类?

null在Java中是一个合法值,目的是为了表示变量值的缺失。但对null引用会引起NullPointerException(空指针异常)。在Java8以前,Java程序员操作对象时,为了避免错误引用null造成的空指针异常,往往需要一系列繁杂冗余的判空操作,增加了许多重复代码,降低了代码可读性,于是Java 8 引入Optional,优雅简洁的对null值进行处理

为了便于理解和说明,本文将用以下的模型进行讲解:

假设有以下嵌套对象:人持有车,车持有保险,保险有名字属性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8GCA9EtV-1633876364039)(Java8-Optional.assets/image-20210925222029200-2579657.png)]

public class Person {
    private Car car;
    public Car getCar() { return car; }
}
public class Car {
    private Insurance insurance;
    public Insurance getInsurance() { return insurance; }
   
}
public class Insurance {
    private String name;
    public String getName() { return name; }
}

假设要获取某一Person对象的车的保险的名字:

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

上面代码看似正常,但实际情况是getCar()和getInsurance()方法都可能返回null值,导致后续引用引起空指针异常。

Java 8 以前是怎么处理以保证null安全的呢

1) 过多的 if 嵌套:

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

或者:

2)过多的 return语句:

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

接下来让我们进入下一章,了解Optional类,看看Optional类是怎样处理上述情形的。

二、什么是Optional类?

Java 8 中引入了一个新的类java.util.Optional<T>。可以理解为这是一个封装普通类对象(包括空值null)的容器类,Optional 对象最多只包含一个值。举例说明:

如果你知道一个人可能有也可能没有车,那么Person类内部的car变量就不应该声明为Car,当某人没有车时把null引用赋值给它,而是应该像下图那样直接将其声明为Optional<Car>类型。变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空” 的Optional对象,由方法Optional.empty()返回。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NEaWuMLz-1633876364043)(Java8-Optional.assets/image-20210925224832661.png)]

使用Optional重新定义Person/Car/Insurance的数据模型

public class Person {
  	// 人可能有车,也可能没有车,因此将这个字段声明为Optional
    private Optional<Car> car; 
    public Optional<Car> getCar() { return car; }
}
public class Car {
  	// 车可能进行了保险,也可能没有保险,所以将这个字段声明为Optional
    private Optional<Insurance> insurance; 
    public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
  	//  保险公司必须有名字, 因此没有使用Optional
    private String name;
    public String getName() { return name; }
}

优点

  • Optional丰富了你模型的语义,这种方式非常清晰地表达了你的模型中一个person 可能拥有也可能没有car的情形,同样,car可能有保险,也可能没有保险;insurance必须含有名字。

  • 在你的代码中始终如一地使用Optional,能非常清晰地界定出变量值的缺失是结构上的问 题,还是你算法上的缺陷,抑或是你数据中的问题。一旦获取insurance公司名称时发生NullPointerException,你就能非常确定地知道出错的原因,不再需要为其添加null的检查,因为null的检查只会掩盖问题,并未真正地修复问题。 insurance公司必须有个名字,所以,如果你遇到一个公司没有名称,你需要调查你的数据出了什么问题,而不应该再添加一段代码,将这个问题隐藏。

  • 帮助你更好地设计出普适的API, 让程序员看到方法签名,就能了解它是否接受一个null的值

三、如何创建Optional对象

  1. 声明一个空的Optional
		// 通过静态工厂方法Optional.empty(),创建一个空的Optional对象
		Optional<Car> optCar = Optional.empty();
  1. 依据一个非空值创建Optional (不推荐)

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

		// 静态工厂方法Optional.of(T t),依据一个非空值创建一个Optional对象
		Optional<Car> optCar = Optional.of(car);
  1. 可接受nullOptional (推荐)
		// 用静态工厂方法Optional.ofNullable(T t),你可以创建一个允许null值的Optional对象
    Optional<Car> optCar = Optional.ofNullable(car);

四、如何从Optional对象中提取和转换值?

1. map方法

如果值存在,就对该值执行提供的mapping 函数调用, 如果值不存在,则返回一个空的Optional对象

引入Optional 以前:

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

引入Optional 后:

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

Optional的map方法和 Java 8 中Stream的map方法相差无几:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xpGeZiPt-1633876364045)(Java8-Optional.assets/image-20210925231616967.png)]

2. flatMap方法

试试重构第一章中的代码:

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

由于我们刚刚学习了如何使用map,你的第一反应可能是我们可以利用map重写之前的代码:

  Optional<Person> optPerson = Optional.of(person);
      Optional<String> name =
          optPerson.map(Person::getCar)
                   .map(Car::getInsurance)
                   .map(Insurance::getName);

不幸的是,这段代码无法通过编译。为什么呢? optPerson是Optional<Person>类型的 变量, 调用map方法应该没有问题。但getCar返回的是一个Optional<Car>**类型的对象,这意味着map操作的结果是一个Optional<Optional<Car>>类型的对象。因此,它对getInsurance的调用是非法的。上面代码map(Car :: getInsurance) 的返回结果如下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BWuBfO7u-1633876364048)(Java8-Optional.assets/image-20210926150047811.png)]

对于这种嵌套式的Optiona结构,我们应该使用flatMap方法,将两层的Optional合并成一个

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OloChE9q-1633876364051)(Java8-Optional.assets/image-20210926150405501.png)]

下面应用map和flatMap对上述示例进行重写:

使用Optional获取car的保险公司名称:

  public String getCarInsuranceName(Optional<Person> person) { 
      return person.flatMap(Person::getCar)
                       .flatMap(Car::getInsurance)
                       .map(Insurance::getName)
                       .orElse("Unknown"); // 如果Optional的结果 值为空设置默认值
  }

上述步骤可图解为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IsKidDnx-1633876364055)(Java8-Optional.assets/image-20210926150957998.png)]

对比第一章中Java 8 之前的代码和上面的代码,不难发现引入Optional后的优点:

  • 不再需要使用那么多的条件分支,也不会增加代码的复杂性

五、如何获取Optional对象和设置默认值?

1. get()

  • get() 是这些方法中最简单但又最不安全的方法。不推荐使用

    如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常,不推荐使用。

2. orElse(T other)

  • orElse(T other) 它允许你在 Optional对象不包含值时提供一个默认值。

3. orElseGet(Supplier<? extends T> other)

  • orElseGet(Supplier<? extends T> other)orElse方法的延迟调用版,Supplier 方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作, 你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在 Optional为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。

4. orElseThrow(Supplier<? extends X> exceptionSupplier)

  • orElseThrow(Supplier<? extends X> exceptionSupplier) 和get方法非常类似, 它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型。

5. ifPresent(Consumer<? super T> consumer)

  • ifPresent(Consumer<? super T> consumer) 让你能在变量值存在时执行一个作为参数传入的 方法,否则就不进行任何操作

p.s. orElse中调用的方法一直都会被执行,orElseGet方法只有在Optional对象不含值时才会被调用,所以使用orElse方法时需要谨慎, 以免误执行某些不被预期的操作

六、其他Optional对象的方法

1. isPresent方法

如果Optional对象包含值,该方法就返回true,否则为false

2. filter方法

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

比如,你可能需要检查保险公司的名称是否为“Cambridge-Insurance”。

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

使用Optional对象的filter方法,这段代码可以重构如下:

Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance ->
                        "CambridgeInsurance".equals(insurance.getName()))
            .ifPresent(x -> System.out.println("ok"));

七、Optional类常用方法总结

方法描述
empty返回一个空的 Optional 实例
filter如果值存在并且满足提供的谓词,就返回包含该值的 Optional 对象;否则返回一个空的 Optional 对象
flatMap如果值存在,就对该值执行提供的 mapping 函数调用,返回一个 Optional 类型的值,否则就返回一个空的 Optional 对象
get如果该值存在,将该值用 Optional 封装返回,否则抛出一个 NoSuchElementException 异常
ifPresent如果值存在,就执行使用该值的方法调用,否则什么也不做
isPresent如果值存在就返回 true,否则返回 false
map如果值存在,就对该值执行提供的mapping 函数调用
of将指定值用 Optional 封装之后返回,如果该值为 null,则抛出一个 NullPointerException 异常
ofNullable将指定值用 Optional 封装之后返回,如果该值为 null,则返回一个空的 Optional 对象
orElse如果有值则将其返回,否则返回一个默认值
orElseGet如果有值则将其返回,否则返回一个由指定的 Supplier 接口生成的值
orElseThrow如果有值则将其返回,否则抛出一个由指定的 Supplier 接口生成的异常

八 、使用示例

1. 两个Optional对象的组合

现在,我们假设你有这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:

  public Insurance findCheapestInsurance(Person person, Car car) { 
      // 不同的保险公司提供的查询服务
      // 对比所有数据
      return cheapestCompany;
  }

我们还假设你想要该方法的一个null-安全的版本,它接受两个Optional对象作为参数, 返回值是一个Optional<Insurance>对象,如果传入的任何一个参数值为空,它的返回值亦为空。Optional类还提供了一个isPresent方法,如果Optional对象包含值,该方法就返回true, 所以你的第一想法可能是通过下面这种方式实现该方法:

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
        if (person.isPresent() && car.isPresent()) {
          return Optional.of(findCheapestInsurance(person.get(), car.get())); 
        } else {
          return Optional.empty();
        }
}

你可以像使用三元操作符那样,无需任何条件判断的结构,以一行语句实现该方法,代码如下。

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
  
  		return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c))); 
}

2. Optional工具类的封装

相信你已经了解,有效地使用Optional类意味着你需要对如何处理潜在缺失值进行全面的反思。这种反思不仅仅限于你曾经写过的代码,更重要的可能是,你如何与原生Java API实现共存共赢。

实际上,我们相信如果Optional类能够在这些API创建之初就存在的话,很多API的设计编写可能会大有不同。为了保持后向兼容性,我们很难对老的Java API进行改动,让它们也使用 Optional,但这并不表示我们什么也做不了。你可以在自己的代码中添加一些工具方法,修复或者绕过这些问题,让你的代码能享受Optional带来的威力。

1)对Java API中返回null值的方法进行封装

用Map做例子,假设你有一个Map<String, Object>方法,访问由key索引的值时,如果map 中没有与key关联的值,该次调用就会返回一个null:

		Object value = map.get("key");

可以采用我们前文介绍的Optional.ofNullable方法这段代码进行优化:

		Optional<Object> value = Optional.ofNullable(map.get("key"));

每次你希望安全地对潜在为null的对象进行转换,将其替换为Optional对象时,都可以考 虑使用这种方法。

2)对异常的封装

由于某种原因,函数无法返回某个值,这时除了返回null,Java API比较常见的替代做法是抛出一个异常。

比如:方法Integer.parseInt(String),将 String转换为int。在这个例子中,如果String无法解析到对应的整型,该方法就抛出一个 NumberFormatException。

我们无法修改最初的Java方法,但是这无碍我们进 行需要的改进,你可以实现一个工具方法,将这部分逻辑封装于其中,最终返回一个我们希望的 Optional对象,代码如下所示。

  public static Optional<Integer> stringToInt(String s) {
    try {
          return Optional.of(Integer.parseInt(s));
      } catch (NumberFormatException e) {
          return Optional.empty();
      }
  }

可以将多个类似的方法封装到一个工具类中,让我们称之为OptionalUtility。通过这种方式,你以后就能直接调用OptionalUtility.stringToInt方法,将String转换为一个Optional对象,而不再需要记得你在其中封装了笨拙的 try/catch的逻辑了。

3)工具类的使用示例

假设你需要向你的程序传递一些属性。为了举例以及测试你开发的代码,你创建了一些示例属性,如下所示:

		Properties props = new Properties();
    props.setProperty("a", "5");
    props.setProperty("b", "true");
    props.setProperty("c", "-3");

现在,我们假设你的程序需要从这些属性中读取一个值,该值是以秒为单位计量的一段时间。 由于一段时间必须是正数,你想要该方法符合下面的签名:

		public int readDuration(Properties props, String name)

即,如果给定属性对应的值是一个代表正整数的字符串,就返回该整数值,任何其他的情况都返 回0。为了明确这些需求,你可以采用JUnit的断言,将它们形式化:

		assertEquals(5, readDuration(param, "a"));
    assertEquals(0, readDuration(param, "b"));
    assertEquals(0, readDuration(param, "c"));
    assertEquals(0, readDuration(param, "d"));

让我们先以传统的方式实现满足这些需求的方法, 代码清单如下所示:

  public int readDuration(Properties props, String name) {
      String value = props.getProperty(name);
      if (value != null) {
          try {
            int i = Integer.parseInt(value); //将String属性转 换为数字类型
            if (i > 0) { //检查返回的数字是否为正数
              return i;
            }
          } catch (NumberFormatException nfe) { }
      }
    	return 0; 
  }

上面的实现既复杂又不具备可读性,让我们用之前封装的OptionalUtility来重写上面代码:

  public int readDuration(Properties props, String name) {
          return Optional.ofNullable(props.getProperty(name))
            .flatMap(OptionalUtility::stringToInt)
            .filter(i -> i > 0)
            .orElse(0);
  }

九、使用Optional需要注意的点

1. Optional的序列化问题

由于Optional类设计时就没特别考虑将其作为类的字段使用,所以它也并未实现 Serializable接口。由于这个原因,如果你的应用使用了某些要求序列化的库或者框架,在域模型中使用Optional,有可能引发应用程序故障

然而,我们相信,通过前面的介绍,你 已经看到用Optional声明域模型中的某些类型是个不错的主意,尤其是你需要遍历有可能全 部或部分为空,或者可能不存在的对象时。如果你一定要实现序列化的域模型,作为替代方案, 我们建议你像下面这个例子那样,提供一个能访问声明为Optional、变量值可能缺失的接口,代码清单如下:

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

2. 避免使用基础类型的Optional对象

Optional提供了的一些基础类型——OptionalIntOptionalLong以及OptionalDouble. 但不推荐大家使用基础类型的Optional,因为基础类型的Optional不支持map、flatMap以及filter方法,而这些却是Optional类常用的方法。可以使用Optional<Int>, Optional<Long>, Optional<Double>等替代。

3. orElse方法的使用

orElse中调用的方法一直都会被执行,orElseGet方法只有在Optional对象不含值时才会被调用,所以使用orElse方法时需要谨慎, 以免误执行某些不被预期的操作。此种情况下,可使用orElseGet方法代替它。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值