Java Reinforcement 13 默认方法和Optional类

Java Reinforcement 13 默认方法和Optional类

接口中本来只能有抽象方法, 如果有域会变成静态域, 现在接口新增了默认方法, 其实是让接口更加灵活, 也便于在原来的类的基础上进行扩展出一套新的体系. Java 8 接口的两大变化 一是允许在接口内声明静态方法, 二是一个新功能:默认方法, 可以指定接口方法的默认实现. 一言以蔽之, 原来接口仅仅是

接口中本来只能有抽象方法, 如果有域会变成静态域, 现在接口新增了默认方法, 其实是让接口更加灵活, 也便于在原来的类的基础上进行扩展出一套新的体系. Java 8 接口的两大变化 一是允许在接口内声明静态方法, 二是一个新功能:默认方法, 可以指定接口方法的默认实现. 一言以蔽之, 原来接口仅仅是抽象的, 现在接口也可以带有方法实现. 仅仅增加接口而不重新编写接口的方法, 就可以实现新功能. 传统的Java编写方式, 会为一个类提供一个辅助的工具类, 其中存放静态方法, 有了默认方法之后, 可以将静态方法转移到默认接口中, 就无需编写辅助工具类, 而且更加合理.
  1. 默认方法的使用
  2. 解决冲突的规则
  3. Optional对象
  4. Optional类的使用
  5. Optional解引用

默认方法的使用

在接口中定义默认方法的关键字是default, 其他就和普通的方法修饰一样, 注意这个方法也会自动成为public方法. 默认方法是非抽象方法, 因此函数式接口可以附带默认方法. 默认方法也可以由具体实现类覆盖. 有了默认方法之后, 通过在接口中定义一些抽象方法和一个默认方法, 就可以组成一套功能体系, 可以用来方便快捷的通过实现接口来得到不同的功能. 之后在开发的时候也要注意使用默认接口, 以及将工具方法写在接口中. 书中有一点非常好, 即尽量不要简单的遇到问题就使用继承, 可以使用代理或者分解成接口, 以及适当的使用final来保护核心类.

解决冲突的规则

默认方法解决冲突的规则如下三个:
  1. 类里的方法优先
  2. 没有类实现方法, 先把所有的接口的方法排出来找到根源, 再进行对比, 越详细的接口的方法越优先. 这个方法指的是要追根溯源的方法
  3. 手工指定
看几个例子:
public interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}
public interface B extends A {
    default void hello() {
        System.out.println("Hello from B");
    }
}
public class C implements B, A {
    public static void main(String... args) {
        new C().hello();
    }
}
这个继承链条很清晰, B继承A然后覆盖了A的默认方法, C同时实现B和A, 由于B中更具体, 所以使用的是接口B中的方法. 现在引入一个新的类D, D与A和B都没有关系:
public class D {
    public void hello() {
        System.out.println("Hello from D");
    }
}
让C继承D:
public class C extends D implements B, A {
    public static void main(String... args) {
        new C().hello();
    }
}
这个时候类中的方法比接口的方法要优先, 所以使用的是D的方法. 如果复杂一些, 比如D仅仅实现A接口, 然后C继承D,实现A和B:
public class D implements A {

}

public class C extends D implements B, A {
    public static void main(String... args) {
        new C().hello();
    }
}
这个时候就要分析, 始终记得方法要追根溯源. 首先C继承D, D中没有覆盖默认方法, 因此D中的默认方法来自A, 那么C继承的所有方法的备选中首先是来自A的方法. 之后由于实现B, 所以方法备选也有一个是来自B的方法. 还实现A, 也是一个来自A的方法. 由于没有类方法, 所以需要比较所有来自接口的方法的所在接口的详细程度, 由于B继承A, 所以B更具体, 所以依然选择的是B中的方法. 如果两个接口的地位相等, 没有继承关系, 也没有更具体的实现, 那么编译器会报错, 让你手工指定一个来自于某个接口的方法. 指定的方式在Java 8 中是一个新的语法, 比如现在需要强行指定C的方法来自于A, 需要先覆盖方法, 然后在方法中采取特殊的写法:
//在上边的例子中, B不再继承A, 这样A,B无关系, C同时实现A和B
public interface B {
    default void hello() {
        System.out.println("Hello from B");
    }
}

