本文亦发布于ThinkBucket

抽象类和接口是 Java 面向对象编程中非常重要的元素,在面向接口的编程中两者更是经常用到。类是对象的模版,抽象类和接口可以看作是具体的类的模版。

抽象类

如果一个 classabstract修饰,它就是抽象类。除了正常的方法定义外,抽象类里的方法可以是空的,直接以分号结尾,没有具体执行代码,这个方法就是抽象方法,它必须用 abstract 修饰。

我们无法实例化一个抽象类,抽象类必须通过一个具体的子类实例化:

Person p = new Person(); // 编译错误
public final class Demo {
  public static void main(String[] args) {
    Teacher t = new Teacher();
    t.setName("王明");
    t.work();
    Driver d = new Driver();
    d.setName("小陈");
    d.work();
  }
}
// 定义一个抽象类
abstract class People {
  private String name; // 实例变量
  // 共有的 setter 和 getter 方法
  public void setName(String name){
    this.name = name;
  }
  public String getName(){
    return this.name;
  }
  // 抽象方法
  public abstract void work();
}

class Teacher extends People {
  // 必须实现该方法
  public void work() {
    System.out.println("我的名字叫" + this.getName() + ",我正在讲课,请大家不要东张西望…");
  }
}

class Driver extends People {
  // 必须实现该方法
  public void work() {
    System.out.println("我的名字叫" + this.getName() + ",我正在开车,不能接听电话…");
  }
}

除了可以拥有抽象方法和不能实例化的特性外,抽象类拥有普通类的所有特点,比如:

  • 可以继承父类(但抽象类的父类必须是抽象类)
  • 可以实现接口
  • 可以写 privateprotectedpublic 的成员变量和方法
  • 可以写 static final 的常量

接口

接口一般是描述一些行为,是对接口使用者的一个承诺。在面向接口的编程中,接口的使用者只需要调用接口的某个方法达到其目的,而无需关心是哪个类实现的。接口的一个例子:

interface Person {
  void run();
  String getName();
}

接口的一些特性:

  • 所有方法都是 public abstract 的,必须被接口的实现类实现(Java 8之前)
  • 所有的变量都是 public static final 的,其实就是常量

:::tip

因为接口定义的所有方法默认都是 public abstract 的,所以这两个修饰符不需要写出来(写不写效果都一样)。

当一个具体的 class 去实现一个 interface 时,需要使用 implements 关键字。举个例子:

class Student implements Person {
  private String name;

  public Student(String name) {
    this.name = name;
  }

  @Override
  public void run() {
    System.out.println(this.name + " run");
  }

  @Override
  public String getName() {
    return this.name;
  }
}

空接口

我们常常看到 Java 程序里有定义的一些空接口,那么空接口是什么作用呢?

空接口的主要是用来做判断的,也就是作为一个标记。为了判断某一个类是否满足其筛选条件时可以做一个空接口,然后利用 instanceof 方法来判断某一类是否使用了该接口,以达到你要筛选指定类型类的需求。

接口和抽象类比较

相同点

从某种角度讲,接口是一种特殊的抽象类,它们有很大的相似处:

  • 都代表类树形结构的抽象层。在使用引用变量时,尽量使用类结构的抽象层,使方法的定义和实现分离,这样做对于代码有松散耦合的好处。
  • 都不能被实例化。
  • 都能包含抽象方法。

区别

  • 接口里只能有常量,而且会“污染”实现类里的作用域;抽象类可以拥有 private 的变量,有一定程度的封装。
  • 接口只能继承接口;抽象类既可以继承抽象类,也可以实现接口。
  • 接口里的所有方法使用者都能直接调用;抽象类里可以封装一些 privateprotected 或者包访问级别的方法
  • 接口里的所有方法都是抽象的,没有方法体(Java 8 之前);抽象类里的非抽象方法可以拥有方法体。
  • 一个实现类一旦继承了某个抽象类,可以实现别的接口,但是不能继承其他类了;而如果它实现了某个接口,还可以实现别的接口,也可以继承别的类。体为空),但抽象类实现某个接口,可以不实现所有接口的方法,可以由它的子类实现。
  • 接口是对行为的一种抽象,而抽象类是对类的抽象,包括属性、方法。继承抽象类的类往往是具有一些相似特点的类,而实现接口的类可以跨不同的域,仅仅实现了接口定义的契约。类继承抽象类像是一个 ”is-a” 特点,类实现接口像是 ”like-a” 特点。
  • 在设计时,对接口往往是自上而下的,先定义接口行为,然后再针对其做具体实现;抽象类往往是自下而上的,我们先知道子类后才对其进行抽象出父类。
class Student implements Person, Hello { // 实现了两个interface
  ...
}

接口继承

一个 interface 可以使用 extends 继承自另一个 interface 。例如:

interface Hello {
  void hello();
}

interface Person extends Hello {
  void run();
  String getName();
}

此时, Person 接口继承自 Hello 接口,因此, Person 接口现在实际上有 3 个抽象方法签名,其中一个来自继承的 Hello 接口。

类与接口的继承关系

合理设计 interface 和 abstract class 的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在 abstract class 中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考 Java 的集合类定义的一组接口、抽象类以及具体子类的继承关系:

在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:

List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口

接口里的默认方法

从 Java 8 开始,Java 为接口提供了默认方法的功能,用 default 关键字表示,比如:

interface InterfaceA {
  default void foo() {
    System.out.println("InterfaceA foo");
  }
}

class ClassA implements InterfaceA {
}

public class Test {
  public static void main(String[] args) {
    new ClassA().foo(); // Will print "InterfaceA foo"
  }
}

ClassA 没有实现 InterfaceAfoo 方法,但是 InterfaceA 提供了默认实现,当 ClassA 的实例调用到 foo 方法时,实际上是调用了接口里的默认实现。

为什么引入默认方法

在 Java 8 之前,接口和实现类之间高度耦合,当接口中添加一个方法时,它的所有实现类都需要修改,否则会发生编译错误。无法在不破坏现有实现的条件下向接口添加方法。

:::tip

To use default method, JDK >= 1.8 is a must.

在 Java 8 里面,引入默认方法的意图是允许向现有接口添加方法,Java 8 里有一个重要新功能: lamda 表达式,这需要升级旧接口并保持向后兼容。

String[] array = new String[] {
  "hello",
  ", ",
  "world",
};
List<String> list = Arrays.asList(array);
list.forEach(System.out::println); // additional method in JDK 1.8

forEach 方法是 Java 8 里为 Iterable 接口添加的新默认方法,实现类不需要做任何修改就可以直接用它。下面是 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);
    }
  }
}

更多细节,可以参考这篇文章:https://ebnbin.com/2015/12/20/java-8-default-methods/

:::caution

向现有接口添加新方法充满了风险。在存在默认方法的情况下,接口的现有实现可能编译没有错误或警告,但在运行时会失败。

:::good

除非有必要,否则应避免使用默认方法向现有接口添加新方法,在这种情况下,你应该认真考虑一下现有接口实现是否会被默认方法实现破坏。

以上都是需要注意的,但是默认方法对于在创建接口时提供标准方法实现非常有用,它能简化实现接口的任务。

参考资料

  1. Effective Java, By Joshua Bloch
  2. Java 8 Default Methods, By Ebn Zhang