Java8笔记(4)

2020-01-27  本文已影响0人  Cool_Pomelo

Java8笔记(4)

默认方法

传统上,Java程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接
口中定义的每个方法提供一个实现,或者从父类中继承它的实现。但是,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题。现实情况是,现存的实体类往往不在接口设计者的控制范围之内,这些实体类为了适配新的接口约定也需要进行修改

Java 8为了解决这一问题引入了一种新的机制,通过两种方式可以完成这种操作。

默认方法的引入就是为了以兼容的方式解决像Java API这样的类库的演进问题的:

默认方法.png

简而言之,向接口添加方法是诸多问题的罪恶之源;一旦接口发生变化,实现这些接口的类
往往也需要更新,提供新添方法的实现才能适配接口的变化。如果你对接口以及它所有相关的实现有完全的控制,这可能不是个大问题。但是这种情况是极少的。这就是引入默认方法的目的:它让类可以自动地继承接口的一个默认实现

不断演进的 API

假设你是一个流行Java绘图库的设计者(为了说明本节的内容,我们做了这样的假想)。你的库中包含了一个 Resizable接口,它定义了一个简单的可缩放形状必须支持的很多方法, 比如: setHeight 、 setWidth 、getHeight 、 getWidth 以及 setAbsoluteSize 。此外,你还提供了几个额外的实现(out-of-boximplementation),如正方形、长方形。由于你的库非常流行,你的一些用户使用 Resizable 接口创建了他们自己感兴趣的实现,比如椭圆

发布API几个月之后,你突然意识到 Resizable 接口遗漏了一些功能。比如,如果接口提供
一个 setRelativeSize 方法,可以接受参数实现对形状的大小进行调整,那么接口的易用性会更好。你会说这看起来很容易啊:为 Resizable 接口添加 setRelativeSize 方法,再更新 Square和 Rectangle 的实现就好了。

不过,事情并非如此简单!你要考虑已经使用了你接口的用户,他们已经按照自身的需求实现了 Resizable 接口,他们该如何应对这样的变更呢?非常不幸,你无法访问,也无法改动他们实现了 Resizable 接口的类

初始版本的 API

Resizable 接口的最初版本提供了下面这些方法:


public interface Resizable {

    int getWidth();
    int getHeight();
    void setWidth(int width);
    void setHeight(int height);
    void setAbsoluteSize(int width, int height);


}

用户实现:


public class Ellipse implements Resizable{

    @Override
    public int getWidth() {
        return 0;
    }

    @Override
    public int getHeight() {
        return 0;
    }

    @Override
    public void setWidth(int width) {

    }

    @Override
    public void setHeight(int height) {

    }

    @Override
    public void setAbsoluteSize(int width, int height) {

    }
}

他实现了一个处理各种 Resizable 形状(包括 Ellipse )的游戏

public class Game {

    public static void main(String[] args) {

//        可以调整大小//的形状列表
        List<Resizable> resizableShapes =
                Arrays.asList(new Ellipse());
        Utils.paint(resizableShapes);

    }
}



public class Utils {

    public static void paint(List<Resizable> l){
        l.forEach(r -> {

//            调用每个形状自己的//setAbsoluteSize//方法
            r.setAbsoluteSize(42, 42);
//            r.draw();
        });
    }


}



第二版 API

库上线使用几个月之后,你收到很多请求,要求你更新 Resizable 的实现,让 Square 、
Rectangle 以及其他的形状都能支持 setRelativeSize 方法。为了满足这些新的需求,你发布了第二版API

public interface Resizable {

    int getWidth();
    int getHeight();
    void setWidth(int width);
    void setHeight(int height);
    void setAbsoluteSize(int width, int height);

    // 第二版API添加了一个新方法
    void setRelativeSize(int wFactor, int hFactor);

}



为 Resizable 接口添加新方法改进API。再次编译应用时会遭遇错误,因为它依赖的 Resizable 接口发生了变化

用户面临的窘境

对 Resizable 接口的更新导致了一系列的问题。首先,接口现在要求它所有的实现类添加
setRelativeSize 方法的实现。但是用户最初实现的 Ellipse 类并未包含setRelativeSize方法。向接口添加新方法是二进制兼容的,这意味着如果不重新编译该类,即使不实现新的方法,现有类的实现依旧可以运行

用户可能修改他的游戏,在他的 Utils.paint 方法中调用setRelativeSize 方法,因为 paint 方法接受一个 Resizable 对象列表作为参数。如果传递的是一个 Ellipse 对象,程序就会抛出一个运行时错误,因为它并未实现 setRelativeSize 方法

这就是默认方法试图解决的问题。它让类库的设计者放心地改进应用程序接口,无需担忧对
遗留代码的影响,这是因为实现更新接口的类现在会自动继承一个默认的方法实现

默认方法

