澳门新葡萄京官网首页 3

Java 8 默认方法(Default Methods)

Java 8支持default方法,本文也主要来讲解一下Java
8中default方法的使用方法,比较详细。

本文由 ImportNew –
miracle1919 翻译自
javacodegeeks。欢迎加入翻译小组。转载请见文末要求。

转载自http://ebnbin.com/2015/12/20/java-8-default-methods/

Java 8 引入了新的语言特性——默认方法(Default Methods)。
Default
methods
enable new functionality to be added to the interfaces of libraries
and ensure binary compatibility with code written for older versions
of those interfaces.

什么是default方法?

Java
8发布以后,可以给接口添加新方法,但是,接口仍然可以和它的实现类保持兼容。这非常重要,因为你开发的类库可能正在被多个开发者广泛的使用着。而Java
8之前,在类库中发布了一个接口以后,如果在接口中添加一个新方法,那些实现了这个接口的应用使用新版本的接口就会有崩溃的危险。

有了Java 8,是不是就没有这种危险了?答案是否定的。

给接口添加default方法可能会让某些实现类不可用。

首先,让我们看下default方法的细节。

在Java
8中,接口中的方法可以被实现(Java8中的static的方法也可以在接口中实现,但这是另一个话题)。接口中被实现的方法叫做default方法,用关键字default作为修饰符来标识。当一个类实现一个接口的时候,它可以实现已经在接口中被实现过的方法,但这不是必须的。这个类会继承default方法。这就是为什么当接口发生改变的时候,实现类不需要做改动的原因。

什么是default方法?

默认方法允许您添加新的功能到现有库的接口中,并能确保与采用旧版本接口编写的代码的二进制兼容性。

多继承的时候呢?

当一个类实现了多于一个(比如两个)接口,而这些接口又有同样的default方法的时候,事情就变得很复杂了。类继承的是哪一个default方法呢?哪一个也不是!在这种情况下,类要自己(直接或者是继承树上更上层的类)来实现default方法(才可以)。

当一个接口实现了default方法,另一个接口把default方法声明成了abstract的时候,同样如此。Java
8试图避免不明确的东西,保持严谨。如果一个方法在多个接口中都有声明,那么,任何一个default实现都不会被继承,你将会得到一个编译时错误。

但是,如果你已经把你的类编译过了,那就不会出现编译时错误了。在这一点上,Java
8是不一致的。它有它自己的原因,有于各种原因,在这里我不想详细的说明或者是深入的讨论(因为:版本已经发布了,讨论时间太长,这个平台从来没有这样的讨论)。

  • 假如你有两个接口,一个实现类。
  • 其中一个接口实现了一个default方法m()。
  • 把接口和实现类一块编译。
  • 修改那个没有包含m()方法的接口,声明m()方法为abstract。
  • 单独重新编译修改过的接口。
  • 运行实现类。

澳门新葡萄京官网首页 1

上面的情况下类可以正常运行。但是,不能用修改过的接口重新编译,但是用老的接口编译仍然可以运行。接下来

  • 修改那个含有abstract方法m()的接口,创建一个default实现。
  • 编译修改后的接口
  • 运行类:失败。

当两个接口给同一个方法都提供了default实现的时候,这个方法是无法被调用的,除非实现类也实现了这个default方法(要么是直接实现,要么是继承树上更上层的类做实现)。

澳门新葡萄京官网首页 2
但是,这个类是兼容的。它可以在使用新接口的情况下被载入,甚至可以执行,只要它没有调用在两个接口中都有default实现的方法。

实例代码:

澳门新葡萄京官网首页 3

为了演示上面的例子,我给C.java创建了一个测试目录,它下面还有3个子目录,用于存放I1.java和I2.java。测试目录下包含了类C的源码C.java。base目录包含了可以编译和运行的那个版本的接口。I1包含了有default实现的m()方法,I2不包含任何方法。

实现类包含了main方法,所以我们可以在测试中执行它。它会检查是否存在命令行参数,这样,我们就可以很方便的执行调用m()和不调用m()的测试。

~/github/test$ cat C.java
public class C implements I1, I2 {
  public static void main(String[] args) {
    C c = new C();
    if(args.length == 0 ){
        c.m();
    }
  }
}
~/github/test$ cat base/I1.java
public interface I1 {
  default void m(){
    System.out.println("hello interface 1");
  }
}
~/github/test$ cat base/I2.java
public interface I2 {
}

使用下面的命令行来编译运行:

~/github/test$ javac -cp .:base C.java
~/github/test$ java -cp .:base C
hello interface 1

