Java 8 接口里的默认方法特性

本文由码农网 –
civic5216原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划!

1.简述

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

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

default void sort(Comparator<? super E> c){
    Collections.sort(this, c);
}

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

这篇文章我们将要探讨Java 8中接口里的默认方法特性。Java8指出“默认方法使得新功能被添加到库中的接口里面,同时又能保证与这些接口老版本代码的二进制兼容性。”

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时面对的问题。让我们以一个具体的实例为例,深入探讨修改一个已发布接口的种种后果。

这些年Java进化升级了很多,在Java库中引入的接口需要添加新的功能。在没有默认方法特性时,当你往接口中添加新方法时,接口内部所有实现的类都要历经一些修改。这将导致上千行的代码修改工作量。为了避免这点,Java
8引入了默认对象方法。亦即,如果你想要往现存的接口中添加任何功能,你只需添加默认方法特性而不会影响接口实现形式。

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

    }
}

让我们看一些例子来更好的理解它。例如,我声明了一个具有打开和读取功能的接口“BookIterface”。接口的类需要实现打开和读取方法。

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创建不同的发布版本,同时维护老版本和新版本,但这是非常费时费力的,原因如下。其一,这增加了你作为类库的设计者维护类库的复杂度。其次,类库的用户不得不同时使用一套代码的两个版本,而这会增大内存的消耗,延长程序的载入时间,因为这种方式下项目使用的类文件数量更多了。

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

package org.smarttechie;
/**
* The interface is intended to open and read. The implementors should implement the methods to open and read.
* @author Siva Prasad Rao Janapati
*
*/
public interface BookInterface {
/**
* The method opens the book
*/
public void openTheBook();
/**
* The method reads the book
*/
public void readTheBook();
}

3.详解默认方法

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

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

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

现在,我们提供上面接口的实现代码。

3.1默认方法的使用模式

package org.smarttechie;
/**
* The JavaBookImpl is the implementation of BookInterface
* @author Siva Prasad Rao Janapati
*
*/
public class JavaBookImpl implements BookInterface {
/**
* This opens the book
*/
@Override
public void openTheBook() {
System.out.println("The Java book is opened");
}

/**
* This reads the book
*/
@Override
public void readTheBook() {
System.out.println("Reading the Java book");
 }
}

3.1.1可选方法

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

public interface Iterator<E> {

    boolean hasNext();

    E next();

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

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

现在,我们想要给接口提供一个关闭功能。如果你直接添加关闭功能到book接口中,现存的实现类需要历经一些修改。有了默认方法特性后,我们能给book接口直接添加关闭功能。默认方法对所有实现都可用。

3.1.2多继承

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

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

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable,
Serializable, Iterable<E>, Collection<E> {
}
package org.smarttechie;
/**
* The interface is intended to open and read. The implementors should implement the methods to open and read.
* @author Siva Prasad Rao Janapati
*
*/
public interface BookInterface {
/**
* The method opens the book
*/
public void openTheBook();
/**
* The method reads the book
*/
public void readTheBook();
/**
* The default method implementation
*/
public default void closeTheBook() {
System.out.println("Closting the book");
 }
}

package org.smarttechie;
/**
 * The JavaBookImpl is the implementation of BookInterface
 * @author Siva Prasad Rao Janapati
 *
 */
public class JavaBookImpl implements BookInterface {
 /**
 * This opens the book
 */
 @Override
 public void openTheBook() {
 System.out.println("The Java book is opened");
 }
 /**
 * This reads the book
 */
 @Override
 public void readTheBook() {
 System.out.println("Reading the Java book");
 }
 public static void main (String[] args) {
 BookInterface bookInter = new JavaBookImpl();
 //Call the default method declared in BookInterface
 bookInter.closeTheBook();
 JavaBookImpl book = new JavaBookImpl();
 book.closeTheBook();
 }
}

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

下面给出了上述调用方法的字节码。从字节码中,我们可以认为默认方法是一种“虚方法”。