解析Java中的默认方法

 更新时间:2015年07月10日 11:58:20   投稿:goldensun  
这篇文章主要介绍了Java中的默认方法,包括继承和调用等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设计中看到更多的默认方法的应用。

相关文章

  • springBoot+webMagic实现网站爬虫的实例代码

    springBoot+webMagic实现网站爬虫的实例代码

    这篇文章主要介绍了springBoot+webMagic实现网站爬虫的实例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05
  • springboot + JPA 配置双数据源实战

    springboot + JPA 配置双数据源实战

    这篇文章主要介绍了springboot + JPA 配置双数据源实战,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • 详解Idea SpringBoot搭建SpringCloud的准备工作(推荐)

    详解Idea SpringBoot搭建SpringCloud的准备工作(推荐)

    这篇文章主要介绍了Idea SpringBoot搭建SpringCloud的准备工作(推荐),本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10
  • Java杂谈之如何消除代码中一大串参数列表

    Java杂谈之如何消除代码中一大串参数列表

    参数列表和字面意思差不多,主要是表示该参数数量不是固定的,可能会有一个参数,可能多个,不管多少个参数,都放到一个数组处理,这种参数叫可变参数。可变长参数要放在最后一个参数位置处理,但是一个函数里面不能有俩种类型的可变参数
    2021-10-10
  • SpringBoot文件访问映射如何实现

    SpringBoot文件访问映射如何实现

    这篇文章主要介绍了SpringBoot文件访问映射如何实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • java实现在SSM下使用支付宝扫码支付功能

    java实现在SSM下使用支付宝扫码支付功能

    这篇文章主要为大家详细介绍了java实现在SSM下使用支付宝扫码支付功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-02-02
  • 关于自定义过滤器获取不到session问题

    关于自定义过滤器获取不到session问题

    这篇文章主要介绍了关于自定义过滤器获取不到session问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • SpringBoot2.0整合Redis自定义注入bean组件配置的实战教程

    SpringBoot2.0整合Redis自定义注入bean组件配置的实战教程

    这篇文章主要介绍了SpringBoot2.0整合Redis自定义注入bean组件配置,我们将基于SpringBoot2.0整合搭建的微服务项目为奠基,开启中间件Redis的实战之路,需要的朋友可以参考下
    2023-06-06
  • 关于@Valid注解大全以及用法规范

    关于@Valid注解大全以及用法规范

    这篇文章主要介绍了关于@Valid注解大全以及用法规范,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • Mybatis下的SQL注入漏洞原理及防护方法解析

    Mybatis下的SQL注入漏洞原理及防护方法解析

    SQL 注入是发生在 Web 程序中数据库层的安全漏洞,是网站存在最多也是最简单的漏洞,在实际项目中,即使使用了 Mybatis 框架,但仍然有可能因为编码人员安全意识不足而导致 SQL 注入问题,这篇文章主要介绍了Mybatis下的SQL注入漏洞原理及防护方法 ,需要的朋友可以参考下
    2022-11-11

最新评论