Optional对象

Optional是Java 8 新引入的, 为了配合流和函数式编程, 同时也为了处理null指针问题而引入的一个新对象. Optional带有泛型, 用于包裹可能(也可以强制不为null)为null的对象, 用于处理, 可以将其抽象成只有一个元素的流. 看一个简单的例子, 一个人拥有车, 车有保险, 保险有一个名称, 这是一个对象的组合关系. 如果我们得到一个Person 对象, 就可以取出其中的保险名称.
public class Insurance {
    private String name;
    public String getName() { return name; }

    public Insurance(String name) {
        this.name = name;
    }
}

public class Car {
    private Insurance insurance;
    public Insurance getInsurance() { return insurance; }
}

public class Person {
    private Car car;
    public Car getCar() { return car; }
}

public class Test {

    public static void main(String[] args) {
        Person a = new Person();
        a.getCar().getInsurance();
    }
}
结果这段代码能通过编译, 但执行的时候会发现有空指针错误, 这是因为不一定每个Person都有车, 每个车都有保险. 如果为了防止空指针, 那需要在每个类的操作中都加上对空指针, 或者采取if判断. 导致代码臃肿, 而且无法应对变化. 为了解决这个问题, 用Optional类. 使用Optional类, 要先确定那个对象可能会出现空指针. 由于Person对象是要使用的, 所以不可能是空指针. 但一个Person对象中的Car对象可能是空指针, 因此就将其改成Optional类型. 同样, Car也可能没有保险, 则Car 中的 Insurance 也要改成Optional对象:
public class Person {
    private Optional<Car> car;

    public Optional<Car> getCar() {
        return car;
    }
}

public class Car {
    private Optional<Insurance> insurance;

    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}
在这里IDEA会有提示, 将Optional字段设置为域, 感觉上不是太好, 不过这是为了说明如何使用Optional对象. 在改造完成之后, 实际上看到Optional, 就要想到, 这可能是一个空指针. 这样修改以后, 如果代码出错, 几乎可以确定是Insurance类的问题, 因为通过Optional已经标识了可能出现的空指针. 当然现在还没有具体指向对象, 如何具体使用, 要来看看.

Optional类的使用

仅仅包裹原来的对象引用是不够的, 其中的对象如果确实是空指针, 使用 .get()方法, 依然会报错. 所以要系统的学习一下. 首先是如何创建一个Optional对象, 首先任何Optional 对象 都可以被赋值为一个 Optional.empty() 得到的空Optional对象. 有点类似null可以赋值给任何对象引用:
//声明一个空的Optional对象
Optional<Car> optCar = Optional.empty();
使用静态工厂方法Optional.of来依据一个非空值创建一个Optional对象:
Optional<Car> optCar = Optional.of(car);
这个.of()方法的好处是, 如果car是null, 会立刻抛出一个NullPointerException, 也就不会让你在访问的时候才出现空指针错误. 还有一种方法是法Optional.ofNullable, 可以允许上边的car为空的时候调用. 如果car真的为空, 则会得到一个和Optional.empty()一样的空对象. 使用上边三种方法来包裹可能出现null指针的对象的话, 得到的结果只有两种, 即包裹了一个正常引用的Optional对象, 或者一个空Optional对象. 包裹之后, 如何取出其中的值就是关键了. 有一个.get()方法之前使用过, 但是如果其中是空的话, 依然会抛出异常, 所以必须按照一定的约定来使用. 先看看一系列操作.

map

将Optional对象抽象成只有一个元素的流, 其也提供了一个map方法, 与流的map方法类似, 提供一个映射, 如果是空Optional, 就什么也不做. 这就比简单的直接使用原始类型有意思的多. 比如我们可以把一个车的对象转换成一个其保险名称的String类型:
public class Test {