默认方法是Java 8中引入的一个新特性,希望能借此以兼容的方式改进API。现在,接口包含的方法签名在它的实现类中也可以不提供实现。那么,谁来具体实现这些方法呢?实际上,缺失的方法实现会作为接口的一部分由实现类继承(所以命名为默认实现),而无需由实现类提供

默认方法由 default 修饰符修饰,并像类中声明的其他方法一样包含方法体

像下面这样在集合库中定义一个名为Sized 的接口,在其中定义一个抽象方法 size ,以及一个默认方法 isEmpty


public interface Sized {
    int size();

    default boolean isEmpty() {
            return size() == 0;
    }

}

这样任何一个实现了 Sized 接口的类都会自动继承 isEmpty 的实现。因此,向提供了默认实现的接口添加方法就不是源码兼容的

回顾一下最初的例子,为了以兼容的方式改进这个库(即使用该库的用户不需要修改他们实现了 Resizable 的类),可以使用默认方法,提供 setRelativeSize 的默认实现:


default void setRelativeSize(int wFactor, int hFactor){
        setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
}

Java 8中的抽象类和抽象接口

抽象类和抽象接口之间的区别是什么呢:

默认方法的使用模式

可选方法

类实现了接口,不过却刻意地将一些方法的实现留白。我们以Iterator 接口为例来说。 Iterator 接口定义了 hasNext 、 next ,还定义了 remove 方法。Java 8之前,由于用户通常不会使用该方法, remove 方法常被忽略。因此,实现 Interator 接口的类通常会为 remove 方法放置一个空的实现,这些都是些毫无用处的模板代码

采用默认方法之后,你可以为这种类型的方法提供一个默认的实现,这样实体类就无需在自
己的实现中显式地提供一个空方法

在Java 8中, Iterator 接口就为 remove 方法提供了一个默认实现,如下所示

interface Iterator<T> {
    boolean hasNext();
    T next();
    default void remove() {
            throw new UnsupportedOperationException();
    }
}


行为的多继承

默认方法让之前无法想象的事儿以一种优雅的方式得以实现,即行为的多继承。这是一种让类从多个来源重用代码的能力

Java的类只能继承单一的类,但是一个类可以实现多接口

如:

Java API中对 ArrayList 类的定义


public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable,
        Serializable, Iterable<E>, Collection<E> {
}

类型的多继承

ArrayList 继承了一个类,实现了六个接口。因此 ArrayList 实际是七个类型的直接子类,分别是: AbstractList 、 List 、 RandomAccess 、 Cloneable 、Serializable 、Iterable 和 Collection 。所以,在某种程度上,我们早就有了类型的多继承

利用正交方法的精简接口

假设你需要为你正在创建的游戏定义多个具有不同特质的形状。有的形状需要调整大小,但是不需要有旋转的功能;有的需要能旋转和移动,但是不需要调整大小

定义一个单独的 Rotatable 接口,并提供两个抽象方法 setRotationAngle 和
getRotationAngle


public interface Rotatable {

    void setRotationAngle(int angleInDegrees);


    int getRotationAngle();


    default void rotateBy(int angleInDegrees){
        setRotationAngle((getRotationAngle () + angleInDegrees) % 360);
    }


}

实现了 Rotatable 的所有类都需要提供 setRotationAngle 和 getRotationAngle的实现,但与此同时它们也会天然地继承 rotateBy 的默认实现

public interface Moveable {

    int getX();

    int getY();

    void setX(int x);

    void setY(int y);

    default void moveHorizontally(int distance){
        setX(getX() + distance);
    }

    default void moveVertically(int distance){
        setY(getY() + distance);
    }

}


public interface Resizable {

    int getWidth();

    int getHeight();

    void setWidth(int width);

    void setHeight(int height);

    void setAbsoluteSize(int width, int height);

    default void setRelativeSize(int wFactor, int hFactor){
        setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
    }


}



组合接口

//Monster 类会自动继承 Rotatable 、 Moveable 和 Resizable 接口的默认方法
public class Monster implements Rotatable,Moveable,Resizable{

    @Override
    public int getX() {
        return 0;
    }

    @Override
    public int getY() {
        return 0;
    }

    @Override
    public void setX(int x) {

    }

    @Override
    public void setY(int y) {

    }

    @Override
    public int getWidth() {
        return 0;
    }

    @Override
    public int getHeight() {
        return 0;
    }

    @Override
    public void setWidth(int width) {

    }

    @Override
    public void setHeight(int height) {

    }

    @Override
    public void setAbsoluteSize(int width, int height) {

    }

    @Override
    public void setRotationAngle(int angleInDegrees) {

    }

    @Override
    public int getRotationAngle() {
        return 0;
    }
}

使用:


 public static void main(String[] args) {

        Monster monster = new Monster();

        monster.rotateBy(180);

        monster.moveVertically(10);

    }


//需要给出所有抽象方 法的实现,但无需重 复实现默认方法
public class Sun implements Moveable,Rotatable{

    @Override
    public int getX() {
        return 0;
    }

