美文网首页
Java8笔记(5)

Java8笔记(5)

作者: Cool_Pomelo | 来源:发表于2020-01-28 10:09 被阅读0次

Java8笔记(5)

用 Optional 取代 null

如何为缺失的值建模

假设你需要处理下面这样的嵌套对象,这是一个拥有汽车及汽车保险的客户

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; }


}



下面这段代码存在怎样的问题呢:


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


现实生活中很多人没有车。所以调用 getCar 方法的结果会怎样呢?在实践中,一种比较常见的做法是返回一个 null 引用,表示该值的缺失,即用户没有车。而接下来,对getInsurance 的调用会返回 null 引用的 insurance ,这会导致运行时出现一个 NullPointerException ,终止程序的运行

采用防御式检查减少 NullPointerException

通常,你可以在需要的地方添加 null 的检查(过于激进的防御式检查甚至会在不太需要的地方添加检测代码),并且添加的方式往往各有不同

null -安全的第一种尝试:深层质疑


 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";
    }


这个方法每次引用一个变量都会做一次 null 检查,如果引用链上的任何一个遍历的解变量
值为 null ,它就返回一个值为“Unknown”的字符串。唯一的例外是保险公司的名字,你不需要对它进行检查,原因很简单,因为任何一家公司必定有个名字。注意到了吗,由于你掌握业务领域的知识,避免了最后这个检查,但这并不会直接反映在你建模数据的Java类之中

将上面代码标记为“深层质疑”,原因是它不断重复着一种模式:每次你不确定一个变量是否为 null 时,都需要添加一个进一步嵌套的 if 块,也增加了代码缩进的层数。很明显,这种方式不具备扩展性,同时还牺牲了代码的可读性

null -安全的第二种尝试:过多的退出语句


 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();
    }


第二种尝试中,你试图避免深层递归的 if 语句块,采用了一种不同的策略:每次你遭遇 null变量,都返回一个字符串常量“Unknown”。然而,这种方案远非理想,现在这个方法有了四个截然不同的退出点,使得代码的维护异常艰难。更糟的是,发生 null 时返回的默认值,即字符串“Unknown”在三个不同的地方重复出现——出现拼写错误的概率不小

null 带来的种种问题

在Java程序开发中使用 null 会带来理论和实际操作上的种种问题

  • 它是错误之源。NullPointerException 是目前Java程序开发中最典型的异常
  • 它会使你的代码膨胀。它让你的代码充斥着深度嵌套的 null 检查,代码的可读性糟糕透顶
  • 它自身是毫无意义的。null 自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模
  • 它破坏了Java的哲学。Java一直试图避免让程序员意识到指针的存在,唯一的例外是: null 指针
  • 它在Java的类型系统上开了个口子。null 并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个 null 变量最初的赋值到底是什么类型

Optional 类

Java 8中引入了一个新的类 java.util.Optional<T> 。这是一个封装 Optional 值的类。举例来说,使用新的类意味着,如果你知道一个人可能有也可能没有车,那么 Person 类内部的 car 变量就不应该声明为 Car ,遭遇某人没有车时把 null 引用赋值给它,而是应该直接将其声明为 Optional<Car> 类型

变量存在时, Optional 类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的 Optional 对象,由方法 Optional.empty() 返回。 Optional.empty() 方法是一个静态工厂方法,它返回 Optional 类的特定单一实例。你可能还有疑惑, null 引用和 Optional.empty()

有什么本质的区别吗?从语义上,你可以把它们当作一回事儿,但是实际中它们之间的差别非常大 :

如 果 你 尝 试 解 引 用 一 个 null , 一 定 会 触 发NullPointerException , 不 过 使 用Optional.empty() 就完全没事儿,它是 Optional 类的一个有效对象,多种场景都能调用,非常有用

使用 Optional 而不是 null 的一个非常重要而又实际的语义区别是,第一个例子中,我们
在声明变量时使用的是 Optional<Car> 类型,而不是 Car 类型,这句声明非常清楚地表明了这里发生变量缺失是允许的。与此相反,使用 Car 这样的类型,可能将变量赋值为null ,这意味着你需要独立面对这些,你只能依赖你对业务模型的理解,判断一个 null 是否属于该变量的有效范畴

使用 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 {

    //保险公司必//须有名字
    private String name;
    public String getName() { return name; }
}


代码中 person 引用的是 Optional<Car> ,而 car 引用的是 Optional<Insurance> ,这种方式非常清晰地表达了你的模型中一个 person可能拥有也可能没有 car 的情形,同样, car 可能进行了保险,也可能没有保险

与此同时,我们看到 insurance 公司的名称被声明成 String 类型,而不是 Optional-
<String> ,这非常清楚地表明声明为 insurance 公司的类型必须提供公司名称

应用 Optional 的几种模式

创建 Optional 对象

声明一个空的 Optional

可以通过静态工厂方法 Optional.empty ,创建一个空的 Optional对象


Optional<Car> optCar = Optional.empty();

依据一个非空值创建 Optional

使用静态工厂方法 Optional.of ,依据一个非空值创建一个 Optional 对象


Optional<Car> optCar = Optional.of(car);

可接受 null 的 Optional

使用静态工厂方法 Optional.ofNullable ,你可以创建一个允许 null 值的 Optional
对象


Optional<Car> optCar = Optional.ofNullable(car);

如果 car 是 null ,那么得到的 Optional 对象就是个空对象

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

从对象中提取信息是一种比较常见的模式。比如,你可能想要从 insurance 公司对象中提取公司的名称。提取名称之前,你需要检查 insurance 对象是否为 null

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