    public static void main(String[] args) {
        Insurance insurance = new Insurance("saner");

        Car car = new Car();

        car.setInsurance(Optional.of(insurance));

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

        Optional<String> carToString = carOptional.map(s -> s.getInsurance().get().getName());

        System.out.println(carToString.get());
    }
}
这个和流操作很类似, 同样也有解包的flatmap, 注意上边的代码:
carOptional.map(s -> s.getInsurance());
其中的红色部分执行之后, 得到了是一个Option<Insurance>对象, 如果去掉后边的.get().getName()这部分, 当前的代码的类型其实是Optional<Optional<Insurance>>类型. 像极了之前的流中套流, 所以可以将其拆开, 让Optional<Optional<Insurance>>变成Option<Insurance>, 代码如下改造:
Optional<String> carToString = carOptional.flatMap(s -> s.getInsurance()).map(x -> x.getName());
通过Optional对象, 感觉flatMap的用法更清晰了. 其他一个小要点就是Optional对象无法序列化. 下边就来看上边遗留的问题, 就是解引用, 即从Optional对象中获取数据 map和flatMap的好处在于本身就已经使用了判断, 如果要操作的对象是一个空Optional, map和flatMap不会执行其中的lambda表达式参数. 因此可以将多个Optional判断是否为空, 直接改成map和flatMap的组合调用:
//像下边的先判断是否存在值, 可以修改成连续的调用, 并且用flatMap展开
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();
    }
}
//由于是两个Optional对象, 在使用的时候, car.map之后的得到一个Optional<Insurance>, 然后传入的函数将Person对象转换成Optional<Insurance>, 此时类型是Option<Optional<Insurance>>, 再使用flatMap展开成Optional<Insurance>:
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
    return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
既然可以看做单个元素的流, 自然也有filter方法了, 传递一个Predicate表达式, 然后根据表达式的结果, 保留内部的内容, 或者将其置空.

从Optional中取值

这一部分实际上和上一小节一样都是Optional类的API学习. 要安全的从Optional中获取数据, 先看看如下API:
  1. .get(), 最简单但不安全, 直接返回封装的变量, 否则抛出NoSuchElementException异常. 如果要使用这个方法, 需要搭配其他方法使用.
  2. .orElse(T other), 如果不存在就提供默认值other.
  3. .orElseGet(Supplier<? extends T> other), 如果不存在就执行其中的Supplier方法, 返回一个值.
  4. .orElseThrow(Supplier<? extends X> exceptionSupplier), 如果不存在抛出异常, 这个方法在于可以指定想抛出的异常.
  5. ifPresent(Consumer<? super T>), 如果存在就对其执行一个操作.
  6. isPresent(), 如果有值返回true, 如果是空就返回false.
使用这些方法, 就可以操作Optional对象了.来看几个实际的例子.

使用Optional 封装可能为null的值

由于很多类库的历史原因, 都会返回null值. 典型的就是Map系列集合中, 如果找不到键, 就会返回null值. 对于知道一定会存在null值的情况, 可以使用.ofNullable()进行封装, 比如:
Optional<Object> value = Optional.ofNullable(map.get("key"));

使用Optional 应对异常

类库中还有一些常用的工具方法, 比如 Integer.parseInt(String), 在遇到无法转换的时候, 肯定不能返回默认值, 而是抛出错误. 对于此类异常, 我们可以将其通过一个方法转换成Optional对象, 使用try-catch语句即可, 如果在代码的初期就编写这种方法, 就会很少遇到null对象:
public static Optional<Integer> stringToInt(String s) {
    try {
        return Optional.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

使用类型特化的Optional

OptionalInt、OptionalLong以及OptionalDouble都是存在的, 但这三个类型特化的类不支持map, flatMap 和 filter方法, 而且与Stream一样, 无法对其进行组合等操作. 在选择使用通用的还是特化类型的时候, 要考虑是不是需要对其中的数据进行处理, 还是仅仅用作防止空指针的对象来用, 再结合对性能的考虑来使用.
LICENSED UNDER CC BY-NC-SA 4.0
Comment