文章目录

  • 1、简介
  • 2、java 8中的默认方法是什么?
  • 3、为什么java 8中需要默认方法?
  • 4、调用默认方法时如何解决冲突?
  • 5、注意事项


1、简介

Java 程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接口中定义的每个方法提供具体的实现,或者从父类中继承它。但是,一旦接口更新,现有的实体类为了适配接口也需要进行更新。

Java 8为了解决这一问题引入了一种新的机制。Java 8中的接口现在支持在声明方法的同时提供实现,通过两种方式可以完成这种操作:

  • 第一,Java 8允许在接口内声明静态方法。
  • 第二,Java 8引入了一个新功能,叫默认方法,通过默认方法你可以指定接口方法的默认实现。

接口能提供方法的具体实现。因此,实现接口的类不需要显式地提供该方法的具体实现,就可以自动继承默认的实现。

我将在这篇文章中讨论弄清楚以下几点:

  1. java 8中的默认方法是什么?
  2. 为什么java 8中需要默认方法?
  3. 调用默认方法时如何解决冲突?

2、java 8中的默认方法是什么?

顾名思义,java 8中的默认方法只是默认方法。 如果不覆盖它们,则它们是调用者类将调用的方法。

默认方法使您能够向接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性。

public interface Moveable {
    default void move(){
        System.out.println("I am moving");
    }
}

Moveable 接口定义了一个方法move(), 并提供了默认实现。 如果任何类实现此接口,则它不需要实现move()方法。 它可以直接调用instance.move()。

public class Animal implements Moveable {
    public static void main(String[] args) {
        Animal dog = new Animal();
        dog.move();
    }
}

如果我们要自定义行为,那么可以提供自定义实现并覆盖该方法。

public class Animal implements Moveable {
    public void move() {
        System.out.println("I am running");
    }

    public static void main(String[] args) {
        Animal tiger = new Animal();
        tiger.move();
    }
}

3、为什么java 8中需要默认方法?

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

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

在java 8之前,如果迭代一个java集合,那么你将获得一个迭代器实例并调用它的下一个方法,直到hasNext()返回false。 这是常用代码,我们在日常编程中使用了数千次。 语法也总是一样的。

现在,我们可以使用紧凑的单行代码,实现跟之前一样的功能,如下:

public class Animal implements Moveable{
    public static void main(String[] args){
        List<Animal> list = new ArrayList();
        list.add(new Animal());
        list.add(new Animal());
        list.add(new Animal());

        list.forEach((Consumer<Moveable>) Moveable::move);
    }
}

4、调用默认方法时如何解决冲突?

在java中,一个类可以实现N个接口。 另外,接口也可以扩展另一个接口。 如果有任何默认方法在两个接口中声明,这些接口由单个类实现。 很显然方法的调用会混淆,不清楚调用的是哪个类中方法。

interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}

interface B extends A {
    default void hello() {
        System.out.println("Hello from B");
    }
}

class C implements A, B {
    public static void main(String[] args) {
        new C().hello();
    }
}

解决冲突的规则如下:

如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了方法,通过三条规则可以进行判断。

1、类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。

2、如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果 B 继承了 A ,那么 B 就比 A 更加具体。

3、最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。

我们回顾上边的例子,例子中 C 类同时实现了 B 接口和 A 接口,而这两个接口恰巧又都定义了名为 hello 的默认方法。

编译器会使用声明的哪一个 hello 方法呢?

其实上面的代码是编译不通过的,按照规则(2),应该选择的是提供了最具体实现的默认方法的接口。

但,在C中不知道谁比谁更具体,所以需要显示的指定调用哪个接口的方法:

public class C implements A, B {
    public static void main(String[] args) {
        new C().hello();
    }

    @Override
    public void hello() {
       A.super.hello();
    }

    OR

    @Override
    public void hello() {
       B.super.hello();
    }

    OR

    @Override
    public void hello() {
       System.out.println("Hello from C!");
    }
}

比如:调用 A.super.Hello(),那么打印的是 Hello form A!,调用 B.super.Hello() 那么输出的是 Hello from B!。

5、注意事项

1)default 关键字只能在接口中使用(以及用在 switch 语句的 default 分支),不能用在抽象类中。

2)接口默认方法不能覆写 Object 类的 equals、hashCode 和 toString 方法。

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

参考资料

https://howtodoinjava.com/java8/default-methods-in-java-8/

《Java 8 实战》