    @Override
    public int getY() {
        return 0;
    }

    @Override
    public void setX(int x) {

    }

    @Override
    public void setY(int y) {

    }

    @Override
    public void setRotationAngle(int angleInDegrees) {

    }

    @Override
    public int getRotationAngle() {
        return 0;
    }
}

假设你需要修改moveVertically 的实现,让它更高效地运行。你可以在 Moveable 接口内直接修改它的实现,所有实现该接口的类会自动继承新的代码(这里我们假设用户并未定义自己的方法实现)

解决冲突

随着默认方法在Java 8中引入,有可能出现一个类继承了多个方法而它们使用的却是同样的函数签名。这种情况下,类会选择使用哪一个函数?

例子:


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


解决问题的三条规则

如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条
规则可以进行判断:

选择提供了最具体实现的默认方法的接口

上面的例子 C 类同时实现了 B 接口和 A 接口,而这两个接口恰巧又都定义了名为 hello 的默认方法, B 继承自 A

编译器会使用声明的哪一个 hello 方法呢?按照规则(2),应该选择的是提供了最具体实现的默认方法的接口。由于 B 比 A 更具体,所以应该选择 B 的 hello 方法。所以,程序会打印输出“Hellofrom B”

如果 C 像下面这样继承自 D ,会发生什么情况


public class D implements A {
}





public class C extends D implements B, A {

    public static void main(String[] args) {

        new C().hello();
    }
}

依据规则(1),类中声明的方法具有更高的优先级。 D 并未覆盖 hello 方法,可是它实现了接口 A 。所以它就拥有了接口 A 的默认方法。规则(2)说如果类或者父类没有对应的方法,那么就应该选择提供了最具体实现的接口中的方法。因此,编译器会在接口 A 和接口 B 的 hello 方法之间做选择。由于 B 更加具体,所以程序会再次打印输出“Hello from B”

再比如:

D 现在显式地覆盖了从 A 接口中继承的 hello 方法


public class D implements A {

    public void hello(){
        System.out.println("Hello from D");
    }
}

public class C extends D implements B, A {

    public static void main(String[] args) {

        new C().hello();

    }
}

由于依据规则(1),父类中声明的方法具有更高的优先级,所以程序会打印输出“Hellofrom D”

冲突及如何显式地消除歧义

假设 B不再继承 A


public interface A {

    default void hello(){
        System.out.println("Hello from A");
    }

}

public interface B {

    default void hello(){
        System.out.println("Hello from B");
    }
}

public class C implements B,A{

    @Override
    public void hello() {

    }
}

这时规则(2)就无法进行判断了,因为从编译器的角度看没有哪一个接口的实现更加具体两个都差不多

A 接口和 B 接口的 hello 方法都是有效的选项。所以,Java编译器这时就会抛出一个
编译错误,因为它无法判断哪一个方法更合适

冲突的解决

解决这种两个可能的有效方法之间的冲突,没有太多方案;你只能显式地决定你希望在 C 中使用哪一个方法。为了达到这个目的,你可以覆盖类 C 中的 hello 方法,在它的方法体内显式地调用你希望调用的方法

Java 8中引入了一种新的语法 X.super.m(…) ,其中 X 是你希望调用的 m方法所在的父接口

如果你希望 C 使用来自于 B 的默认方法,它的调用方式看起来就如下所示


public class CC implements B,A{

    @Override
    public void hello() {
        B.super.hello();
    }
}

几乎完全一样的函数签名
public interface A {

    default Number get(){
        return 10;
    }
}


public interface B {

    default Integer get(){
        return 55;
    }
}

public class C implements B,A{

    public static void main(String[] args) {

        System.out.println(new C().get());
    }
}

类 C 无法判断 A 或者 B 到底哪一个更加具体。这就是类 C 无法通过编译的原因

菱形继承问题


public interface A {

    default void hello(){
        System.out.println("Hello from A");
    }
}

public interface B extends A{
}


public interface C extends A{
}




public class D implements B,C{

    public static void main(String[] args) {
        new D().hello();
    }
}

这种问题叫“菱形问题”,因为类的继承关系图形状像菱形。这种情况下类 D 中的默认方法到底继承自什么地方 ——源自 B 的默认方法,还是源自 C 的默认方法?实际上只有一个方法声明可以选择。只有 A 声明了一个默认方法。由于这个接口是 D 的父接口,代码会打印输出“Hello from A”

现在,我们看看另一种情况,如果 B 中也提供了一个默认的 hello 方法,并且函数签名跟 A中的方法也完全一致,这时会发生什么情况呢?根据规则(2),编译器会选择提供了更具体实现的接口中的方法。由于 B 比 A 更加具体,所以编译器会选择 B 中声明的默认方法。如果 B 和 C 都使用相同的函数签名声明了 hello 方法,就会出现冲突,正如我们之前所介绍的,你需要显式地指定使用哪个方法

上一篇下一篇

猜你喜欢

热点阅读