默认方法

1.简述

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

在Java8中为了解决这个问题引入了一种新的机制。Java8中的接口现在支持在声明方法的同时提供实现。有两种方式可以完成这种操作。其一,Java8允许在接口内声明静态方法。其二,Java8引入了一个新功能,叫默认方法。通过默认方法,即使实现接口的方法也可以自动继承默认的实现,你可以让你的接口可以平滑地进行接口的进化和演进。比如我们的List接口中的sort方法是java8中全新的方法,定义如下:

default void sort(Comparator c){
    Collections.sort(this, c);
}

在方法有个default修饰符用来表示这是默认方法。

2.进化的API

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

发布API几个月之后,你突然意识到Resizable接口遗漏了一些功能。比如,如果接口提供一个setRelativeSize方法,可以接受参数实现对形状的大小进行调整,那么接口的易用性会更好。你会说这看起来很容易啊:为Resizable接口添加setRelativeSize方法,再更新Square和Rectangle的实现就好了。不过,事情并非如此简单!你要考虑已经使用了你接口的用户,他们已经按照自身的需求实现了Resizable接口,他们该如何应对这样的变更呢?非常不幸,你无法访问,也无法改动他们实现了Resizable接口的类。这也是Java库的设计者需要改进JavaAPI时面对的问题。让我们以一个具体的实例为例,深入探讨修改一个已发布接口的种种后果。

2.1初始化版本的API

Resizable最开始的版本如下:

public interface Resizable{
    int getWidth();
    int getHeight();
    void setWidth(int width);
    void setHeight(int height);
    void setAbsoluteSize(int width, int height);
}

这时候有一位用户实现了你的Resizable接口,创建了Ellipse类:

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) {

    }
}

2.2第二版本API

库上线使用几个月之后,你收到很多请求,要求你更新Resizable的实现,所以你更新了一个方法。

public interface Resizable{
    int getWidth();
    int getHeight();
    void setWidth(int width);
    void setHeight(int height);
    void setAbsoluteSize(int width, int height);
    void setRelativeSize(int wFactor, int hFactor);//第二版本API
}

接下来用户便会面临很多问题。首先,接口现在要求它所有的实现类添加setRelativeSize方法的实现。但我们刚才的用户最初实现的Ellipse类并未包含setRelativeSize方法。向接口添加新方法是二进制兼容的,这意味着如果不重新编译该类,即使不实现新的方法,现有类的实现依旧可以运行。但是这种情况少之又少,基本项目每次发布时都会重新编译,所以必定会报错。

最后,更新已发布API会导致后向兼容性问题。这就是为什么对现存API的演进,比如官方发布的Java.Collection.API,会给用户带来麻烦。当然,还有其他方式能够实现对API的改进,但是都不是明智的选择。比如,你可以为你的API创建不同的发布版本,同时维护老版本和新版本,但这是非常费时费力的,原因如下。其一,这增加了你作为类库的设计者维护类库的复杂度。其次,类库的用户不得不同时使用一套代码的两个版本,而这会增大内存的消耗,延长程序的载入时间,因为这种方式下项目使用的类文件数量更多了。

这就是我们默认方法所要做的工作。它让我们的类库设计者放心地改进应用程序接口,无需担忧对遗留代码的影响。

3.详解默认方法

经过前述的介绍,我们已经了解了向已发布的API添加方法,会对我们现存的代码会造成多大的危害。默认方法是Java8中引入的一个新特性,依靠他我们可以在实现类中不用提供实现。
我们要使用我们的默认方法非常简单,只需要在我们要实现的方法签名前面添加default修饰符进行修饰,并像类中声明的其他方法一样包含方法体。如下面的接口一样:

public interface Sized {
    int size();
    default boolean isEmpty(){
        return size() == 0;
    }
}

这样任何一个实现了Sized接口的类都会自动继承isEmpty的实现。

3.1默认方法的使用模式

3.1.1可选方法

你有时候会碰到这种情况,类实现了接口,不过却可以将一些方法的实现留白。比如我们Iterator接口,我们一般不会去实现remove方法,经常实现都会留白,在Java8中为了解决这种办法回味我们的remove方法添加默认的实现,如下:

public interface Iterator {
    
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

通过这种方式,我们可以减少无效的模板代码。实现Iterator接口的每一个类都不需要再次实现remove的模板方法了。

3.1.2多继承

默认方法让之前的Java是不支持多继承,但是默认方法的出现让多继承在java中变得可能了。

Java的类只能继承单一的类,但是一个类可以实现多接口。要确认也很简单,下面是Java API中对ArrayList类的定义:

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

3.1.3冲突问题

我们知道Java语言中一个类只能继承一个父类,但是一个类可以实现多个接口。随着默认方法在Java8中引入,有可能出现一个类继承了多个方法而它们使用的却是同样的函数签名。这种情况下,类会选择使用哪一个函数?在实际情况中,虽然这样的冲突很难发生,但是一旦发生,就必须要规定一套约定来处理这些冲突。这一节中,我们会介绍Java编译器如何解决这种潜在的冲突。

public interface A {
    default void hello(){
        System.out.println("i am A");
    }
}
interface B extends A{
    default void hello(){
        System.out.println("i am B");
    }
}
class C implements A,B{
    public static void main(String[] args) {
        new C().hello();
    }
}

上面的代码会输出i am B。为什么呢?我们下面有三个规则:

  1. 类中的方法优先级最高。类或父类中的声明的方法的优先级高于任何声明为默认方法的优先级。
  2. 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口。如果B继承了A,那么B就比A的更具体。
  3. 最后,如果还是无法判断,继承了多个接口的类必须通过显示覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。

接下来举几个例子

public interface A {
    default void hello(){
        System.out.println("i am A");
    }
}
interface B extends A{
    default void hello(){
        System.out.println("i am B");
    }
}
class D implements A{
    public void hello(){
        System.out.println("i am D");
    }
}
class C extends D implements A,B{
    public static void main(String[] args) {
        new C().hello();
    }
}

上面会输出D,遵循我们的第一条原则,类中的方法优先级最高。

public interface A {
    default void hello(){
        System.out.println("i am A");
    }
}
interface B {
    default void hello(){
        System.out.println("i am B");
    }
}

class C implements A,B{
    public static void main(String[] args) {
        new C().hello();
    }
}

上面代码会出现编译错误:Error:(19, 1) java: 类 java8.C从类型 java8.A 和 java8.B 中继承了hello() 的不相关默认值,这个时候必须利用第三条,显式得去调用父类的接口:

class C implements A,B{
    public void hello(){
        B.super.hello();
    }
    public static void main(String[] args) {
        new C().hello();
    }
}

你可能感兴趣的:(默认方法)