compatible目录包含了有abstract方法m()的I2接口,和未修改的I1接口。

~/github/test$ cat compatible/I2.java
public interface I2 {
  void m();
}

这个不能用来编译类C:

~/github/test$ javac -cp .:compatible C.java
C.java:1: error: C is not abstract and does not override abstract method m() in I2
public class C implements I1, I2 {
   ^
1 error

澳门新葡萄京官网首页,错误信息非常精确。因为我们有前一次编译获得的C.class,如果我们编译compatible目录下的接口,我们仍然会得到能运行实现类的两个接口:

~/github/test$ javac compatible/I*.java
~/github/test$ java -cp .:compatible C
hello interface 1

第三个叫做wrong的目录,包含的I2接口也定义了m()方法:

~/github/test$ cat wrong/I2.java
public interface I2 {
  default void m(){
    System.out.println("hello interface 2");
  }
}

我们应该不厌其烦的编译它。尽管m()方法被定义了两次,但是,实现类仍然可以运行,只要它没有调用那个定义了多次的方法,但是,只要我们调用m()方法,立即就会失败。这是我们使用的命令行参数:

~/github/test$ javac wrong/*.java
~/github/test$ java -cp .:wrong C
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting
default methods: I1.m I2.m
    at C.m(C.java)
    at C.main(C.java:5)
~/github/test$ java -cp .:wrong C x
~/github/test$

Java
8发布以后,可以给接口添加新方法,但是,接口仍然可以和它的实现类保持兼容。这非常重要,因为你开发的类库可能正在被多个开发者广泛的使用着。而Java
8之前,在类库中发布了一个接口以后,如果在接口中添加一个新方法,那些实现了这个接口的应用使用新版本的接口就会有崩溃的危险。

默认方法是在接口中的方法签名前加上了 default
关键字的实现方法。

结论

当你把给接口添加了default实现的类库移植到Java
8环境下的时候,一般不会有问题。至少Java8类库开发者给集合类添加default方法的时候就是这么想的。使用你类库的应用程序仍然依赖没有default方法的Java7的类库。当使用和修改多个不同的类库的时候,有很小的几率会发生冲突。如何才能避免呢?

像以前那样设计你的类库。可能依赖default方法的时候不要掉以轻心。万不得已不要使用。明智的选择方法名,避免和其它接口产生冲突。我们将会学习到Java编程中如何使用这个特性做开发。

有了Java 8,是不是就没有这种危险了?答案是否定的。

一个简单的例子

interface InterfaceA { 
    default void foo() { 
        System.out.println("InterfaceA foo"); 
    }
}
class ClassA implements InterfaceA {}
public class Test { 
    public static void main(String[] args) { 
         new ClassA().foo(); // 打印:“InterfaceA foo” 
    }
}

ClassA
类并没有实现 InterfaceA接口中的 foo 方法,InterfaceA 接口中提供了
foo 方法的默认实现,因此可以直接调用 ClassA 类的 foo方法。

给接口添加default方法可能会让某些实现类不可用。

为什么要有默认方法

在 java 8 之前,接口与其实现类之间的 耦合度 太高了(tightly
coupled
),当需要为一个接口添加方法时,所有的实现类都必须随之修改。默认方法解决了这个问题,它可以为接口添加新的方法,而不会破坏已有的接口的实现。这在
lambda 表达式作为 java 8
语言的重要特性而出现之际,为升级旧接口且保持向后兼容(backward
compatibility)提供了途径。

String[] array = new String[] {
        "hello",
        ", ",
        "world",
};
List<String> list = Arrays.asList(array);
list.forEach(System.out::println); // 这是 jdk 1.8 新增的接口默认方法

这个 forEach方法是 jdk 1.8
新增的接口默认方法,正是因为有了默认方法的引入,才不会因为
Iterable接口中添加了forEach方法就需要修改所有 Iterable
接口的实现类。
下面的代码展示了 jdk 1.8 的Iterable 接口中的forEach
默认方法:

package java.lang;

import java.util.Objects;
import java.util.function.Consumer;

public interface Iterable<T> {
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

首先,让我们看下default方法的细节。

默认方法的继承

和其它方法一样,接口默认方法也可以被继承。

interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}

interface InterfaceB extends InterfaceA {
}

interface InterfaceC extends InterfaceA {
    @Override
    default void foo() {
        System.out.println("InterfaceC foo");
    }
}

interface InterfaceD extends InterfaceA {
    @Override
    void foo();
}

public class Test {
    public static void main(String[] args) {
        new InterfaceB() {}.foo(); // 打印:“InterfaceA foo”
        new InterfaceC() {}.foo(); // 打印:“InterfaceC foo”
        new InterfaceD() {
            @Override
            public void foo() {
                System.out.println("InterfaceD foo");
            }
        }.foo(); // 打印:“InterfaceD foo”

        // 或者使用 lambda 表达式
        ((InterfaceD) () -> System.out.println("InterfaceD foo")).foo();
    }
}

接口默认方法的继承分三种情况(分别对应上面的InterfaceB接口,InterfaceC接口和
InterfaceD 接口):

  • 不覆写默认方法,直接从父接口中获取方法的默认实现。
  • 覆写默认方法,这跟类与类之间的覆写规则相类似。
  • 覆写默认方法并将它重新声明为抽象方法,这样新接口的子类必须再次覆写并实现这个抽象方法。

在Java
8中,接口中的方法可以被实现(Java8中的static的方法也可以在接口中实现,但这是另一个话题)。接口中被实现的方法叫做default方法,用关键字default作为修饰符来标识。当一个类实现一个接口的时候,它可以实现已经在接口中被实现过的方法,但这不是必须的。这个类会继承default方法。这就是为什么当接口发生改变的时候,实现类不需要做改动的原因。

默认方法的多继承

Java
使用的是单继承、多实现的机制,为的是避免多继承带来的调用歧义的问题。当接口的子类同时拥有具有相同签名的方法时,就需要考虑一种解决冲突的方案。

interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}

interface InterfaceB {
    default void bar() {
        System.out.println("InterfaceB bar");
    }
}

interface InterfaceC {
    default void foo() {
        System.out.println("InterfaceC foo");
    }

    default void bar() {
        System.out.println("InterfaceC bar");
    }
}

class ClassA implements InterfaceA, InterfaceB {
}

// 错误
//class ClassB implements InterfaceB, InterfaceC {
//}

class ClassB implements InterfaceB, InterfaceC {
    @Override
    public void bar() {
        InterfaceB.super.bar(); // 调用 InterfaceB 的 bar 方法
        InterfaceC.super.bar(); // 调用 InterfaceC 的 bar 方法
        System.out.println("ClassB bar"); // 做其他的事
    }
}

ClassA
类中,它实现的InterfaceA接口和InterfaceB接口中的方法不存在歧义,可以直接多实现。
ClassB类中,它实现的 InterfaceB接口和 InterfaceC
接口中都存在相同签名的
foo方法,需要手动解决冲突。覆写存在歧义的方法,并可以使用
InterfaceName.super.methodName()的方式手动调用需要的接口默认方法。

多继承的时候呢?

接口继承行为发生冲突时的解决规则

值得注意的是这么一种情况:

interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}

interface InterfaceB extends InterfaceA {
    @Override
    default void foo() {
        System.out.println("InterfaceB foo");
    }
}

// 正确
class ClassA implements InterfaceA, InterfaceB {
}

class ClassB implements InterfaceA, InterfaceB {
    @Override
    public void foo() {
//        InterfaceA.super.foo(); // 错误
        InterfaceB.super.foo();
    }
}

ClassA
类多实现InterfaceA接口和InterfaceB接口时,不会出现方法名歧义的错误。当
ClassB类覆写foo方法时,无法通过 InterfaceA.super.foo(); 调用
InterfaceA 接口的 foo方法。
因为InterfaceB接口继承了 InterfaceA接口,那么 InterfaceB
接口一定包含了所有
InterfaceA接口中的字段方法,因此一个同时实现了InterfaceA接口和
InterfaceB接口的类与一个只实现了InterfaceB
接口的类完全等价。
这很好理解,就相当于class SimpleDateFormat extends DateFormat
class SimpleDateFormat extends DateFormat, Object等价(如果允许多继承)。
或者换种方式理解:

class ClassC {
    public void foo() {
        System.out.println("ClassC foo");
    }
}

class ClassD extends ClassC {
    @Override
    public void foo() {
        System.out.println("ClassD foo");
    }
}

public class Test {
    public static void main(String[] args) {
        ClassC classC = new ClassD();
        classC.foo(); // 打印:“ClassD foo”
    }
}

这里的 classC.foo();同样调用的是
ClassD类中的foo方法,打印结果为“ClassD foo”,因为 ClassC类中的
foo 方法在 ClassD类中被覆写了。
在上面的
ClassA类中不会出现方法名歧义的原因是所谓“存在歧义”的方法其实都来自于InterfaceA接口,InterfaceB接口中的“同名方法”只是继承自
InterfaceA 接口而来并对其进行了覆写。
ClassA类实现的两个接口不是两个毫不相干的接口,因此不存在同名歧义方法。而覆写意味着对父类方法的屏蔽,这也是
Override的设计意图之一。因此在实现了
InterfaceB接口的类中无法访问已被覆写的InterfaceA
接口中的 foo方法。
这是当接口继承行为发生冲突时的规则之一,即
被其它类型所覆盖的方法会被忽略
如果想要调用 InterfaceA接口中的
foo方法,只能通过自定义一个新的接口同样继承InterfaceA接口并显示地覆写foo方法,在方法中使用InterfaceA.super.foo();
调用 InterfaceA接口的 foo方法,最后让实现类同时实现
InterfaceB接口和自定义的新接口,代码如下:

interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}

interface InterfaceB extends InterfaceA {
    @Override
    default void foo() {
        System.out.println("InterfaceB foo");
    }
}

interface InterfaceC extends InterfaceA {
    @Override
    default void foo() {
        InterfaceA.super.foo();
    }
}

class ClassA implements InterfaceB, InterfaceC {
    @Override
    public void foo() {
        InterfaceB.super.foo();
        InterfaceC.super.foo();
    }
}

注意!
虽然InterfaceC接口的foo方法只是调用了一下父接口的默认实现方法,但是这个覆写
不能省略,否则 InterfaceC接口中继承自
InterfaceA接口的隐式的foo方法同样会被认为是被
InterfaceB接口覆写了而被屏蔽,会导致调用 InterfaceC.super.foo()
时出错。
通过这个例子,应该注意到在使用一个默认方法前,一定要考虑它是否真的需要。因为
默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。滥用默认方法可能给代码带来意想不到、莫名其妙的错误。

当一个类实现了多于一个(比如两个)接口,而这些接口又有同样的default方法的时候,事情就变得很复杂了。类继承的是哪一个default方法呢?哪一个也不是!在这种情况下,类要自己(直接或者是继承树上更上层的类)来实现default方法(才可以)。

接口与抽象类

当接口继承行为发生冲突时的另一个规则是,类的方法声明优先于接口默认方法,无论该方法是具体的还是抽象的

interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }

    default void bar() {
        System.out.println("InterfaceA bar");
    }
}

abstract class AbstractClassA {
    public abstract void foo();

    public void bar() {
        System.out.println("AbstractClassA bar");
    }
}

class ClassA extends AbstractClassA implements InterfaceA {
    @Override
    public void foo() {
        InterfaceA.super.foo();
    }
}

public class Test {
    public static void main(String[] args) {
        ClassA classA = new ClassA();
        classA.foo(); // 打印:“InterfaceA foo”
        classA.bar(); // 打印:“AbstractClassA bar”
    }
}

ClassA
类中并不需要手动覆写bar方法,因为优先考虑到 ClassA类继承了的
AbstractClassA抽象类中存在对bar方法的实现,同样的因为
AbstractClassA抽象类中的foo方法是抽象的,所以在ClassA类中必须实现
foo方法。
虽然 Java 8
的接口的默认方法就像抽象类,能提供方法的实现,但是他们俩仍然是
不可相互代替的:接口可以被类多实现(被其他接口多继承),抽象类只能被单继承。

接口中没有 this
指针,没有构造函数,不能拥有实例字段(实例变量)或实例方法,无法保存
状态state),抽象方法中可以。

抽象类不能在 java 8 的 lambda 表达式中使用。

从设计理念上,接口反映的是 “like-a” 关系,抽象类反映的是 “is-a”
关系。

当一个接口实现了default方法,另一个接口把default方法声明成了abstract的时候,同样如此。Java
8试图避免不明确的东西,保持严谨。如果一个方法在多个接口中都有声明,那么,任何一个default实现都不会被继承,你将会得到一个编译时错误。

接口静态方法

除了默认方法,Java 8 还在允许在接口中定义静态方法。

interface InterfaceA {
    default void foo() {
        printHelloWorld();
    }

    static void printHelloWorld() {
        System.out.println("hello, world");
    }
}

public class Test {
    public static void main(String[] args) {
        InterfaceA.printHelloWorld(); // 打印:“hello, world”
    }
}

但是,如果你已经把你的类编译过了,那就不会出现编译时错误了。在这一点上,Java
8是不一致的。它有它自己的原因,有于各种原因,在这里我不想详细的说明或者是深入的讨论(因为:版本已经发布了,讨论时间太长,这个平台从来没有这样的讨论)。

其他注意点

default关键字只能在接口中使用(以及用在 switch 语句的
default分支),不能用在抽象类中。
接口默认方法不能覆写Object类的 equalshashCodetoString
方法。

接口中的静态方法必须是 public 的,public修饰符可以省略,static
修饰符不能省略。

即使使用了 java 8 的环境,一些 IDE
仍然可能在一些代码的实时编译提示时出现异常的提示(例如无法发现 java 8
的语法错误),因此不要过度依赖 IDE。

  • 假如你有两个接口,一个实现类。
  • 其中一个接口实现了一个default方法m()。
  • 把接口和实现类一块编译。
  • 修改那个没有包含m()方法的接口,声明m()方法为abstract。
  • 单独重新编译修改过的接口。
  • 运行实现类。

澳门新葡萄京官网首页 4

上面的情况下类可以正常运行。但是,不能用修改过的接口重新编译,但是用老的接口编译仍然可以运行。接下来

  • 修改那个含有abstract方法m()的接口,创建一个default实现。
  • 编译修改后的接口
  • 运行类:失败。

当两个接口给同一个方法都提供了default实现的时候,这个方法是无法被调用的,除非实现类也实现了这个default方法(要么是直接实现,要么是继承树上更上层的类做实现)。

澳门新葡萄京官网首页 5

但是,这个类是兼容的。它可以在使用新接口的情况下被载入,甚至可以执行,只要它没有调用在两个接口中都有default实现的方法。

实例代码

澳门新葡萄京官网首页 6

为了演示上面的例子,我给C.java创建了一个测试目录,它下面还有3个子目录,用于存放I1.java和I2.java。测试目录下包含了类C的源码C.java。base目录包含了可以编译和运行的那个版本的接口。I1包含了有default实现的m()方法,I2不包含任何方法。

实现类包含了main方法,所以我们可以在测试中执行它。它会检查是否存在命令行参数,这样,我们就可以很方便的执行调用m()和不调用m()的测试。

    ~/github/test$ cat C.java
    public class C implements I1, I2 {
      public static void main(String[] args) {
        C c = new C();
        if(args.length == 0 ){
            c.m();
        }
      }
    }
    ~/github/test$ cat base/I1.java
    public interface I1 {
      default void m(){
        System.out.println("hello interface 1");
      }
    }
    ~/github/test$ cat base/I2.java
    public interface I2 {
    }

使用下面的命令行来编译运行:

    ~/github/test$ javac -cp .:base C.java
    ~/github/test$ java -cp .:base C
    hello interface 1

compatible目录包含了有abstract方法m()的I2接口,和未修改的I1接口。

    ~/github/test$ cat compatible/I2.java
    public interface I2 {
      void m();
    }

这个不能用来编译类C:

    ~/github/test$ javac -cp .:compatible C.java
    C.java:1: error: C is not abstract and does not override abstract method m() in I2
    public class C implements I1, I2 {
       ^
    1 error

错误信息非常精确。因为我们有前一次编译获得的C.class,如果我们编译compatible目录下的接口,我们仍然会得到能运行实现类的两个接口:

    ~/github/test$ javac compatible/I*.java
    ~/github/test$ java -cp .:compatible C
    hello interface 1

第三个叫做wrong的目录,包含的I2接口也定义了m()方法:

    ~/github/test$ cat wrong/I2.java
    public interface I2 {
      default void m(){
        System.out.println("hello interface 2");
      }
    }

我们应该不厌其烦的编译它。尽管m()方法被定义了两次,但是,实现类仍然可以运行,只要它没有调用那个定义了多次的方法,但是,只要我们调用m()方法,立即就会失败。这是我们使用的命令行参数:

    ~/github/test$ javac wrong/*.java
    ~/github/test$ java -cp .:wrong C
    Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting
    default methods: I1.m I2.m
        at C.m(C.java)
        at C.main(C.java:5)
    ~/github/test$ java -cp .:wrong C x
    ~/github/test$

结论

当你把给接口添加了default实现的类库移植到Java
8环境下的时候,一般不会有问题。至少Java8类库开发者给集合类添加default方法的时候就是这么想的。使用你类库的应用程序仍然依赖没有default方法的Java7的类库。当使用和修改多个不同的类库的时候,有很小的几率会发生冲突。如何才能避免呢?

像以前那样设计你的类库。可能依赖default方法的时候不要掉以轻心。万不得已不要使用。明智的选择方法名,避免和其它接口产生冲突。我们将会学习到Java编程中如何使用这个特性做开发。

原文链接:
javacodegeeks
翻译: ImportNew.com –
miracle1919
译文链接:
[ 转载请保留原文出处、译者和译文链接。]

关于作者: miracle1919

查看miracle1919的更多文章
>>

发表评论

电子邮件地址不会被公开。 必填项已用*标注