解析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设计中看到更多的默认方法的应用。

(0)

相关推荐

  • Java基础教程之封装与接口

    总结之前的内容,对象(object)指代某一事物,类(class)指代象的类型.对象可以有状态和动作,即数据成员和方法. 到现在为止,数据成员和方法都是同时开放给内部和外部的.在对象内部,我们利用this来调用对象的数据成员和方法.在对象外部,比如当我们在另一个类中调用对象的时,可以使用 对象.数据成员 和 对象.方法() 来调用对象的数据成员和方法. 我们将要封装(encapsulation)对象的成员(成员包括数据成员和方法),从而只允许从外部调用部分的成员.利用封装,我们可以提高对象的易用

  • java实现小i机器人api接口调用示例

    复制代码 代码如下: package com.weixin.util; import java.io.IOException;import java.util.Random;import org.apache.commons.codec.binary.Hex;import org.apache.commons.codec.digest.DigestUtils;import org.apache.commons.httpclient.HttpClient;import org.apache.com

  • Java8新特性之默认方法(default)浅析

    一.什么是默认方法,为什么要有默认方法 简单说,就是接口可以有实现方法,而且不需要实现类去实现其方法.只需在方法名前面加个default关键字即可. 为什么要有这个特性?首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现.然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现.所以引进的默认方法

  • java线程之使用Runnable接口创建线程的方法

    实现Runnable接口的类必须使用Thread类的实例才能创建线程.通过Runnable接口创建线程分为两步: 1. 将实现Runnable接口的类实例化. 2. 建立一个Thread对象,并将第一步实例化后的对象作为参数传入Thread类的构造方法. 最后通过Thread类的start方法建立线程. 下面的代码演示了如何使用Runnable接口来创建线程: 复制代码 代码如下: package mythread; public class MyRunnable implements Runn

  • Java8接口的默认方法

    Java8接口的默认方法 什么是默认方法,为什么要有默认方法? 简单说,就是接口可以有实现方法,而且不需要实现类去实现其方法.只需在方法名前面加个default关键字即可. 为什么要有这个特性?首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现.然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有

  • java后台调用HttpURLConnection类模拟浏览器请求实例(可用于接口调用)

    一般在项目开发中难免遇到外部接口的调用,本文实例讲述了java后台调用HttpURLConnection类模拟浏览器请求的方法.可用于接口调用.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: package com.cplatform.movie.back.test; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.InputStreamReader; import ja

  • java自定义注解接口实现方案

    java注解是附加在代码中的一些元信息,用于一些工具在编译.运行时进行解析和使用,起到说明.配置的功能. 注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用.包含在 java.lang.annotation 包中. 1.元注解 元注解是指注解的注解.包括 @Retention @Target @Document @Inherited四种. 1.1.@Retention: 定义注解的保留策略 Java代码 复制代码 代码如下: @Retention(RetentionPolicy.SOURCE

  • 解析Java中的默认方法

    为什么有默认方法? Java 8 就要来临,尽管发布期限已经被推迟, 我们仍非常确信在它最终发布的时候会支持lambdas 表达式. 前面提到过,我们之前关于这个主题已经讨论了不少,不过,lambdas表达式并不是Java 8中唯一改变的游戏规则. 假设Java 8 已经发布并且包含了lambda.现在你打算用一下lambda,最明显的应用场景莫过于对collection的每一个元素应用lambda. List<?> list = - list.forEach(-); // 这就是lambda

  • 一文解析Java中的方法重写

    目录 1.含义 2.为什么要使用方法重写 3.如何使用方法重写 3.1 基本语法 3.2 具体分析 3.3 方法重写的一些小技巧 1.含义 子类继承父类后,可以在子类中书写一个与父类同名同参的方法,从而实现对父类中同名同参数的方法的覆盖,我们把这一过程叫做方法的重写(override) 2.为什么要使用方法重写 2.1 当父类的方法满足不了子类的需求的时候,需要在子类中对该方法进行重写 2.2 题目与分析 例如存在一个父类Peple,子类Chinese,父类中有一个say()方法,输出人在说话,

  • 深入解析Java中反射中的invoke()方法

    先讲一下java中的反射: 反射就是将类别的各个组成部分进行剖析,可以得到每个组成部分,就可以对每一部分进行操作 反射机制应用场景:逆向代码.动态生成类框架等,使用反射机制能够大大的增强程序的扩展性. 反射的基本步骤:首先获得Class对象,然后实例化对象,获得类的属性.方法或者构造函数,最后访问属性.调用方法.调用构造函数创建对象.而invoke()方法就是用来执行指定对象的方法. 在比较复杂的程序或框架中来使用反射技术,可以简化代码提高程序的复用性. 讲的是Method类的invoke()方

  • 详细讲解Java中的main()方法

    前言 JAVA中的主函数是我们再熟悉不过的了,相信每个学习过JAVA语言的人都能够熟练地写出这个程序的入口函数,但对于主函数为什么这么写,其中的每个关键字分别是什么意思,可能就不是所有人都能轻松地答出来的了.我也是在学习中碰到了这个问题,通过在网上搜索资料,并加上自己的实践终于有了一点心得,不敢保留,写出来与大家分享. Java中的main()方法 java虚拟机通过main方法找到需要启动的运行程序,并且检查main函数所在类是否被java虚拟机装载.如果没有装载,那么就装载该类,并且装载所有

  • Java中的hashcode方法介绍

    哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个方法的声明可知,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没有给出具体的实现. 为何Object类需要这样一个方法?它有什么作用呢?今天我们就来具体探讨一下hashCode方法. 一.hashCode方法的作用 对于包含容器类型的程序设计语言来说,基本上都会涉及到has

  • 解析HashMap中的put方法执行流程

    目录 引言 HashMap底层数据结构 put方法的执行流程 总结 引言 在Java集合中,HashMap的重要性不言而喻,作为一种存储键值对的数据结构,它在日常开发中有着非常多的应用场景,也是面试中的高频考点,本篇文章就来分析一下HashMap集合中的put方法. HashMap底层数据结构 先来了解一下HashMap底层的数据结构,它实质上是一个散列表,在数据结构课程中,我们应该都学习过散列表,它是通过关键码值而直接进行访问的一种数据结构,比如存储这样的一个序列:5,12,7,6,1,3.我

  • java中重写equals()方法的同时要重写hashcode()方法(详解)

    object对象中的 public boolean equals(Object obj),对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true: 注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码.如下: (1) 当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true (2) 当obj

  • 浅谈Java中的hashcode方法(推荐)

    哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个方法的声明可知,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没有给出具体的实现. 为何Object类需要这样一个方法?它有什么作用呢?今天我们就来具体探讨一下hashCode方法. 一.hashCode方法的作用 对于包含容器类型的程序设计语言来说,基本上都会涉及到has

  • Java中Object toString方法简介_动力节点Java学院整理

    一.Object类介绍  Object类在Java里面是一个比较特殊的类,JAVA只支持单继承,子类只能从一个父类来继承,如果父类又是从另外一个父类继承过来,那他也只能有一个父类,父类再有父类,那也只能有一个,JAVA为了组织这个类组织得比较方便,它提供了一个最根上的类,相当于所有的类都是从这个类继承,这个类就叫Object.所以Object类是所有JAVA类的根基类,是所有JAVA类的老祖宗.所有的类,不管是谁,都是从它继承下来的. 二.toString方法介绍  一个字符串和另外一种类型连接

  • 实例解析Java中的构造器初始化

    1.初始化顺序 当Java创建一个对象时,系统先为该对象的所有实例属性分配内存(前提是该类已经被加载过了),接着程序开始对这些实例属性执行初始化,其初始化顺序是:先执行初始化块或声明属性时制定的初始值,再执行构造器里制定的初始值. 在类的内部,变量定义的先后顺序决定了初始化的顺序,即时变量散布于方法定义之间,它们仍就会在任何方法(包括构造器)被调用之前得到初始化. class Window { Window(int maker) { System.out.println("Window(&quo

随机推荐