解析Java中的默认方法
为什么有默认方法?
java 8 就要来临,尽管发布期限已经被推迟, 我们仍非常确信在它最终发布的时候会支持lambdas 表达式。 前面提到过,我们之前关于这个主题已经讨论了不少,不过,lambdas表达式并不是java 8中唯一改变的游戏规则。
假设java 8 已经发布并且包含了lambda。现在你打算用一下lambda,最明显的应用场景莫过于对collection的每一个元素应用lambda。
list<?> list = … list.foreach(…); // 这就是lambda代码
在java.util.list或者java.util.collection接口里都找不到foreach的定义。通常能想到的解决办法是在jdk里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。
因此,如果在java 8里使用lambda的时候,因为向前兼容的原因而不能用于collection库,那有多糟糕啊。
由于上述原因,引入了一个新的概念。虚拟扩展方法,也即通常说的defender方法, 现在可以将其加入到接口,这样可以提供声明的行为的默认实现。
简单的说,java的接口现在可以实现方法了。默认方法带来的好处是可以为接口添加新的默认方法,而不会破坏接口的实现。
在我看来,这并非那种每天都会用到的java特性,但是它绝对能让java的collections api可以很自然的使用lambda。
最简单的例子
让我们看一个最简单的例子:一个接口a,clazz类实现了接口a。
public interface a { default void foo(){ system.out.println("calling a.foo()"); } } public class clazz implements a { }
代码是可以编译的,即使clazz类并没有实现foo()方法。在接口a中提供了foo()方法的默认实现。
使用这个例子的客户端代码:
clazz clazz = new clazz(); clazz.foo(); // 调用a.foo()
多重继承?
有一个常见的问题:人们会问 当他们第一次听到关于默认方法的新的特性时 “如果一个类实现了两个接口,并且两个接口都用相同的签名定义了默认方法,这该怎么办?”让我们用先前的例子来展示这个解决方案:
public interface a { default void foo(){ system.out.println("calling a.foo()"); } } public interface b { default void foo(){ system.out.println("calling b.foo()"); } } public class clazz implements a, b { }
这段代码不能编译 有以下原因:
java:class clazz 从types a到b给foo()继承了不相关的默认值
为了修复这个,在clazz里我们不得不手动解决通过重写冲突的方法:
public class clazz implements a, b { public void foo(){} }
但是如果我们想从接口a中调用默认实现方法foo(),而不是实现我们自己的方法,该怎么办呢?这是有可能的,引用a中的foo(),如下所示:
public class clazz implements a, b { public void foo(){ a.super.foo(); } }
现在我不能十分确信我喜欢这个最终方案。也许它比在签名里声明默认方法的实现更为简练,正如在默认方法规范的第一手稿里所声明的:
public class clazz implements a, b { public void foo() default a.foo; }
但是这确实更改了语法,难道不是吗?它看起来更像一个接口的方法声明而不是实现。假若接口a和接口b定义了许多相互冲突的默认方法,而我愿意使用所有接口a的默认方法解决冲突,那又如何呢?目前我不得不一个接着一个的解决冲突,改写每一对冲突的方法。这可能需要大量的工作和书写大量的模板代码。
我估计解决冲突的方法需要进行大量的讨论,不过看起来创建者决定接受这无法避免的灾难。
真实的例子
默认方法实现的真实例子可以在 jdk8早期打的包中找到。回到集合的foreach方法的例子中, 我们可以发现在java.lang.iterable接口中,它的默认实现如下:
@functionalinterface public interface iterable<t> { iterator<t> iterator(); default void foreach(consumer<? super t> action) { objects.requirenonnull(action); for (t t : this) { action.accept(t); } } }
foreach 使用了一个java.util.function.consumer功能接口类型的参数,它使得我们可以传入一个lambda表达式或者一个方法引用,如下:
list<?> list = … list.foreach(system.out::println);
方法调用
让我们看一下实际上是如何调用默认的方法的。如果你不熟悉这个问题,那么你可能有兴趣阅读一下rebel实验室有关java字节的报告。
从客户端代码的视角来看,默认的方法仅仅是常见的虚拟方法。因此名字应该是虚拟扩展方法。因此对于把默认方法实现为接口的简单例子类来说,客户端代码将在调用默认方法的地方自动调用接口。
a clazz = new clazz(); clazz.foo(); // invokeinterface foo() clazz clazz = new clazz(); clazz.foo(); // invokevirtual foo()
如果默认方法的冲突已经解决,那么当我们修改默认方法并指定调用其中一个接口时候,invokespecial将给我们指定具体调用哪个接口的实现。
public class clazz implements a, b { public void foo(){ a.super.foo(); // invokespecial foo() } }
下面是javap的输出:
public void foo(); code: 0: aload_0 1: invokespecial #2 // interfacemethod a.foo:()v 4: return
正如你看到的:invokespecial指令用来调用接口方法foo()。从字节码的视角来看,这仍是新鲜的事情,因为以前你只能通过指向一个类(父类)的而不是指向一个接口的super来调用方法。
最后…
默认方法是对java语言的有趣补充 – 你可以把他们看做是lambdas表达式和jdk库之间的桥梁。默认表达式的主要目标是使标准jdk接口得以进化,并且当我们最终开始使用java 8的lambdas表达式时,提供给我们一个平滑的过渡体验。谁知道呢,也许将来我们会在api设计中看到更多的默认方法的应用。
下一篇: Java程序中的延迟加载功能使用