为了支持这种模式, Optional 提供了一个 map 方法


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

map 操作会将提供的函数应用于流的每个元素。你可以把 Optional 对象看成一种特殊的集合数据,它至多包含一个元素。如果 Optional 包含一个值,那函数就将该值作为参数传递给 map ,对该值进行转换。如果 Optional 为空,就什么也不做

使用 flatMap 链接 Optional 对象

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


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

默认行为及解引用 Optional 对象

采用 orElse 方法读取这个变量的值,使用这种方式你还可以定义一个默认值,遭遇空的 Optional 变量时,默认值会作为该方法的调用返回值。 Optional 类提供了多种方法读取Optional 实例中的变量值

  • get() 是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量
    值,否则就抛出一个 NoSuchElementException 异常。所以,除非你非常确定 Optional
    变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于
    嵌套式的 null 检查,也并未体现出多大的改进
  • orElse(T other)它允许你在Optional 对象不包含值时提供一个默认值

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

  • orElseThrow(Supplier<? extends X> exceptionSupplier) 和 get 方法非常类似,它们遭遇 Optional 对象为空时都会抛出一个异常,但是使用 orElseThrow 你可以定制希望抛出的异常类型
  • ifPresent(Consumer<? super T>) 让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作

两个 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();
        }
    }

这个方法具有明显的优势,我们从它的签名就能非常清楚地知道无论是 person 还是 car ,它的值都有可能为空,出现这种情况时,方法的返回值也不会包含任何值。不幸的是,该方法的具体实现和你之前曾经实现的 null 检查太相似了:方法接受一个 Person 和一个 Car 对象作为参数,而二者都有可能为 null

以不解包的方式组合两个 Optional 对象


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

使用 filter 剔除特定的值

经常需要调用某个对象的方法,查看它的某些属性。比如,你可能需要检查保险公司的名
称是否为“Cambridge-Insurance”。为了以一种安全的方式进行这些操作,你首先需要确定引用指向的 Insurance 对象是否为 null ,之后再调用它的 getName 方法

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"));

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

对 Optional 对象进行过滤

找出年龄大于或者等于 minAge 参数的 Person 所对应的保险公司列表

public String getCarInsuranceName(Optional<Person> person, int minAge) {
    return person.filter(p -> p.getAge() >= minAge)
            .flatMap(Person::getCar)
            .flatMap(Car::getInsurance)
            .map(Insurance::getName)
            .orElse("Unknown");
}


使用 Optional 的实战

用 Optional 封装可能为 null 的值

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


public static void main(String[] args) {

        Map<Integer,String> map = new HashMap<>();

        map.put(1,"one");

        String s = map.get(3);

//        null
        System.out.println(s);

        Optional<Object> o = Optional.ofNullable(map.get(3));

        System.out.println(o);


    }


异常与 Optional 的对比

由于某种原因,函数无法返回某个值,这时除了返回 null ,Java API比较常见的替代做法是抛出一个异常。这种情况比较典型的例子是使用静态方法 Integer.parseInt(String) ,将String 转换为 int 。在这个例子中,如果 String 无法解析到对应的整型,该方法就抛出一个NumberFormatException 。最后的效果是,发生 String 无法转换为 int 时,代码发出一个遭遇非法参数的信号,唯一的不同是,这次你需要使用 try / catch 语句,而不是使用 if 条件判断来控制一个变量的值是否非空

将 String 转换为 Integer ,并返回一个 Optional 对象


    public static Optional<Integer> stringToInt(String s){
        try {

//            如果 String 能转换为对//应的 Integer ,将其封装//在 Optioal 对象中返回
            return Optional.of(Integer.parseInt(s));
        } catch (NumberFormatException e){

//            否则返回一个空//的 Optional 对象
            return Optional.empty();
        }
    }


相关文章

  • Java8学习笔记目录

    Java8学习笔记 -- 接口的默认方法与静态方法 Java8学习笔记 -- Lambda表达式,Function...

  • Java8笔记(5)

    Java8笔记(5) 用 Optional 取代 null 如何为缺失的值建模 假设你需要处理下面这样的嵌套对象,...

  • Java8学习笔记-1

    Java8学习笔记-1序在java11即将面世的时候,终于开始行动学习java8的特性。目前没有机会实践,只是跟着...

  • 2018-10-28

    Java8学习笔记-1序在java11即将面世的时候,终于开始行动学习java8的特性。目前没有机会实践,只是跟着...

  • Java8中的并行流

    此笔记是我在阅读《Java8实战》时的一些记录。 Java8中增加了流(stream)的概念,为数据的处理带来了很...

  • java8接口新特性:default、static

    之前面试时被过关于java8中接口的新特性问题,当时没有回答好,后查找资料,在这里做下笔记。 java8之前接口中...

  • java8 stream 流处理探索

     使用java8也有一段时间了,但是一直没有整理过具体的笔记,现补充一下。 java8流处理让集合操作变得简便了很...

  • 【Java8新特性】01 函数式接口和Lambda表达式你真的会

    Java8 由Oracle在2014年发布,是继Java5之后最具革命性的版本。 Java8吸收其他语言的精髓带来...

  • Java8 Stream API 笔记

    Java8 Stream API 笔记 常用的stream操作,做个记录,方便日后使用查询。 [TOC] 1. f...

  • 第64节:Java中的Spring Boot 2.0简介笔记

    Java中的Spring Boot 2.0简介笔记 spring boot简介 依赖java8的运行环境多模块项目...

网友评论

      本文标题:Java8笔记(5)

      本文链接:https://www.haomeiwen.com/subject/pgmpthtx.html