Jakob Jenkov 2021-01-24
Java 接口(interface)有点类似于 Java 类(class),但 Java 接口只能包含方法签名和字段。Java 接口本意并不包含方法的具体实现,只包含方法的签名(名称、参数和异常)。不过,从 Java 8 开始,可以在接口中提供方法的默认实现(default implementation),以简化实现该接口的类。
在 Java 中,你可以使用接口来实现多态性(polymorphism)。本文稍后会详细讲解多态性。
Java 接口示例
下面是一个简单的 Java 接口示例:
public interface MyInterface {
public String hello = "Hello";
public void sayHello();
}
如你所见,接口使用 interface 关键字声明。和类一样,Java 接口可以声明为 public 或包级私有(即不加访问修饰符)。
上面的接口包含一个变量和一个方法。可以直接通过接口访问该变量,如下所示:
System.out.println(MyInterface.hello);
可以看到,从接口访问变量的方式与从类中访问静态变量非常相似。
然而,接口中的方法必须由某个类实现之后才能调用。下一节将说明如何实现接口。
实现接口(Implementing an Interface)
在真正使用接口之前,你必须在某个 Java 类中实现该接口。下面是一个实现上述 MyInterface 接口的类:
public class MyInterfaceImpl implements MyInterface {
public void sayHello() {
System.out.println(MyInterface.hello);
}
}
注意类声明中的 implements MyInterface 部分,这告诉 Java 编译器:MyInterfaceImpl 类实现了 MyInterface 接口。
实现接口的类必须实现接口中声明的所有方法。这些方法的签名(名称 + 参数)必须与接口中声明的完全一致。类不需要重新声明接口中的变量,只需实现方法即可。
接口实例(Interface Instances)
一旦某个 Java 类实现了接口,就可以将该类的实例当作接口类型的实例来使用。例如:
MyInterface myInterface = new MyInterfaceImpl();
myInterface.sayHello();
注意,这里变量被声明为接口类型 MyInterface,而创建的对象是 MyInterfaceImpl 类型。Java 允许这样做,因为 MyInterfaceImpl 实现了 MyInterface 接口。因此,你可以将 MyInterfaceImpl 的实例引用为 MyInterface 类型。
你不能直接创建 Java 接口的实例。必须始终创建某个实现了该接口的类的实例,并将其引用为接口类型。
实现多个接口(Implementing Multiple Interfaces)
一个 Java 类可以实现多个接口。此时,该类必须实现所有被实现接口中声明的所有方法。例如:
public class MyInterfaceImpl implements MyInterface, MyOtherInterface {
public void sayHello() {
System.out.println("Hello");
}
public void sayGoodbye() {
System.out.println("Goodbye");
}
}
这个类实现了两个名为 MyInterface 和 MyOtherInterface 的接口。在 implements 关键字后列出所有要实现的接口名称,用逗号分隔。
如果这些接口不在与实现类相同的包中,则还需要导入这些接口。Java 接口的导入方式与 Java 类相同,使用 import 指令。例如:
import com.jenkov.package1.MyInterface;
import com.jenkov.package2.MyOtherInterface;
public class MyInterfaceImpl implements MyInterface, MyOtherInterface {
// ...
}
以下是上述类所实现的两个 Java 接口:
public interface MyInterface {
public void sayHello();
}
public interface MyOtherInterface {
public void sayGoodbye();
}
如你所见,每个接口都包含一个方法,这些方法都在 MyInterfaceImpl 类中得到了实现。
方法签名重叠(Overlapping Method Signatures)
如果一个 Java 类实现了多个接口,就有可能出现这些接口中包含相同签名(名称 + 参数)的方法的情况。由于 Java 类对给定签名的方法只能有一个实现,这可能会引发问题。
Java 规范对此并未提供解决方案,需要你自己决定如何处理这种情况。
哪些 Java 类型可以实现接口?
以下 Java 类型可以实现接口:
- Java 类(Class)
- Java 抽象类(Abstract Class)
- Java 嵌套类(Nested Class)
- Java 枚举(Enum)
- Java 动态代理(Dynamic Proxy)
接口常量(Interface Constants)
Java 接口可以包含常量。在某些情况下,在接口中定义常量是有意义的,特别是当这些常量要被实现该接口的类用于计算或作为方法参数时。不过,我的建议是:尽可能避免在接口中放置变量。
接口中的所有变量隐式地是 public、static 和 final 的,即使你在声明时省略了这些关键字。
下面是一个包含两个常量的 Java 接口示例:
public interface MyInterface {
int FALSE = 0;
int TRUE = 1;
}
接口方法(Interface Methods)
Java 接口可以包含一个或多个方法声明。如前所述,接口不能为这些方法指定具体实现,实现工作由实现该接口的类完成。
接口中的所有方法都是 public 的,即使你在方法声明中省略了 public 关键字。
接口默认方法(Interface Default Methods)
在 Java 8 之前,Java 接口不能包含方法实现,只能包含方法签名。但这在 API 演进时会带来问题:如果 API 需要在已有接口中添加新方法,那么所有实现该接口的类都必须实现这个新方法。如果这些实现类属于 API 的使用者(客户端代码),升级 API 就会导致编译错误。
举个例子。假设你有一个开源 API,其中包含如下接口:
public interface ResourceLoader {
Resource load(String resourcePath);
}
某项目使用该 API 并实现了这个接口:
public class FileLoader implements ResourceLoader {
public Resource load(String resourcePath) {
// 实现细节 + return 语句
}
}
如果 API 开发者想给 ResourceLoader 接口增加一个新方法,那么当项目升级到新版本 API 时,FileLoader 类就会因缺少新方法的实现而无法编译。
为了解决这个问题,Java 8 引入了接口默认方法(default methods)。默认方法可以包含默认实现。如果某个类实现了该接口但没有提供自己的实现,就会自动继承默认实现。
使用 default 关键字标记默认方法。例如:
public interface ResourceLoader {
Resource load(String resourcePath);
default Resource load(Path resourcePath) {
// 提供默认实现:从 Path 加载资源并返回 Resource 对象
}
}
类可以通过像平常一样显式实现该方法来覆盖默认实现。类中的实现优先于接口中的默认实现。
接口静态方法(Interface Static Methods)
Java 接口可以包含静态方法,且静态方法必须提供实现。例如:
public interface MyInterface {
public static void print(String text) {
System.out.print(text);
}
}
调用接口中的静态方法与调用类中的静态方法方式相同:
MyInterface.print("Hello static method!");
当某些工具方法与接口职责密切相关时,将它们作为接口的静态方法是有用的。例如,Vehicle 接口可以包含一个 printVehicle(Vehicle v) 静态方法。
接口与继承(Interfaces and Inheritance)
Java 接口可以继承其他接口,就像类可以继承其他类一样。使用 extends 关键字指定继承关系。例如:
public interface MySuperInterface {
public void sayHello();
}
public interface MySubInterface extends MySuperInterface {
public void sayGoodbye();
}
MySubInterface 继承自 MySuperInterface,因此它继承了父接口的所有字段和方法。任何实现 MySubInterface 的类都必须实现这两个接口中定义的所有方法。
你也可以在子接口中定义与父接口方法签名相同的方法(尽管通常不推荐)。
与类不同,接口可以继承多个父接口。只需在 extends 后列出所有父接口,用逗号分隔。实现该接口的类必须实现所有继承而来的方法。
例如:
public interface MySubInterface extends SuperInterface1, SuperInterface2 {
public void sayItAll();
}
与实现多个接口类似,如果多个父接口包含相同签名的方法,Java 不会对如何处理这种情况做强制规定。
继承与默认方法(Inheritance and Default Methods)
默认方法为接口继承增加了一些复杂性。通常,一个类可以实现多个包含相同签名方法的接口。但如果其中一个(或多个)接口将该方法声明为默认方法,情况就不同了。
如果两个接口包含相同签名的方法,且其中至少一个将其声明为默认方法,那么实现这两个接口的类必须显式实现该方法,以消除歧义。类中的实现优先于任何默认实现。
同样的规则也适用于接口继承多个父接口且存在方法签名冲突的情况。
接口与多态性(Interfaces and Polymorphism)
Java 接口是实现多态性的一种方式。多态性意味着一个对象可以被当作多种类型使用(这里的“类型”指类或接口)。
考虑下面的类图:
应用程序中使用的两个并行类层次结构
这些类用于建模现实世界中的车辆和驾驶员,包含字段和方法。
现在假设你需要将这些对象存储到数据库,并序列化为 XML、JSON 等格式。你希望每个 Car、Truck 或 Vehicle 对象都有统一的 store()、serializeToXML() 和 serializeToJSON() 方法。
一种方案是为 Vehicle 和 Driver 创建一个公共超类,并在其中添加这些方法。但这会造成概念混乱——类层次结构不再纯粹表示车辆和驾驶员,还耦合了存储和序列化逻辑。
更好的方案是定义接口:
public interface Storable {
public void store();
}
public interface Serializable {
public void serializeToXML(Writer writer);
public void serializeToJSON(Writer writer);
}
每个类实现这些接口后,就可以通过接口类型引用对象,而无需知道其具体类:
Car car = new Car();
Storable storable = (Storable) car;
storable.store();
Serializable serializable = (Serializable) car;
serializable.serializeToXML(new FileWriter("car.xml"));
serializable.serializeToJSON(new FileWriter("car.json"));
接口提供了一种比继承更清晰的方式来实现横切关注点(cross-cutting concerns)。
泛型接口(Generic Interfaces)
泛型接口是可以被类型化的接口,即在使用时可以指定其操作的具体类型。
首先看一个非泛型接口:
public interface MyProducer {
public Object produce();
}
它表示一个能生成对象的生产者。由于返回类型是 Object,它可以返回任意 Java 对象。
实现类:
public class CarProducer implements MyProducer {
public Object produce() {
return new Car();
}
}
使用时需要强制类型转换:
MyProducer carProducer = new CarProducer();
Car car = (Car) carProducer.produce();
使用泛型可以避免类型转换。泛型版本的接口:
public interface MyProducer<T> {
public T produce();
}
实现类也需要使用泛型:
public class CarProducer<T> implements MyProducer<T> {
@Override
public T produce() {
return (T) new Car(); // 注意:此处存在类型安全风险
}
}
使用方式:
MyProducer<Car> myCarProducer = new CarProducer<Car>();
Car car = myCarProducer.produce(); // 无需类型转换
但上述实现存在一个问题:CarProducer 总是返回 Car,但允许用户指定任意泛型类型,可能导致 ClassCastException。
更安全的做法是在实现类中固定泛型类型:
public class CarProducer implements MyProducer<Car> {
@Override
public Car produce() {
return new Car();
}
}
此时使用方式为:
MyProducer<Car> myCarProducer = new CarProducer();
Car car = myCarProducer.produce();
函数式接口(Functional Interfaces)
从 Java 8 开始,引入了函数式接口(functional interface)的概念。简单来说,函数式接口是仅包含一个未实现方法(非默认、非静态方法)的接口。
函数式接口通常用于配合 Java Lambda 表达式 使用。
更多详情请参阅 Java 函数式接口教程,它是 Java 函数式编程教程 的一部分。