说说 Java 8 新特性,default 方法

Java 8 新特性,default 方法

default 方法,也可称为 Defender 方法,或者虚拟拓展方法 (Virtual extension methods)。

1. 来源

Java8 的一个重要的特性就是引入了函数式方法,其中 Collection 接口中增加了新的stream()方法。
我们都知道在 Java 的接口中只能定义方法,而不能对方法进行具体实现。其方法的实现必须要到实现了该接口的非抽象子类中实现。
因为接口导致的这个语法限制,使得要实现 Collection 接口的stream()方法全覆盖变得异常困难。难道所有继承了 Collection 接口的类中都要重写stream()方法的实现吗?显然不是的。机智的 Java 开发工程师们在 Java8 中引入了这样一个新的概念——'default' 方法。

简单的说,Java 接口拥有了非抽象方法了!default 方法带来的好处是可以往接口中新增加方法,而不破坏现有的实现架构。

2. 使用

2.1 HelloWorld

从最简单的例子说起

interface Father {
    default public void sayHi() {
        System.out.println("Hi Father");
    }
}

class FatherImpl implements Father {

}

public class Default {
    public static void main(String[] args) {
        Father f = new FatherImpl();
        f.sayHi();
    }
}

以上代码,声明了一个 Father 接口,接口中包含一个 default 方法sayHi(),其作用是输出一句话。定义了一个 FatherImpl 类实现了 Father 接口。并在主类中新建了 FatherImpl 类,调用其sayHi()方法。
其输出结果为:
Hi Father

2.2 多个 default 方法

接口中可以定义多个 default 方法。

interface Father {
    default public void sayHi() {
        System.out.println("Hi Father");
    }

    default public void sayHello() {
        System.out.println("Hello Father");
    }
}

class FatherImpl implements Father {

}

public class Default {
    public static void main(String[] args) {
        Father f = new FatherImpl();
        f.sayHi(); // Hi Father
        f.sayHello(); // Hello Father
    }
}

2.3 重写 default 方法

default 方法一旦与类中定义的方法有冲突,编译器优先选择类中定义的方法。如下:

interface Father {
    default public void sayHi() {
        System.out.println("Hi Father");
    }
}

class FatherImpl implements Father {
    public void sayHi() {
        System.out.println("Hi FatherImpl");
    }
}

public class Default {
    public static void main(String[] args) {
        Father f = new FatherImpl();
        f.sayHi(); // Hi FatherImpl
    }
}

2.4 同时继承和实现

以下是一种特殊情况:

interface Father {
    default public void sayHi() {
        System.out.println("Hi Father");
    }
}

class FatherImpl implements Father {
    public void sayHi() {
        System.out.println("Hi FatherImpl");
    }
}

class SubFatherImpl extends FatherImpl implements Father {

}

public class Default {
    public static void main(String[] args) {
        SubFatherImpl f = new SubFatherImpl();
        f.sayHi(); // Hi FatherImpl
    }
}

SubFatherImpl中没有做任何其他操作,只是继承了 FatherImpl,实现了 Father 接口。最后通过f.sayHi()调用到的是FatherImpl中的方法,而不是接口中定义的 default 方法,原因在于,与接口中定义的默认方法相比,类中定义的方法更具体。

2.4 能说明,类中重写的方法优先级大于接口中的默认方法。这样的设计主要是由增加默认方法的目的决定的。
增加默认方法的目的是为了在接口上进行向后兼容。

假设已实现了一个继承于 List 接口的定制的列表类 MyList,该类中有一个addAll()方法,如果新的 List 接口增加了默认方法addAll()。如果类中重写的方法没有默认方法的优先级高,那么就会破坏已有的实现。—— Java8 函数式编程

2.5 多重继承

当一个类实现了多个接口,其中有某两个接口定义了相同函数签名的默认方法而又没有进行实现时,就会引起问题,如下:

interface Father {
    default public void sayHi() {
        System.out.println("Hi Father");
    }
}

interface Father2 {
    default public void sayHi() {
        System.out.println("Hi Father2");
    }
}

class FatherImpl implements Father,Father2 {

}

FatherImpl类上有一个错误Duplicate default methods named sayHi with the parameters () and () are inherited from the types Father2 and Father,意思大概是说,编译器不明确应该使用接口中的哪个方法。在类中实现相应的方法就可以解决此问题。
如下:

interface Father {
    default public void sayHi() {
        System.out.println("Hi Father");
    }
}

interface Father2 {
    default public void sayHi() {
        System.out.println("Hi Father2");
    }
}

class FatherImpl implements Father,Father2 {
    public void sayHi() {
        System.out.println("No!! It's my method!");
    }
}

如果我们想实现指定类的默认方法呢?

class FatherImpl implements Father,Father2 {
    public void sayHi() {
        Father2.super.sayHi();
    }
}

当然,我们可以同时使用两个接口的 default 方法

interface Father {
    default public void sayHi() {
        System.out.println("Hi Father");
    }
}

interface Father2 {
    default public void sayHi() {
        System.out.println("Hi Father2");
    }
}

class FatherImpl implements Father,Father2 {
    public void sayHi() {
        Father2.super.sayHi();
        Father.super.sayHi();
    }
}

public class Default {
    public static void main(String[] args) {
        FatherImpl f = new FatherImpl();
        f.sayHi(); 
        // 输出 Hi Father2 \n Hi Father
    }
}

3. 三定律

  1. 类优先级高于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义的方法。
  2. 子类优先级高于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法,那么子类中定义的方法胜出。
  3. 如果上面两条规则都不适用,子类要么需要实现该方法,要么将该方法声明为抽象方法。

4. 权衡

在接口中定义方法的诸多变化引发了一系列问题。
既然可以用代码主体定义方法,那 Java8 种的接口还是旧版本中界定的代码吗?
我觉得,不到万不得已的时候不要使用 default。在对问题域建模时,还是应该根据具体情况进行权衡。
我目前所接触到的使用 default 方法的地方是 SpringDataJPA 使用 QueryDsl 时的场景,其需求是对传入参数进行自定义的绑定操作。详情可参考
Spring Data JPA - Reference

5. 参考

  1. 《Java8 函数式编程》
  2. Java8 揭秘 (三)Default 方法
  3. Java 8 新特性——default 方法(defender 方法)介绍