Java17 入门基础知识(十三)
二十一、接口
在本章中,您将学习:
-
什么是接口
-
如何声明接口
-
如何在接口中声明抽象、默认、私有和静态方法
-
如何在类中完全和部分实现接口
-
接口发布后如何发展
-
如何从其他接口继承一个接口
-
通过接口使用
instanceof操作符 -
什么是标记接口
-
如何使用接口实现多态
-
动态绑定如何应用于接口类型变量的方法调用
-
什么是功能接口以及如何使用它们
本章中的所有示例程序都是清单 21-1 中声明的jdojo.interfaces模块的成员。
// module-info.java
module jdojo.interfaces {
exports com.jdojo.interfaces;
}
Listing 21-1The Declaration of a jdojo.interfaces Module
什么是接口?
一个接口在 Java 中是一个非常重要的概念。Java 开发人员的知识是不完整的,除非他们理解接口的作用。通过例子比通过正式定义更容易理解。在我们提供接口的正式定义之前,让我们讨论一个简单的例子,它将为关于接口需求的详细讨论奠定基础。
Java 应用程序由交互对象组成。一个对象通过发送消息与其他对象进行交互。对象接收消息的能力是通过在对象的类中提供方法来实现的。假设有一个名为Person的类,它提供了一个walk()方法。walk()方法为Person类的每个对象提供了接收“行走”消息的能力。让我们将Person类定义如下:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void walk() {
System.out.println(name + " (a person) is walking.");
}
}
Person类的一个对象将有一个名字,这个名字将在它的构造器中设置。当它接收到一个“walk”消息时,也就是说,当它的walk()方法被调用时,它在标准输出上打印一条消息。
让我们创建一个名为Walkables的实用程序类,用于向一组对象发送特定的消息。让我们假设您想要向Walkables类添加一个letThemWalk()静态方法,它接受一个Person对象数组。它向数组中的所有元素发送“行走”消息。你可以如下定义你的Walkables类。该方法顾名思义;也就是它让大家走路!
public class Walkables {
public static void letThemWalk(Person[] list) {
for (Person person : list) {
person.walk();
}
}
}
以下代码片段可用于测试Person和Walkable的类:
public class WalkablesTest {
public static void main(String[] args) {
Person[] persons = new Person[3];
persons[0] = new Person("Jack");
persons[1] = new Person("Jeff");
persons[2] = new Person("John");
// Let everyone walk
Walkables.letThemWalk(persons);
}
}
Jack (a person) is walking.
Jeff (a person) is walking.
John (a person) is walking.
到目前为止,您还没有看到Person和Walkables类的设计有任何问题,对吗?它们执行它们被设计来执行的动作。Person类的设计保证了它的对象将响应“行走”消息。通过将Person数组声明为Walkables类中letThemWalk()方法的参数类型,编译器确保对persons[i].walk()的调用是有效的,因为Person对象保证会响应“walk”消息。
让我们通过添加一个名为Duck的新类来扩展这个项目,它代表了现实世界中的一只鸭子。我们都知道鸭子也会走路。一只鸭子能做许多其他人能做或不能做的事情。然而,为了我们讨论的目的,我们将只关注鸭子的行走能力。您可以如下定义您的Duck类:
public class Duck {
private String name;
public Duck(String name) {
this.name = name;
}
public void walk() {
System.out.println(name + " (a duck) is walking.");
}
}
您可能会注意到在Person类和Duck类之间有一个相似之处。两个类的对象都可以响应“行走”消息,因为它们都提供了一个walk()方法。然而,这两个类之间的相似之处仅此而已。除了它们都以Object类作为它们的共同祖先之外,它们之间没有任何其他的联系。Duck类的引入扩展了应用程序中对象的行走能力。在有鸭子之前,只有人会走路。添加了Duck类后,鸭子也能走路了。
现在,您想使用您的Walkables类让鸭子走路。你的Walkables课能让鸭子走路吗?不。它不能让鸭子走路,除非你做一些改变。一辆Duck的行走能力对现有的Walkables级来说不构成任何问题。此时的问题是,letThemWalk()方法已经将其参数类型声明为一个数组Person。一个Duck不是一个Person。您不能编写此处显示的代码。不能将Duck对象分配给Person类型的引用变量。以下代码片段不会编译:
Person[] list = new Person[3];
list[0] = new Person("Jack");
list[1] = new Duck("Jeff"); // A compile-time error
list[2] = new Person("John");
Walkables.letThemWalk(list);
你怎么解决这个问题让你的Walkables类让一个人和一只鸭子走在一起?根据您现有的 Java 编程语言知识,有三种方法可以解决这个问题。请注意,我们在这一点上不是在谈论接口。在本节的末尾,您将使用接口有效而正确地解决这个问题。让我们暂时忘记这一章的标题,这样你就能体会到接口在 Java 编程中扮演的重要角色。解决这个问题的三种方法如下:
-
将
Walkables类的letThemWalk()方法的参数类型从数组Person改为数组Object。使用反射对传入数组的所有元素调用walk()方法。现在不要担心“反射”这个术语。 -
在
Walkables类中定义一个名为letDucksWalk(Duck[] ducks)的新静态方法。想让鸭子走路就调用这个方法。 -
从一个共同的祖先类继承
Person和Duck类,比如说Animal类,在Animal类中增加一个walk()方法。将Walkables类的letThemWalk()方法的参数类型从数组Person改为数组Animal。
我们来详细看看这三种解决方案。
提议的解决方案#1
您可以通过将这两个方法添加到Walkables类来实现第一个解决方案,如下所示:
// Walkables.java
import java.lang.reflect.Method;
public class Walkables {
public static void letThemWalk(Object[] list) {
for (Object obj : list) {
// Get the walk method reference
Method walkMethod = getWalkMethod(obj);
if (walkMethod != null) {
try {
// Invoke the walk() method
walkMethod.invoke(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static Method getWalkMethod(Object obj) {
Class<?> c = obj.getClass();
try {
Method walkMethod = c.getMethod("walk");
return walkMethod;
} catch (NoSuchMethodException e) {
// walk() method does not exist
}
return null;
}
}
getWalkMethod()方法在指定对象的类中寻找walk()方法。如果找到一个walk()方法,它将返回该方法的引用。否则返回null。您已经将letThemWalk()方法的参数类型从数组Person更改为数组Object。您可以使用下面的代码片段来测试修改后的Walkables类:
Object[] list = new Object[4];
list[0] = new Person("Jack");
list[1] = new Duck("Jeff");
list[2] = new Person("John");
list[3] = new Object(); // Does not have a walk() method
// Let everyone walk
Walkables.letThemWalk(list);
Jack (a person) is walking.
Jeff (a duck) is walking.
John (a person) is walking.
输出表明您的解决方案是可行的。它让人和鸭子一起走。同时,如果一个物体不知道如何行走,它也不会强迫该物体行走。您向letThemWalk()方法传递了四个对象,并且没有尝试对数组的第四个元素调用walk()方法,因为Object类没有walk()方法。
让我们拒绝这个解决方案,原因很简单,您使用反射对传入的所有对象调用了walk()方法,并且您依赖于这样一个事实,即所有知道如何行走的对象都有一个名为“walk”的方法如果您更改方法名,比如说在Person类中,从walk()更改为walkMe(),这个解决方案很容易被悄悄地破解。你的程序将继续工作,不会出现任何错误,但是当你用一个Person对象调用letThemWalk()方法时,它改变后的walkMe()方法将不会被调用。
提议的解决方案#2
让我们看看第二个建议的解决方案。您建议向您的Walkables类添加一个新方法letDucksWalk(),如下所示:
public class Walkables {
public static void letThemWalk(Person[] list) {
for (Person person : list) {
person.walk();
}
}
public static void letDucksWalk(Duck[] list) {
for (Duck duck : list) {
duck.walk();
}
}
}
这在某种意义上解决了问题,它将让所有的鸭子走路。然而,这也不是一个理想的解决方案。它还是不会让人和鸭子走在一起。这种解决方案的另一个问题是可扩展性。它不是一个可扩展的解决方案。如果你看一下letThemWalk()和letDucksWalk()这两个方法,你会发现除了参数类型Person和Duck之外,它们的逻辑是一样的。如果你添加一个名为Cat的新类,它的对象也会走路,会发生什么?这个解决方案将迫使您向Walkables类添加另一个方法letCatsWalk(Cat[] cats)。因此,您应该拒绝这种解决方案,因为它不可扩展。
提议的解决方案#3
让我们看看第三个建议的解决方案。它建议从一个共同的祖先类继承Person类和Duck类,比如说Animal,后者有一个walk()方法。它还会让您将Walkables类中的letThemWalk()方法的参数从Person数组更改为Animal数组。这个解决方案与您正在寻找的解决方案非常接近,在某些情况下,它可能被认为是一个好的解决方案。但是,由于以下两个原因,您拒绝了这个解决方案:
-
这种解决方案迫使您在类层次结构中拥有一个共同的祖先。例如,其对象知道如何行走的所有类必须有相同的祖先(直接或间接)。假设您创建了一个名为
Dog的新类,它的对象可以行走。在这个提议的解决方案中,Dog类必须从Animal类继承,所以您可以使用letThemWalk()方法让Dog行走。有时你想给一个类的对象增加行走功能,这个类已经从另一个类继承了。在这种情况下,不可能将现有类的超类更改为Animal类。 -
假设你继续这个解决方案。您添加了一个名为
Fish的新类,它继承了Animal类。一个Fish类的对象不知道如何行走。因为Fish类继承了Animal类,所以它也会继承walk()方法,也就是行走的能力。毫无疑问,您需要在Fish类中覆盖walk()方法。现在问题来了,Fish类应该如何实现walk()方法?它是否应该回答说“我是一条鱼,我不知道如何走路”?它应该抛出一个异常声明“要求鱼走路是非法的”吗?
你可以看到第三个解决方案似乎是一个非常接近的解决方案。然而,它不是一个理想的。这也证明了一点,在 Java 程序中使用继承是一件好事,但它并不总是提供理想的解决方案。
理想的解决方案
您正在寻找一种解决方案,它能提供两件事:
-
在
Walkables类中的一个单独的方法letThemWalk(),应该能够发送一个“行走”消息给所有作为参数传递给它的对象(例如,调用walk()方法)。这个方法应该适用于所有类型的可以行走的物体(你现在拥有的或者将来会拥有的)。 -
如果你想增加遍历一个现有类的能力,你不应该被迫改变这个类的超类。
Java 中的接口在这种情况下提供了一个完美的解决方案。在我们开始详细讨论接口之前,让我们先完成本节中提出的问题的解决方案。首先,您需要定义一个接口。现在,只要把接口想象成一个编程结构。
使用关键字“interface”声明一个接口,它可以有abstract方法声明。注意,abstract方法没有主体。每个接口都应该有一个名称。你的接口被命名为Walkable。它包含一个叫做walk()的方法。清单 21-2 显示了您的Walkable接口的完整代码。
// Walkable.java
package com.jdojo.interfaces;
public interface Walkable {
void walk();
}
Listing 21-2The Declaration for a Walkable Interface
所有对象可以行走的类都应该实现Walkable接口。一个类可以在其声明中使用关键字implements实现一个或多个接口。通过实现一个接口,一个类保证它将为接口中声明的所有abstract方法提供一个实现,或者这个类必须声明自己abstract。现在,让我们忽略第二部分,假设这个类实现了它实现的接口的所有abstract方法。如果一个类实现了Walkable接口,它必须为walk()方法提供一个实现。
Person和Duck类的对象需要行走的能力。你需要实现这些类的Walkable接口。清单 21-3 和 21-4 有这些类的完整修订代码。
// Duck.java
package com.jdojo.interfaces;
public class Duck implements Walkable {
private String name;
public Duck(String name) {
this.name = name;
}
public void walk() {
System.out.println(name + " (a duck) is walking.");
}
}
Listing 21-4The Revised Duck Class, Which Implements the Walkable Interface
// Person.java
package com.jdojo.interfaces;
public class Person implements Walkable {
private String name;
public Person(String name) {
this.name = name;
}
public void walk() {
System.out.println(name + " (a person) is walking.");
}
}
Listing 21-3The Revised Person Class, Which Implements the Walkable Interface
请注意,修改后的类的声明与其原始声明略有不同。他们都在声明中增加了一个新的“implements Walkable”条款。因为它们都实现了Walkable接口,所以它们必须提供在Walkable接口中声明的walk()方法的实现。您不必定义一个新的walk()方法,因为您从一开始就已经实现了它。如果这些类没有walk()方法,您必须在这个阶段将它添加到它们中。
在您修改您的Walkables类的代码之前,让我们看看您可以用Walkable接口做的其他事情。像类一样,接口定义了一个新的引用类型。当你定义一个类时,它定义了一个新的引用类型,并允许你声明该类型的变量。同样,当你定义一个新的接口时(例如Walkable,你可以定义一个新接口类型的引用变量。变量范围可以是局部的、实例的、静态的或方法参数。以下声明有效:
// w is a reference variable of type Walkable
Walkable w;
您不能创建接口类型的对象。以下代码无效:
// A compile-time error
new Walkable();
您只能创建一个类类型的对象。然而,接口类型变量可以引用任何其类实现该接口的对象。因为Person和Duck类实现了Walkable接口,所以Walkable类型的引用变量可以引用这些类的对象:
Walkable w1 = new Person("Jack"); // OK
Walkable w2 = new Duck("Jeff"); // OK
// A compile-time error as the Object class does not implement the Walkable interface
Walkable w3 = new Object();
你能用接口类型的引用变量做什么?您可以使用接口的引用类型变量来访问接口的任何成员。由于您的Walkable接口只有一个成员,即walk()方法,您可以编写如下所示的代码:
// Let the person walk
w1.walk();
// Let the duck walk
w2.walk();
当您在w1上调用walk()方法时,它会调用Person对象的walk()方法,因为w1正在引用一个Person对象。当您在w2上调用walk()方法时,它会调用Duck对象的walk()方法,因为w2正在引用一个Duck对象。当您使用接口类型的引用变量调用方法时,它会调用它所引用的对象上的方法。有了这些关于接口的知识,让我们来修改你的Walkables类的代码。清单 21-5 包含了修改后的代码。请注意,在修改后的letThemWalk()方法代码中,您所要做的就是将参数类型从Person更改为Walkable。其他一切都保持不变。
// Walkables.java
package com.jdojo.interfaces;
public class Walkables {
public static void letThemWalk(Walkable[] list) {
for (Walkable w : list) {
w.walk();
}
}
}
Listing 21-5The Revised Walkables Class
清单 21-6 展示了如何用Walkable接口测试你修改过的类。它创建了一个Walkable类型的数组。允许声明接口类型的数组,因为数组提供了创建许多相同类型变量的快捷方式。这一次,您可以将一个Walkable类型的数组中的Person类和Duck类的对象传递给Walkables类的letThemWalk()方法,这样大家就可以一起走了,如输出所示。
// WalkablesTest.java
package com.jdojo.interfaces;
public class WalkablesTest {
public static void main(String[] args) {
Walkable[] w = new Walkable[3];
w[0] = new Person("Jack");
w[1] = new Duck("Jeff");
w[2] = new Person("John");
// Let everyone walk
Walkables.letThemWalk(w);
}
}
Jack (a person) is walking.
Jeff (a duck) is walking.
John (a person) is walking.
Listing 21-6A Test Class to Test the Revised Person, Duck, and Walkables Classes
如果你想创建一个名为Cat的新类,它的对象应该具有行走能力,那么你现有的代码会有什么变化?您可能会惊讶地发现,您不需要更改现有代码中的任何内容。Cat类应该实现Walkable接口,仅此而已。清单 21-7 包含了Cat类的代码。
// Cat.java
package com.jdojo.interfaces;
public class Cat implements Walkable {
private String name;
public Cat(String name) {
this.name = name;
}
public void walk() {
System.out.println(name + " (a cat) is walking.");
}
}
Listing 21-7A Cat Class
您可以使用以下代码片段用现有代码测试新的Cat类。查看输出,您已经通过使用Walkable接口使人、鸭子和猫一起行走!这是接口在 Java 中的用途之一——它让您将原本不相关的类放在一个保护伞下:
Walkable[] w = new Walkable[4];
w[0] = new Person("Jack");
w[1] = new Duck("Jeff");
w[2] = new Person("John");
w[3] = new Cat("Jace");
// Let everyone walk
Walkables.letThemWalk(w);
Jack (a person) is walking.
Jeff (a duck) is walking.
John (a person) is walking.
Jace (a cat) is walking.
您已经实现了使用接口构造使不同种类的对象一起行走的目标。那么到底什么是接口呢?
Java 中的接口定义引用类型来指定抽象概念。它由提供概念实现的类来实现。在 Java 8 之前,接口只能包含抽象方法。Java 17 允许一个接口拥有静态、私有和默认的方法,这些方法也可以包含实现。但是,接口不能有非最终变量。接口让你通过抽象的概念定义不相关的类之间的关系。在我们的例子中,Walkable接口代表了一个概念,使您能够以相同的方式对待两个不相关的类Person和Duck,因为它们都实现了相同的概念(行走)。
是时候详细了解如何在 Java 程序中创建和使用接口了。当我们讨论接口的技术细节时,我们也回顾了接口的正确使用和常见误用。
声明接口
接口可以声明为顶级接口、嵌套接口或注释类型。我们将在本章后面讨论嵌套接口。注释类型的接口将在本系列的第二卷中讨论。我们使用术语接口来表示顶级接口。声明接口的一般(不完整)语法如下:
[modifiers] interface <interface-name> {
<constant-declaration>
<method-declaration>
<nested-type-declaration>
}
接口声明以可选的修饰符列表开始。像类一样,接口可以有公共或包级范围。关键字public用于表示接口具有公共范围。可以从应用程序的任何地方引用公共接口。跨模块引用一个接口取决于模块可访问性规则,如第十章所讨论的。缺少范围修饰符表示接口具有包级别的范围。具有包级范围的接口只能在其包的成员中引用。
关键字interface用于声明一个接口。关键字后面是接口的名称。接口的名称是有效的 Java 标识符。接口体跟在它的名字后面,名字放在大括号内。接口的成员是在主体内部声明的。在特殊情况下,接口体可以是空的。下面是最简单的接口声明:
package com.jdojo.interfaces;
interface Updatable {
// The interface body is empty
}
这段代码声明了一个名为Updatable的接口,它有一个包级别的作用域。它只能在com.jdojo.interfaces包内使用,因为它有包级范围。它不包含任何成员声明。
像类一样,接口有一个简单的名称和一个完全限定的名称。关键字接口后面的标识符是它的简单名称。接口的完全限定名是由它的包名和用点分隔的简单名组成的。在前面的例子中,Updatable是简单名称,com.jdojo.interfaces.Updatable是完全限定名称。使用简单和完全限定的接口名称的规则与使用类名称的规则相同。
下面的代码声明了一个名为ReadOnly的接口。它有一个公共范围。也就是说,ReadOnly接口的定义在同一个模块或其他模块中的任何地方都是可用的,这取决于模块可访问性规则:
package com.jdojo.interfaces;
public interface ReadOnly {
// The interface body is empty
}
接口声明是隐式抽象的。您可以如下声明Updatable和ReadOnly接口,而不改变它们的含义。换句话说,一个接口声明总是abstract,不管你是否显式声明它abstract:
abstract interface Updatable {
// The interface body is empty
}
public abstract interface ReadOnly {
// The interface body is empty
}
Note
Java 中的接口是隐式的abstract。在它们的声明中使用关键字abstract已经过时,不应该在新程序中使用。前面的例子仅用于说明目的。
声明接口成员
一个接口可以有三种类型的成员:
-
常量字段
-
抽象、静态、私有和默认方法
-
静态类型(嵌套接口和类)
注意,接口声明很像类声明,除了接口不能有可变的实例和类变量。与类不同,接口不能被实例化。接口的所有成员都是隐式公共的。
Tip
直到 Java 8,接口中所有类型的成员都是隐式公共的。Java 9 和更高版本允许你在一个接口中拥有私有方法,这一点我们将在本章后面讨论。
常量字段声明
你可以在一个接口中声明常量字段,如清单 21-8 所示。它声明了一个名为Choices的接口,该接口有两个int字段的声明:YES和NO。
// Choices.java
package com.jdojo.interfaces;
public interface Choices {
public static final int YES = 1;
public static final int NO = 2;
}
Listing 21-8Declaring Fields in an Interface
一个接口中的所有字段都是隐式的public、static和final。尽管接口声明语法允许在字段声明中使用这些关键字,但它们的使用是多余的。建议在接口中声明字段时不要使用这些关键字。Choices接口可以声明如下,不改变其含义:
public interface Choices {
int YES = 1;
int NO = 2;
}
您可以使用如下形式的点符号来访问界面中的字段:
<interface-name>.<field-name>
您可以使用Choices.YES和Choices.NO来访问Choices界面中YES和NO字段的值。清单 21-9 展示了如何使用点符号来访问接口的字段。
// ChoicesTest.java
package com.jdojo.interfaces;
public class ChoicesTest {
public static void main(String[] args) {
System.out.println("Choices.YES = " + Choices.YES);
System.out.println("Choices.NO = " + Choices.NO);
}
}
Choices.YES = 1
Choices.NO = 2
Listing 21-9Accessing Fields of an Interface
无论关键字final是否在声明中使用,接口中的字段总是final。这意味着您必须在声明时初始化字段。可以用编译时或运行时常量表达式初始化字段。因为一个final字段(常量字段)只被赋值一次,所以你不能设置一个接口的字段的值,除非在它的声明中。以下代码片段会生成编译时错误:
Choices.YES = 5; // A compile-time error
以下代码片段显示了接口的一些有效和无效字段声明:
/* All fields declarations are valid in the ValidFields interface */
public interface ValidFields {
int X = 10;
// You can use one field to initialize another if the referenced
// field is declared before the one that references it.
int Y = X;
double N = X + 10.5;
boolean YES = true;
boolean NO = false;
// Assuming Test is a class that exists
Test TEST = new Test();
}
/* Examples of invalid field declarations. */
public interface InvalidFields {
int X; // Invalid. X is not initialized
int Y = Z; // Invalid. Forward referencing of Z is not allowed.
int Z = 10; // Valid by itself.
Test TEST; // Invalid. TEST is not initialized, assuming a Test class exists
}
Tip
在接口中的字段名称中使用全部大写字母来表示它们是常量是一种约定。然而,Java 对接口字段的命名没有任何限制,只要它们遵循标识符的命名规则。一个接口的字段总是public。然而,从声明包外部对public字段的可访问性取决于接口的范围。例如,如果一个接口被声明为具有包级范围,那么它的字段在包外是不可访问的,因为接口本身在包外是不可访问的,即使它的字段是public。
建议您不要声明一个只有常量字段的接口。接口的正确(也是最常用的)用法是声明一组定义 API 的方法。如果你想在一个构造中组合常量,使用枚举,而不是接口。如果不能使用枚举,则使用类来声明常量。使用枚举为常量提供类型安全和编译时检查。枚举包含在第二十二章中。
方法声明
您可以在接口中声明四种类型的方法:
-
抽象方法
-
静态方法
-
默认方法
-
私有方法
在 Java 8 之前,你只能在接口中声明abstract方法。修饰符static、default和private分别用于声明静态、默认和私有方法。缺少一个static、default或private修饰符会产生一个方法abstract。下面是一个包含所有四种类型方法的接口示例:
interface AnInterface {
// An abstract method
int m1();
// A static method
static int m2() {
// The method implementation goes here
}
// A default method
default int m3() {
// The method implementation goes here
}
// A private method
private int m4() {
// The method implementation goes here
}
}
以下部分详细讨论了每个方法类型声明。
抽象方法声明
声明接口的主要目的是通过声明零个或多个抽象方法来创建一个抽象规范(或概念)。接口中的所有方法声明都是隐式抽象和公共的,除非它们被声明为static或default。像在类中一样,接口中的abstract方法没有实现。abstract方法的主体总是用分号表示,而不是用一对大括号表示。下面的代码片段声明了一个名为Player的接口:
public interface Player {
public abstract void play();
public abstract void stop();
public abstract void forward();
public abstract void rewind();
}
Player接口有四种方法:play()、stop()、forward()、rewind()。Player接口是音频/视频播放器的规范。一个真正的播放器,例如 DVD 播放器,将通过实现Player接口的所有四个方法来提供规范的具体实现。
在接口的方法声明中使用abstract和public关键字是多余的,即使编译器允许,因为接口中的方法是隐式抽象和公共的。在不改变其含义的情况下,前面的Player接口声明可以重写如下:
public interface Player {
void play();
void stop();
void forward();
void rewind();
}
接口中的抽象方法声明可能包括参数、返回类型和一个throws子句。下面的代码片段声明了一个ATM接口。它声明了四个方法。如果账户信息错误,login()方法抛出一个AccountNotFoundException。当用户试图提取一笔金额时,withdraw()方法抛出一个InsufficientBalanceException,这会将余额减少到低于所需最小余额的金额:
public interface ATM {
boolean login(int account) throws AccountNotFoundException;
boolean deposit(double amount);
boolean withdraw(double amount) throws InsufficientBalanceException;
double getBalance();
}
接口的抽象方法由实现该接口的类继承,类重写它们以提供实现。这意味着接口中的抽象方法不能被声明为 final,因为方法声明中的关键字final表示该方法是 final,并且不能被覆盖。然而,一个类可以声明一个接口 final 的重写方法,表明子类不能重写该方法。
静态方法声明
让我们参考清单 21-5 中显示的Walkables类的代码。它是一个包含名为letThemWalk()的静态方法的实用程序类。在 Java 8 之前,创建这样一个实用程序类来提供使用接口的静态方法是很常见的。你会在 Java 库中找到许多接口/实用程序类对,例如,Collection/Collections、Path/Paths、Channel/Channels、Executor/Executors等。遵循这个约定,您将您的接口/实用程序类对命名为Walkable/Walkables。Java 设计者意识到了额外的实用程序类和接口的必要性。在 Java 8 中,接口中可以有静态方法。静态方法的声明包含static修饰符。它们是隐式公共的。您可以重新定义Walkable接口,如清单 21-10 所示,以包含letThemWalk()方法并完全去掉Walkables类。
// Walkable.java
package com.jdojo.interfaces;
public interface Walkable {
// An abstract method
void walk();
// A static convenience method
public static void letThemWalk(Walkable[] list) {
for (Walkable w : list) {
w.walk();
}
}
}
Listing 21-10The Revised Walkable Interface with an Additional Static Convenience Method
您可以使用点标记法来使用接口的静态方法:
<interface-name>.<static-method>
下面的代码片段调用了Walkable.letThemWalk()方法:
Walkable[] w = new Walkable[4];
w[0] = new Person("Jack");
w[1] = new Duck("Jeff");
w[2] = new Person("John");
w[3] = new Cat("Jace");
// Let everyone walk
Walkable.letThemWalk(w);
Jack (a person) is walking.
Jeff (a duck) is walking.
John (a person) is walking.
Jace (a cat) is walking.
与类中的静态方法不同,接口中的静态方法不会被实现的类或子接口继承。从另一个接口继承的接口称为子接口。只有一种方法可以调用接口的静态方法:使用接口名。必须使用I.m()调用接口I的静态方法m()。您可以使用方法的非限定名m()来调用它,只在接口体中或者当您使用静态import语句导入方法时。
默认方法声明
接口中的默认方法是用修饰符default声明的。默认方法为实现接口的类提供方法的默认实现,但不重写默认方法。
默认方法是在 Java 8 中引入的。在 Java 8 之前,接口只能有抽象方法。为什么在 Java 8 中增加了默认方法?简而言之,添加它们是为了让现有的接口可以发展。在这一点上,答案可能很难理解。让我们看一个例子来澄清这一点。
假设在 Java 8 之前,您想为可移动对象创建一个规范来描述它们在 2D 平面中的位置。让我们通过创建一个名为Movable的接口来创建规范,如清单 21-11 所示。
// Movable.java
package com.jdojo.interfaces;
public interface Movable {
void setX(double x);
void setY(double y);
double getX();
double getY();
}
Listing 21-11A Movable Interface
该接口声明了四个抽象方法。setX()和setY()方法让Movable使用绝对定位改变位置。getX()和getY()方法根据x和y坐标返回当前位置。
考虑清单 21-12 中的Pen类。它实现了Movable接口,并且作为规范的一部分,它为接口的四个方法提供了实现。该类包含两个实例变量,名为x和y,用于跟踪笔的位置。
// Pen.java
package com.jdojo.interfaces;
public class Pen implements Movable {
private double x;
private double y;
public Pen() {
// By default, the pen is at (0.0, 0.0)
}
public Pen(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public void setX(double x) {
this.x = x;
}
@Override
public void setY(double y) {
this.y = y;
}
@Override
public double getX() {
return x;
}
@Override
public double getY() {
return y;
}
@Override
public String toString() {
return "Pen(" + x + ", " + y + ")";
}
}
Listing 21-12A Pen Class That Implements the Movable Interface
以下代码片段使用了Movable接口和Pen类:
// Create a Pen and assign its reference to a Movable variable
Movable p1 = new Pen();
System.out.println(p1);
// Move the Pen
p1.setX(10.0);
p1.setY(5.0);
System.out.println(p1);
Pen(0.0, 0.0)
Pen(10.0, 5.0)
到目前为止,Movable接口和Pen类没有什么特别之处。假设Movable接口是你开发的库的一部分。您已经将该库分发给您的客户。客户已经在他们的类中实现了Movable接口。
现在故事发生了转折。一些客户要求Movable界面包含使用相对坐标改变位置的规范。他们希望您向Movable接口添加一个move()方法,如下所示。请求的部分以粗体显示:
public interface Movable {
void setX(double x);
void setY(double y);
double getX();
double getY();
void move(double deltaX, double deltaY);
}
你是一个很好的商人;你总是想要一个快乐的顾客。你满足了顾客的要求。您进行更改并重新分发新版本的库。几小时后,你接到几个愤怒的顾客打来的电话。他们很生气,因为新版本的库破坏了他们现有的代码。我们来分析一下哪里出了问题。
在 Java 8 之前,接口中的所有方法都是隐式抽象的。因此,新方法move()是一个抽象方法。所有实现Movable接口的类都必须提供新方法的实现。注意,客户已经有了几个类,例如,Pen类,它实现了Movable接口。除非将新方法添加到这些类中,否则所有这些类都不会再编译。这个故事的寓意是,在 Java 8 之前,如果不破坏现有的代码,就不可能在发布给公众的接口上添加方法。
Java 库已经发布了数百个接口,这些接口被世界各地的客户使用了数千次。Java 设计者迫切需要一种在不破坏现有代码的情况下改进现有接口的方法。他们探索了几种解决方案。默认方法是演化接口的公认解决方案。默认方法可以添加到现有接口中。它为方法提供了默认实现。所有实现接口的类都将继承默认实现,因此不会破坏它们。类可以选择重写默认实现。
使用关键字default声明默认方法。默认方法不能声明为抽象或静态。它必须提供一个实现。否则,会发生编译时错误。清单 21-13 显示了Movable接口的修改代码。它包含一个名为move()的默认方法,该方法是根据现有的四种方法定义的。
// Movable.java
package com.jdojo.interfaces;
public interface Movable {
void setX(double x);
void setY(double y);
double getX();
double getY();
// A default method
default void move(double deltaX, double deltaY) {
double newX = getX() + deltaX;
double newY = getY() + deltaY;
setX(newX);
setY(newY);
}
}
Listing 21-13The Movable Interface with a Default Method
任何实现了Movable接口的现有类,包括Pen类,都将像以前一样继续编译和工作。新的move()方法及其默认实现可用于所有这些类。清单 21-14 展示了Movable与Pen类接口的新旧方法。
// MovableTest.java
package com.jdojo.interfaces;
public class MovableTest {
public static void main(String[] args) {
// Create a Pen and assign its reference to a Movable variable
Movable p1 = new Pen();
System.out.println(p1);
// Move the Pen using absolute coordinates
p1.setX(10.0);
p1.setY(5.0);
System.out.println(p1);
// Move the Pen using relative coordinates
p1.move(5.0, 2.0);
System.out.println(p1);
}
}
Pen(0.0, 0.0)
Pen(10.0, 5.0)
Pen(15.0, 7.0)
Listing 21-14Testing the New Movable Interface with the Existing Pen Class
默认方法的另一个常见用途是在接口中声明可选方法。考虑一个Named接口,如清单 21-15 所示。
// Named.java
package com.jdojo.interfaces;
public interface Named {
void setName(String name);
default String getName() {
return "John Doe";
}
default void setNickname(String nickname) {
throw new UnsupportedOperationException("setNickname");
}
default String getNickname() {
throw new UnsupportedOperationException("getNickname");
}
}
Listing 21-15A Named Interface Using Default Methods to Provide Optional Methods
该接口提供了获取和设置正式名称和昵称的规范。不是所有的东西都有昵称。该接口提供了获取和设置昵称的方法作为默认方法,使它们成为可选方法。如果一个类实现了Named接口,它可以覆盖setNickname()和getNickname()方法来提供实现,如果这个类支持昵称的话。否则,该类不必为这些方法做任何事情。它们只是抛出一个运行时异常来表明它们不受支持。该接口将getName()方法声明为默认方法,并通过返回"John Doe"作为默认名称来为其提供一个合理的默认实现。实现Named接口的类应该覆盖getName()方法以返回真实名称。
就默认方法给 Java 语言带来的好处和能力而言,这只是冰山一角。它赋予了现有 Java APIs 新的生命。在 Java 8 中,默认方法被添加到 Java 库中的几个接口中,以便为现有的 API 提供更多的表达能力和功能。
类中的具体方法和接口中的默认方法有什么异同?
-
两者都提供了一个实现。
-
两者都以相同的方式访问关键字
this。也就是说,关键字this是在其上调用方法的对象的引用。 -
主要区别在于对对象状态的访问。类中的具体方法可以访问该类的实例变量。但是,默认方法不能访问实现该接口的类的实例变量。默认方法可以访问接口的其他成员,例如,其他方法、常量和类型成员。例如,
Movable接口中的默认方法是使用其他成员方法getX()、getY()、setX()和setY()编写的。 -
不用说,这两种类型的方法都可以使用它们的参数。
-
两种方法都可以有一个
throws子句。
我们还没有使用完默认方法。我们将很快讨论它们在继承中的作用。
接口中的私有方法
JDK 8 为接口引入了静态和默认方法。如果您必须在这些方法中多次执行相同的逻辑,您别无选择,只能重复该逻辑或将该逻辑移动到另一个类来隐藏实现。考虑名为Alphabet的接口,如清单 21-16 所示。
// Alphabet.java
package com.jdojo.interfaces;
public interface Alphabet {
default boolean isAtOddPos(char c) {
if (!Character.isLetter(c)) {
throw new RuntimeException("Not a letter: " + c);
}
char uc = Character.toUpperCase(c);
int pos = uc - 64;
return pos % 2 == 1;
}
default boolean isAtEvenPos(char c) {
if (!Character.isLetter(c)) {
throw new RuntimeException("Not a letter: " + c);
}
char uc = Character.toUpperCase(c);
int pos = uc - 64;
return pos % 2 == 0;
}
}
Listing 21-16An Interface Named Alphabet Having Two Default Methods Sharing Logic
isAtOddpos()和isAtEvenPos()方法检查指定的字符在字母顺序上是奇数还是偶数,假设我们只处理英文字母。该逻辑假设'A'和'a'在位置 1,'B'和'b'在位置 2,依此类推。注意,两种方法中的逻辑仅在return语句中有所不同。除了最后的语句之外,这些方法的整体是相同的。我们需要重构这个逻辑。理想的情况是将公共逻辑转移到另一个方法中,并从两个方法中调用新方法。然而,你不希望在 JDK 8 中这样做,因为接口只支持公共方法。这样做会使第三种方法公开化,从而暴露给外界,这是你不想做的。
Java 9 帮了大忙,它允许你在接口中声明私有方法。清单 21-17 显示了使用私有方法的Alphabet接口的重构版本,该私有方法包含两个方法使用的公共逻辑。这一次,我们将接口命名为AlphabetJdk9,只是为了确保我可以在源代码中包含两个版本。这两种现有的方法变成了一行程序。
// AlphabetJdk9.java
package com.jdojo.interfaces;
public interface AlphabetJdk9 {
default boolean isAtOddPos(char c) {
return getPos(c) % 2 == 1;
}
default boolean isAtEvenPos(char c) {
return getPos(c) % 2 == 0;
}
private int getPos(char c) {
if (!Character.isLetter(c)) {
throw new RuntimeException("Not a letter: " + c);
}
char uc = Character.toUpperCase(c);
int pos = uc - 64;
return pos;
}
}
Listing 21-17An Interface Named AlphabetJdk9 That Uses a Private Method
在 JDK 9 之前,接口中的所有方法都是隐式公共的。记住这些适用于所有 Java 程序的简单规则:
-
一个
private方法没有被继承,因此不能被覆盖。 -
不能覆盖
final方法。 -
一个
abstract方法被继承并且意味着被覆盖。 -
一个
default方法是一个实例方法,并提供一个默认的实现;它可以被覆盖。
在接口中声明方法时,您需要遵循一些规则。不支持所有修饰符组合— abstract、public、private、static和final—因为它们没有意义。表 21-1 列出了接口方法声明中支持和不支持的修饰符组合。注意在接口的方法声明中不允许使用final修饰符。根据这个列表,您可以在一个接口中拥有一个私有方法,它可以是非抽象的、非默认的实例方法,也可以是静态方法。
表 21-1
接口中方法声明中支持的修饰符
|修饰语
|
支持?
|
描述
|
| --- | --- | --- |
| public static | 是 | 从 JDK 8 开始支持。 |
| public abstract | 是 | 从 JDK 1 开始支持。 |
| public default | 是 | 从 JDK 8 开始支持。 |
| private static | 是 | 从 JDK 9 开始支持。 |
| Private | 是 | 从 JDK 9 开始支持。这是非抽象的实例方法。 |
| private abstract | 不 | 这种组合没有意义。私有方法不是继承的,所以它不能被重写,而抽象方法必须被重写才能使用。 |
| private default | 不 | 这种组合没有意义。私有方法不被继承,所以它不能被重写,而默认方法则意味着在需要时被重写。 |
嵌套类型声明
接口中的嵌套类型声明定义了一个新的引用类型。您可以将类、接口、枚举和批注声明为嵌套类型。我们还没有讨论 enum 和 annotation,所以我们将在本节中讨论嵌套接口和类。在接口内部声明的接口/类称为嵌套接口/类。
接口和类定义新的引用类型,嵌套接口和嵌套类也是如此。有时一个类型作为嵌套类型更有意义。假设您有一个ATM接口,并且您想要定义另一个名为ATMCard的接口。ATMCard接口可以定义为ATM的顶层接口或嵌套接口。由于ATM卡与ATM一起使用,将ATMCard定义为ATM接口的嵌套接口可能更有意义。因为您将ATMCard定义为ATM的嵌套接口,所以您也可以将"ATM"从其名称中删除,您可以将其命名为Card,如图所示:
public interface ATM {
boolean login(int account) throws AccountNotFoundException;
boolean deposit(double amount);
boolean withdraw(double amount) throws InsufficientFundsException;
double getBalance();
// Card is a nested interface. You can omit the keywords public and static.
public static interface Card {
String getNumber();
String getSecurityCode();
LocalDate getExpirationDate();
String getCardHolderName();
}
}
嵌套接口总是通过其封闭接口来访问。在前面的代码片段中,ATM是一个顶级接口(或者简单地说是一个接口),而Card是一个嵌套接口。ATM接口也被称为Card接口的封闭接口。ATM和Card接口的全限定名分别是com.jdojo.interfaces.ATM和com.jdojo.interfaces.ATM.Card。所有嵌套类型都是隐式公共和静态的。前面的代码片段使用了关键字public和static来声明ATMCard接口,这是多余的。
也可以在接口中声明嵌套类。除非您知道如何实现接口,否则您可能无法理解本节中描述的嵌套类的用法,下一节将对此进行描述。下面的讨论是为了完成关于接口嵌套类型的讨论。在阅读了接下来几节中关于如何实现接口的内容后,您可以再次阅读本节。
在接口中声明嵌套类并不常见。但是,如果您发现一个接口将嵌套类声明为其成员,您应该不会感到惊讶。在一个接口中有一个嵌套类有什么好处?这样做只有一个好处,就是更好地组织相关实体:接口和类。假设您想开发一个Job界面,让用户向作业调度器提交作业。以下是Job接口的代码:
public interface Job {
void runJob();
}
假设每个部门必须每天提交一个作业,即使他们没有要运行的东西。它表明,有时你需要一份空工作或一份无事可做的工作。接口Job的开发者可能会提供一个常量,它代表清单 21-18 中列出的Job接口的一个简单实现。它有一个名为EmptyJob的嵌套类,实现了封闭的Job接口。
// Job.java
package com.jdojo.interfaces;
public interface Job {
// A nested class
class EmptyJob implements Job {
private EmptyJob() {
// Do not allow outside to create its object
}
@Override
public void runJob() {
System.out.println("Nothing serious to run...");
}
}
// A constant field
Job EMPTY_JOB = new EmptyJob();
// An abstract method
void runJob();
}
Listing 21-18The Job Interface with a Nested Class and a Constant Field
如果一个部门没有要提交的有意义的作业,它可以使用Job.EMPTY_JOB常量作为作业。EmptyJob类的全限定名是com.jdojo.interfaces.Job.EmptyJob。注意,封闭接口Job为EmptyJob类提供了一个额外的名称空间。在Job接口内部,EmptyJob类可以通过它的简单名称EmptyJob来引用。但是在Job接口之外,必须简称为Job.EmptyJob。您可能会注意到一个普通的作业对象由Job.EMPTY_JOB常量表示。您已经将EmptyJob嵌套类的构造器设为私有,因此接口之外的任何人都不能创建它的对象。清单 21-19 展示了如何使用这个类。通常,在这种情况下,您会将Job.EmptyJob类的构造器设为私有,这样它的对象就不能在Job接口之外创建,因为EMPTY_JOB常量已经提供了这个类的一个对象。
// JobTest.java
package com.jdojo.interfaces;
public class JobTest {
public static void main(String[] args) {
submitJob(Job.EMPTY_JOB);
}
public static void submitJob(Job job) {
job.runJob();
}
}
Nothing serious to run...
Listing 21-19A Test Program to Test the Job Interface and Its Nested EmptyJob Class
接口定义了一个新的类型
接口定义了一个新的引用类型。您可以在任何可以使用引用类型的地方使用接口类型。例如,您可以使用接口类型来声明变量(实例、静态或局部),或者在方法中声明参数类型,作为方法的返回类型,等等。
考虑下面名为Swimmable的接口声明,它声明了一个方法swim(),如清单 21-20 所示。清单 21-21 中的SwimmableTest类展示了如何使用Swimmable接口作为参考数据类型。
// SwimmableTest.java
package com.jdojo.interfaces;
public class SwimmableTest {
// Interface type to define instance variable
private Swimmable iSwimmable;
// Interface type to define parameter type for a constructor
public SwimmableTest(Swimmable aSwimmable) {
this.iSwimmable = aSwimmable;
}
// Interface type to define return type of a method
public Swimmable getSwimmable() {
return this.iSwimmable;
}
// Interface type to define parameter type for a method
public void setSwimmable(Swimmable newSwimmable) {
this.iSwimmable = newSwimmable;
}
public void letItSwim() {
// Interface type to declare a local variable
Swimmable localSwimmable = this.iSwimmable;
// An interface variable can be used to invoke any method
// declared in the interface and the Object class
localSwimmable.swim();
}
}
Listing 21-21A Test Class That Demonstrates the Use of an Interface Type as a Variable Type
// Swimmable.java
package com.jdojo.interfaces;
public interface Swimmable {
void swim();
}
Listing 21-20The Declaration for a Swimmable Interface
SwimmableTest类以多种方式使用由Swimmable接口定义的新类型。该类的目的只是演示如何使用一个新类型的接口。它使用Swimmable接口作为类型来声明以下内容:
-
名为
iSwimmable的实例变量。 -
为其构造器命名为
aSwimmable的参数。 -
其
getSwimmable()方法的返回类型。 -
为其
setSwimmable()方法命名为newSwimmable的参数。 -
在其
letItSwim()方法内名为localSwimmable的局部变量。在方法内部,您可以直接在实例变量iSwimmable上调用swim()。我们使用局部变量只是为了证明接口类型可以用在任何可以使用类型的地方。
此时,需要回答两个关于接口的问题:
-
接口类型的变量指的是内存中的什么对象?
-
你能用一个接口类型的变量做什么?
因为接口定义了引用类型,那么接口类型的变量引用内存中的什么对象呢?让我们用一个例子来展开这个问题。您有一个Swimmable接口,您可以声明一个类型为Swimmable的引用变量,如下所示:
Swimmable sw;
此时变量sw的值是多少?引用数据类型的变量引用内存中的对象。准确的说,让我们把问题重新表述为“内存中的什么对象sw指的是?”在这一点上,你不能完全回答这个问题。不完整且无法解释的答案是,接口类型的变量指的是内存中的对象,该对象的类实现了该接口。当我们在下一节中讨论实现接口时,答案将会更加清晰。您不能创建接口类型的对象。接口是隐式抽象的,它没有构造器。也就是说,不能使用带有 new 运算符的接口类型来创建对象。以下代码无法编译:
Swimmable sw2 = new Swimmable(); // A compile-time error
在这个语句中,使用new操作符会导致编译时错误,而不是Swimmable sw2部分。Swimmable sw2部分是变量声明,是有效的。
但是,有一点是肯定的:一个接口类型的变量可以引用内存中的一个对象。该场景如图 21-1 所示。
图 21-1
引用内存中对象的可游泳类型变量(sw)
我们来回答第二个问题。你能用一个接口类型的变量做什么?引用类型变量的所有规则同样适用于接口类型的变量。您可以对引用类型的变量做以下几件重要的事情:
-
您可以在内存中分配一个对象的引用,包括一个
null引用值: -
您可以使用接口类型的变量或直接使用接口名称来访问接口中声明的任何常量字段。最好使用接口名来访问接口的常量。考虑带有两个常量
YES和NO的Choices接口。您可以使用接口的简单名称Choices.YES和Choices.NO并使用接口引用变量sw2.YES和sw2.NO来访问这两个常量的值。 -
您可以使用接口类型的变量来调用接口中声明的任何方法。例如,
Swimmable类型的变量可以调用swim()方法,如下所示:
Swimmable sw2 = null;
- 接口类型的变量可以调用
Object类的任何方法。这个规律不是很明显。然而,如果你仔细想想,这是一个非常简单而重要的规则。接口类型的变量可以引用内存中的对象。无论它引用内存中的什么对象,该对象总是属于类类型。Java 中的所有类都必须将Object类作为它们的直接/间接父类。因此,Java 中的所有对象都可以访问Object类的所有方法。因此,允许一个接口类型的变量访问Object类的所有方法是合乎逻辑的。下面的代码片段使用一个Swimmable类型的变量调用了Object类的hashCode()、getClass()和toString()方法:
Swimmable sw3 = get an object instance of the Swimmable type...
sw3.swim();
- 另一个需要记住的重要规则是,默认情况下,接口类型的实例或静态变量被初始化为
null。与所有类型的局部变量一样,默认情况下,接口类型的局部变量不会初始化。在使用它之前,您必须显式地为它赋值。
Swimmable sw4 = get a Swimmable type object...
int hc = sw4.hashCode();
Class c = sw4.getClass();
String str = sw4.toString();
实现接口
接口定义了对象与其他对象通信方式的规范。规范是对象行为的契约或协议。理解两个术语规范(或合同)和实施的区别是非常重要的。规范是一组语句,而实现是这些语句的实现。
让我们举一个真实世界的例子。语句“杰克将在 2014 年 6 月 8 日给约翰十美元”是一个规范。当杰克在 2014 年 6 月 8 日给约翰十美元时,规范被执行。你可以重新表述为,当 2014 年 6 月 8 日杰克给约翰十美元时,规范实现了。有时,在讨论接口时,规范也被称为合同、协议、协定、计划或草案。无论你用哪个术语来指代一个规范,它总是抽象的。规范的实现可以是部分的,也可以是完整的。杰克可能在 2014 年 6 月 8 日给约翰七美元;而且,在这种情况下,规范还没有完全实现。
接口指定了一个对象在与其他对象交互时保证提供的协议。它根据抽象和默认方法来指定协议。规范是在某个时候由某人实现的,接口也是如此。接口是由类实现的。当一个类实现一个接口时,该类为该接口的所有抽象方法提供实现。类可以提供接口抽象方法的部分实现,在这种情况下,类必须声明自己是抽象的。
实现一个接口(或多个接口)的类使用一个implements子句来指定接口的名称。一个implements子句由关键字implements组成,后跟一个逗号分隔的接口类型列表。一个类可以实现多个接口。现在让我们关注一个只实现一个接口的类。实现接口的类声明的一般语法如下所示:
[modifiers] class <class-Name> implements <comma-separated-list-of-interfaces> {
// Class body goes here
}
假设有一个Fish类:
public class Fish {
// Code for Fish class goes here
}
现在,您想在Fish类中实现Swimmable接口。下面的代码显示了Fish类,并声明它实现了Swimmable接口:
public class Fish implements Swimmable {
// Code for the Fish class goes here
}
粗体文本显示了更改后的代码。这个Fish类的代码不会被编译。一个类从它实现的接口继承所有的抽象和默认方法。因此,Fish类从Swimmable接口继承了抽象的swim()方法。如果一个类包含(继承的或声明的)抽象方法,它必须声明为抽象的。你还没有声明Fish类的抽象。这就是前面的声明无法编译的原因。在下面的代码中,Fish类覆盖了swim()方法以提供一个实现:
public class Fish implements Swimmable {
// Override and implement the swim() method
@Override
public void swim() {
// Code for swim method goes here
}
// More code for the Fish class goes here
}
实现接口的类必须重写以实现接口中声明的所有抽象方法。否则,该类必须声明为抽象的。请注意,接口的默认方法也由实现类继承。实现类可以选择(但不是必需的)覆盖默认方法。接口中的静态方法不会被实现类继承。
实现接口的类可以有其他不从实现的接口继承的方法。其他方法可以与实现的接口中声明的方法具有相同的名称和不同数量和/或类型的参数。
在这种情况下,对Fish类的唯一要求是它必须有一个swim()方法,该方法不接受任何参数并返回在Swimmable接口中声明的void。下面的代码定义了Fish类中的两个swim()方法。第一个没有参数的方法实现了Swimmable接口的swim()方法。第二个,swim(double distanceInYards),与Fish类实现的Swimmable接口无关:
public class Fish implements Swimmable {
// Override the swim() method in the Swimmable interface
@Override
public void swim() {
// More code goes here
}
// A valid method for the Fish class. This method declaration has nothing to do
// with the Swimmable interface's swim() method
public void swim(double distanceInYards) {
// More code goes here
}
}
清单 21-22 显示了Fish类的完整代码。一个Fish对象将有一个名字,这个名字在它的构造器中提供。它实现了 Swimmable 接口的swim()方法,以在标准输出中打印一条消息。
// Fish.java
package com.jdojo.interfaces;
public class Fish implements Swimmable {
private String name;
public Fish(String name) {
this.name = name;
}
@Override
public void swim() {
System.out.println(name + " (a fish) is swimming.");
}
}
Listing 21-22Code for the Fish Class That Implements the Swimmable Interface
如何创建实现接口的类的对象?不管一个类是否实现了一个接口,你都可以用同样的方式创建一个类的对象(通过使用带有构造器的new操作符)。您可以创建一个Fish类的对象,如下所示:
// Create an object of the Fish class
Fish fifi = new Fish("Fifi");
当您执行语句new Fish("Fifi")时,它会在内存中创建一个对象,该对象的类型是Fish(由其类定义的类型)。当一个类实现一个接口时,它的对象就多了一个类型,就是被实现的接口定义的类型。在您的例子中,通过执行创建的对象有两种类型:Fish和Swimmable。事实上,它还有第三种类型,那就是Object类型,因为Fish类继承了Object类,这是默认情况下发生的。由于一个Fish类的对象有两种类型,即Fish和Swimmable,你可以将一个Fish对象的引用分配给一个Fish类型的变量以及一个Swimmable类型的变量。以下代码对此进行了总结:
Fish guppi = new Fish("Guppi");
Swimmable hilda = new Fish("Hilda");
变量guppi属于Fish类型。它指的是Guppi鱼的对象。在第一次赋值中没有什么惊奇的,一个Fish对象被赋值给一个Fish类型的变量。第二个赋值也是有效的,因为Fish类实现了Swimmable接口,并且Fish类的每个对象也是Swimmable类型。此时hilda是一个Swimmable类型的变量。它指的是 Hilda fish 对象。以下赋值始终有效:
// A Fish is always Swimmable
hilda = guppi;
但是,另一种方式是不成立的。将Swimmable类型的变量赋给Fish类型的变量会产生编译时错误:
// A Swimmable is not always a Fish
guppi = hilda; // A compile-time error
为什么前面的赋值会产生编译时错误?原因很简单。因为Fish类实现了Swimmable接口,所以Fish类的对象总是Swimmable。因为一个Fish类型的变量只能引用一个Fish对象,这个对象总是Swimmable,赋值hilda = guppi总是有效的。然而,Swimmable类型的变量可以引用其类实现了Swimmable接口的任何对象,不一定只引用Fish对象。例如,考虑一个类Turtle,它实现了Swimmable接口:
public class Turtle implements Swimmable {
@Override
public void swim() {
System.out.println("A turtle can swim too!");
}
}
您可以将一个Turtle类的对象分配给hilda变量:
hilda = new Turtle(); // OK. A Turtle is always Swimmable
如果此时允许赋值guppi = hilda,那么Fish变量guppi将指向Turtle对象!这将是一场灾难。Java 运行时会抛出一个异常,即使编译器允许这种赋值。这种赋值是不允许的,原因如下:
鱼总是可以游泳的。然而,并不是每个游泳者都是鱼。
假设您(以编程方式)确定Swimmable类型的变量包含对Fish对象的引用。如果你想把Swimmable类型的变量赋给Fish类型的变量,你可以通过使用类型转换来实现,如下所示:
// The compiler will pass it. The runtime may throw a ClassCastException.
guppi = (Fish)hilda;
编译器不会抱怨这个语句。它假设您已经确保了hilda变量引用了一个Fish对象,并且强制转换(Fish) hilda将在运行时成功。如果万一hilda变量没有引用Fish对象,Java 运行时将抛出一个ClassCastException。例如,下面的代码片段将通过编译器检查,但会抛出一个ClassCastException:
Fish fred = new Fish("Fred");
Swimmable turti = new Turtle();
// OK for the compiler, but not OK for the runtime. turti is a Turtle, not a Fish at
// runtime. fred can refer to only a Fish, not a Turtle
fred = (Fish) turti;
清单 21-23 展示了让您测试Fish类和Swimmable接口的简短而完整的代码。
// FishTest.java
package com.jdojo.interfaces;
public class FishTest {
public static void main(String[] args) {
Swimmable finny = new Fish("Finny");
finny.swim();
}
}
Finny (a fish) is swimming.
Listing 21-23Demonstrating That a Variable of an Interface Can Store the Reference of the Object of the Class Implementing the Interface
实现接口方法
当一个类完全实现一个接口时,它通过重写这些方法为接口的所有抽象方法提供一个实现。接口中的方法声明包括方法的约束(或规则)。例如,一个方法可以在其声明中声明一个throws子句。方法声明中的throws子句是该方法的约束。如果throws子句声明了一些检查过的异常,该方法的调用者必须准备好处理它们。接口中的方法是隐式公共的。这为方法定义了另一个约束,即接口的所有方法都可以公开访问,并假设接口本身可以公开访问。考虑一个Banker接口,定义如下:
public interface Banker {
double withdraw(double amount) throws InsufficientFundsException;
void deposit(double amount) throws FundLimitExceededException;
}
Banker接口声明了两个名为withdraw()和deposit()的方法。考虑下面在MinimumBalanceBank类中Banker接口的实现。该类中被覆盖的方法具有与在Banker接口中定义的相同的约束。这两个方法都被声明为public,,并且都抛出了与在Banker接口中声明的相同的异常:
public class MinimumBalanceBank implements Banker {
public double withdraw(double amount) throws InsufficientFundsException {
// Code for this method goes here
}
public void deposit(double amount) throws FundLimitExceededException {
// Code for this method goes here
}
}
考虑下面的NoLimitBank类中Banker接口的实现。NoLimitBank规定客户可以无限透支(但愿这发生在现实中),并且余额没有上限。NoLimitBank在覆盖Banker接口的withdraw()和deposit()方法时删除了throws子句:
public class NoLimitBank implements Banker {
public double withdraw(double amount) {
// Code for this method goes here
}
public void deposit(double amount) {
// Code for this method goes here
}
}
尽管覆盖了Banker接口方法的两个方法删除了throws子句,但是NoLimitBank的代码是有效的。当一个类重写一个接口方法时,删除约束(在这种情况下是异常)是允许的。throws子句中的异常强加了一个限制,即调用者必须处理异常。如果您使用Banker类型编写代码,下面是您调用withdraw()方法的方式:
Banker b = get a Banker type object...;
try {
double amount = b.withdraw(1000.90);
// More code goes here
} catch (InsufficientFundsException e) {
// Handle the exception here
}
在编译时,当调用b.withdraw()方法时,编译器强迫你处理从withdraw()方法抛出的异常,因为它知道变量b的类型是Banker,而Banker类型的withdraw()方法抛出一个InsufficientFundsException。如果在前面的代码中将一个对象NoLimitBank赋给变量b,那么当调用b.withdraw()时,不会从NoLimitBank类的withdraw()方法中抛出异常,即使对withdraw()方法的调用是在try-catch块中。编译器无法检查变量b的运行时类型。其安全检查基于变量b的编译时类型。如果运行时抛出的异常量量比代码中预期的要少或者没有异常,这永远不会成为问题。考虑下面由UnstablePredictableBank类实现的Banker接口:
// The following code will not compile
public class UnstablePredictableBank implements Banker {
public
double withdraw(double amount) throws InsufficientFundsException, ArbitraryException {
// Code for this method goes here
}
public void deposit(double amount) throws FundLimitExceededException {
// Code for this method goes here
}
}
这一次,withdraw()方法添加了一个新的异常,ArbitraryException,它向被覆盖的方法添加了一个新的约束。绝不允许向被重写的方法添加约束。考虑以下代码片段:
Banker b = new UnstablePredictableBank();
try {
double amount = b.withdraw(1000.90);
// More code goes here
} catch (InsufficientFundsException e) {
// Handle exception here
}
编译器不知道,在运行时,Banker类型的变量b将引用UnstablePredictableBank类型的对象。因此,当你调用b.withdraw()时,编译器会强迫你只处理InsufficientFundsException。运行时从withdraw()方法抛出ArbitraryException会发生什么?您的代码还没有准备好处理它。这就是为什么您不能向类中的方法声明添加新的异常,这将重写其实现的接口中的方法。
如果一个类重写了实现的接口的方法,该方法必须声明为公共的。回想一下,接口中的所有方法都是隐式公共的,public 是对方法限制最少的范围修饰符。将重写接口方法的类中的方法声明为私有、受保护或包级别,就像限制被重写方法的范围一样(就像放置更多约束)。由于withdraw()和deposit()方法未声明为公共方法,以下代码片段将无法编译:
// Code would not compile
public class UnstablePredictableBank implements Banker{
// withdraw() method must be public
private double withdraw(double amount) throws InsufficientFundsException {
// Code for this method goes here
}
// deposit() method must be public
protected void deposit(double amount) throws FundLimitExceededException {
// Code for this method goes here
}
}
使用一般的经验法则来检查在类方法中是否允许添加或删除约束,这将重写接口的方法。使用分配给实现接口的类的对象的接口类型变量编写代码。如果代码对您有意义(当然,对编译器也有意义),它是允许的。否则是不允许的。假设J是一个接口,它声明了一个方法m1()。假设类C实现了接口J,并且修改了方法m1()的声明。如果下面的代码有意义并且可以编译,那么在类C中的m1()声明中的修改是正确的:
J obj = new C(); // Or any object of any subclass of C
obj.m1();
另一个经验法则是查看类中的重写方法是否放松了接口中为同一方法声明的限制。如果重写方法放松了被重写方法的约束,那就没问题。否则,编译器将生成错误。
实现多个接口
一个类可以实现多个接口。类实现的所有接口都列在类声明中的关键字implements之后。接口名称由逗号分隔。通过实现多个接口,该类同意为所有接口中的所有抽象方法提供实现。假设有两个名为Adder和Subtractor的接口,声明如下:
public interface Adder {
int add(int n1, int n2);
}
public interface Subtractor {
int subtract(int n1, int n2);
}
如果一个ArithOps类实现了这两个接口,它的声明如下所示:
public class ArithOps implements Adder, Subtractor {
// Override the add() method of the Adder interface
@Override
public int add(int n1, n2) {
return n1 + n2;
}
// Override the subtract() method of the Subtractor interface
@Override
public int subtract(int n1, int n2) {
return n1 - n2;
}
// Other code for the class goes here
}
一个类实现的接口的最大数量没有限制。当一个类实现一个接口时,它的对象获得一个额外的类型。如果一个类实现了多个接口,那么它的对象获得的新类型就和实现的接口一样多。考虑一下ArithOps类的对象,它可以通过执行new ArithOps()来创建。ArithOps类的对象获得了两个额外的类型,分别是Adder和Subtractor。下面的代码片段显示了ArithOps类的对象获得两个新类型的结果。您可以将ArithOps的对象视为ArithOps类型、Adder类型或Subtractor类型。当然,Java 中的每个对象都可以被视为一个Object类型:
ArithOps a = new ArithOps();
Adder b = new ArithOps();
Subtractor c = new ArithOps();
b = a;
c = a;
让我们看一个更具体更完整的例子。您已经有了两个接口,Walkable和Swimmable。如果一个类实现了Walkable接口,它必须提供walk()方法的实现。如果你希望一个类的对象被视为Walkable类型,这个类将实现Walkable接口。同样的理由也适用于Swimmable接口。如果一个类实现了两个接口Walkable和Swimmable,那么它的对象可以被视为Walkable类型和Swimmable类型。该类必须做的唯一一件事是为walk()和swim()方法提供实现。让我们创建一个Turtle类,它实现了这两个接口。一个Turtle物体将具有行走和游泳的能力。
清单 21-24 包含了Turtle类的代码。乌龟也会咬人。通过向Turtle类添加一个bite()方法,您已经向Turtle对象添加了这个行为。注意,将bite()方法添加到Turtle类与这两个接口的实现无关。实现接口的类可以拥有任意数量的自己的附加方法。
// Turtle.java
package com.jdojo.interfaces;
public class Turtle implements Walkable, Swimmable {
private String name;
public Turtle(String name) {
this.name = name;
}
// Adding a bite() method to the Turtle class
public void bite() {
System.out.println(name + " (a turtle) is biting.");
}
// Implementation for the walk() method of the Walkable interface
@Override
public void walk() {
System.out.println(name + " (a turtle) is walking.");
}
// Implementation for the swim() method of the Swimmable interface
@Override
public void swim() {
System.out.println(name + " (a turtle) is swimming.");
}
}
Listing 21-24A Turtle Class, Which Implements the Walkable and Swimmable Interfaces
清单 21-25 显示了使用一个Turtle对象作为Turtle类型、Walkable类型和Swimmable类型。
// TurtleTest.java
package com.jdojo.interfaces;
public class TurtleTest {
public static void main(String[] args) {
Turtle turti = new Turtle("Turti");
// Using Turtle type as Turtle, Walkable and Swimmable
letItBite(turti);
letItWalk(turti);
letItSwim(turti);
}
public static void letItBite(Turtle t) {
t.bite();
}
public static void letItWalk(Walkable w) {
w.walk();
}
public static void letItSwim(Swimmable s) {
s.swim();;
}
}
Turti (a turtle) is biting.
Turti (a turtle) is walking.
Turti (a turtle) is swimming.
Listing 21-25Using the Turtle Class
请注意,Turtle类型的变量可以访问所有三种方法— bite()、walk()和swim()—如下所示:
Turtle t = new Turtle("Turti");
t.bite();
t.walk();
t.swim();
当你使用一个Turtle对象作为Walkable类型时,你只能访问walk()方法。当您使用一个Turtle对象作为Swimmable类型时,您只能访问swim()方法。以下代码片段演示了这一规则:
Turtle t = new Turtle("Trach");
Walkable w = t;
w.walk(); // OK. Using w, you can access only the walk() method of Turtle object
Swimmable s = t;
s.swim(); // OK. Using s you can access only the swim() method
部分实现接口
一个类同意为它实现的接口的所有抽象方法提供一个实现。然而,一个类不必为所有方法提供实现。换句话说,一个类可以提供已实现接口的部分实现。回想一下,一个接口是隐式的abstract(意味着不完整)。如果一个类不提供接口的完整实现,它必须被声明为抽象的(意味着不完整)。否则,编译器将拒绝编译该类。考虑一个名为IABC的接口,它有三个方法— m1()、m2()和m3():
package com.jdojo.interfaces;
public interface IABC {
void m1();
void m2();
void m3();
}
假设一个名为ABCImpl的类实现了IABC接口,但它没有为所有三个方法提供实现:
package com.jdojo.interfaces;
// A compile-time error
public class ABCImpl implements IABC {
// Provides implementation for only one method of the IABC interface
@Override
public void m1() {
// Code for the method goes here
}
}
前面的ABCImpl类代码无法编译。它同意为IABC接口的所有三个方法提供实现。然而,阶级的主体并不遵守诺言。它只提供了一种方法的实现,m1()。因为类ABCImpl没有为IABC接口的另外两个方法提供实现,所以ABCImpl类是不完整的,它必须被声明为抽象的以表明它的不完整。如果试图编译ABCImpl类,编译器会产生以下错误:
Error(3,14): class com.jdojo.interfaces.ABCImpl should be declared abstract; it does not define method m2() of interface com.jdojo.interfaces.IABC
Error(3,14): class com.jdojo.interfaces.ABCImpl should be declared abstract; it does not define method m3() of interface com.jdojo.interfaces.IABC
编译器错误一清二楚。它声明ABCImpl类必须被声明为抽象的,因为它没有实现IABC接口的m2()和m3()方法。以下代码片段通过声明抽象类来修复编译器错误:
package com.jdojo.interfaces;
public abstract class ABCImpl implements IABC {
@Override
public void m1() {
// Code for the method goes here
}
}
将一个类声明为抽象类意味着它不能被实例化。以下代码将生成编译时错误:
new ABCImpl(); // A compile-time error. ABCImpl is abstract
使用ABCImpl类的唯一方法是从它继承另一个类,并为IABC接口的m2()和m3()方法提供缺失的实现。下面是一个新类DEFImpl的声明,它继承自ABCImpl类:
package com.jdojo.interfaces;
public class DEFImpl extends ABCImpl {
// Other code goes here
@Override
public void m2() {
// Code for the method goes here
}
@Override
public void m3() {
// Code for the method goes here
}
}
DEFImpl类提供了ABCImpl类的m2()和m3()方法的实现。注意,DEFImpl类从它的超类ABCImpl继承了m1()、m2()和m3()方法。编译器不再强迫你将DEFImpl类声明为抽象类。如果你愿意,你仍然可以声明DEFImpl类抽象。
您可以创建一个DEFImpl类的对象,因为它不是抽象的。DEFImpl类的对象有哪些类型?它有四种类型:DEFImpl、ABCImpl、Object和IABC。DEFImpl类的对象也是ABCImpl类型,因为DEFImpl继承自ABCImpl。因为ABCImpl实现了IABC接口,所以ABCImpl类的一个对象也属于IABC类型。既然一个DEFImpl是一个ABCImpl,一个ABCImpl是一个IABC,那么从逻辑上来说一个DEFImpl也是一个IABC。下面的代码片段演示了这条规则。一个DEFImpl类的对象被分配给DEFImpl、Object、ABCImpl和IABC类型的变量:
DEFImpl d = new DEFImpl();
Object obj = d;
ABCImpl a = d;
IABC ia = d;
超类型-子类型关系
实现类的接口建立了超类型-子类型的关系。该类成为它实现的所有接口的子类型,所有接口成为该类的超类型。替换规则适用于这种超类型-子类型关系。替换的规则是子类型可以在任何地方替换它的父类型。考虑下面这个类C的类声明,它实现了三个接口J、K,和L:
public class C implements J, K, L {
// Code for class C goes here
}
前面的代码在三个接口J、K和L与类C之间建立了超类型-子类型关系。回想一下,接口声明定义了一个新的类型。假设您已经声明了三个接口:J、K和L。三个接口声明定义了三种类型:类型J、类型K和类型L。类C的声明定义了第四种类型:类型C。J、K、L、C四种类型之间是什么关系?类别C是类型J、K和L的子类型;类型J是类型C的超类型;类型K是类型C的超类型;而类型L是类型C的超类型。这种超类型-子类型关系的含义是,只要需要类型J、K或L的值,就可以安全地用类型C的值来替代。以下代码片段演示了这一规则:
C cObject = new C();
// cObject is of type C. It can always be used where J, K or L type is expected.
J jobject = cObject; // OK
K kobject = cObject; // OK
L lobject = cObject; // OK
接口继承
一个接口可以从另一个接口继承。与类不同,一个接口可以从多个接口继承。考虑清单 21-26 到 21-28 中显示的Singer、Writer和Player接口。
// Player.java
package com.jdojo.interfaces;
public interface Player {
void play();
void setRate(double rate);
default double getRate() {
return 300.0;
}
}
Listing 21-28A Player Interface
// Writer.java
package com.jdojo.interfaces;
public interface Writer {
void write();
void setRate(double rate);
double getRate();
}
Listing 21-27A Writer Interface
// Singer.java
package com.jdojo.interfaces;
public interface Singer {
void sing();
void setRate(double rate);
double getRate();
}
Listing 21-26A Singer Interface
所有这三种类型的专业人士(歌手、作家和演奏者)都各司其职,而且都有报酬。这三个接口包含两种类型的方法。一种方法表示他们所做的工作,例如,sing()、write()和play()。另一种方法表示他们的最低时薪。Singer和Writer接口已经声明了setRate()和getRate()方法是抽象的,让实现类指定它们的实现。Player接口声明了setRate()方法抽象,并为getRate()方法提供了默认实现。
就像一个类从另一个类继承一样,一个接口使用关键字extends从其他接口继承。关键字extends后面是逗号分隔的继承接口名称列表。继承的接口称为超级接口,继承它们的接口称为子接口。接口继承其超接口的下列成员:
-
抽象和默认方法
-
常量字段
-
嵌套类型
提示一个接口不从它的超接口继承静态或私有方法。
一个接口可以重写从它的超接口继承的抽象和默认方法。如果接口包含的常量字段和嵌套类型的名称与从超接口继承的常量字段和嵌套类型的名称相同,则接口中的常量字段和嵌套类型隐藏了它们各自的继承对应项的名称。
假设您想要创建一个接口来表示不收费的慈善歌手。慈善歌手也是歌手。您将创建一个名为CharitySinger的接口,它继承自Singer接口,如下所示:
public interface CharitySinger extends Singer {
}
此时,CharitySinger接口从Singer接口继承了三个抽象方法。任何实现CharitySinger接口的类都需要实现这三个方法。因为慈善歌手唱歌不收费,CharitySinger接口可能会覆盖setRate()和getRate()方法,并使用清单 21-29 中所示的默认方法提供一个默认实现。
// CharitySinger.java
package com.jdojo.interfaces;
public interface CharitySinger extends Singer {
@Override
default void setRate(double rate) {
// A no-op method
}
@Override
default double getRate() {
return 0.0;
}
}
Listing 21-29A CharitySinger Interface
setRate()方法是一个空操作。getRate()方法返回零。实现CharitySinger接口的类需要实现sing()方法并为其提供一个实现。这个类将继承默认的方法setRate()和getRate()。
有可能同一个人既是歌手又是作家。你可以创建一个名为SingerWriter的接口,它继承了两个接口Singer和Writer,如清单 21-30 所示。
// SingerWriter.java
package com.jdojo.interfaces;
public interface SingerWriter extends Singer, Writer {
// No code
}
Listing 21-30A SingerWriter Interface That Inherits from Singer and Writer Interfaces
SingerWriter接口有多少方法?它从Singer接口继承了三个抽象方法,从Writer接口继承了三个抽象方法。它继承了方法setRate()和getRate()两次——一次来自Singer接口,一次来自Writer接口。这些方法在两个超接口中有相同的声明,并且它们是抽象的。这不会引起问题,因为两种方法都是抽象的。实现SingerWriter接口的类只需要为这两种方法提供一次实现。
清单 21-31 显示了实现SingerWriter接口的Melodist类的代码。注意,它只覆盖了setRate()和getRate()方法一次。
// Melodist.java
package com.jdojo.interfaces;
public class Melodist implements SingerWriter {
private String name;
private double rate = 500.00;
public Melodist(String name) {
this.name = name;
}
@Override
public void sing() {
System.out.println(name + " is singing.");
}
@Override
public void setRate(double rate) {
this.rate = rate;
}
@Override
public double getRate() {
return rate;
}
@Override
public void write() {
System.out.println(name + " is writing");
}
}
Listing 21-31A Melodist Class That Implements the SingerWriter Interface
以下代码片段显示了如何使用Melodist类:
SingerWriter purcell = new Melodist("Henry Purcell");
purcell.setRate(700.00);
purcell.write();
purcell.sing();
Henry Purcell is writing
Henry Purcell is singing.
一个人可以唱歌,也可以玩游戏。我们来创建一个SingerPlayer界面来表现这类人。让我们从Singer和Player接口继承接口,如下所示:
public interface SingerPlayer extends Singer, Player {
// No code for now
}
尝试编译SingerPlayer接口会导致以下错误:
SingerPlayer.java:4: error: interface SingerPlayer inherits abstract and default for getRate() from types Player and Singer
该错误是由getRate()方法的两个继承版本中的冲突引起的。Singer接口声明了getRate()方法抽象,Player接口声明它是默认的。这导致了冲突。编译器无法决定继承哪个方法。当同一默认方法的多个版本从不同的超接口继承时,可能会出现这种冲突。考虑下面这个CharitySingerPlayer接口的声明:
public interface CharitySingerPlayer extends CharitySinger, Player {
}
尝试编译CharitySingerPlayer接口会导致以下错误:
CharitySingerPlayer.java:4:错误:接口 CharitySingerPlayer 继承了
getRate() from types CharitySinger and Player
CharitySingerPlayer.java:4: error: interface CharitySingerPlayer inherits abstract and default for setRate(double) from types CharitySinger and Player
这一次,错误是因为两个原因:
-
该接口继承了两个默认的
getRate()方法,一个来自CharitySinger接口,一个来自Player接口。 -
该接口从
CharitySinger接口继承了一个默认的setRate()方法,从Player接口继承了一个抽象的setRate()方法。
这种类型的冲突在 Java 8 之前是不可能的,因为缺省方法不可用。当遇到抽象-默认或默认-默认方法的组合时,编译器不知道要继承哪个方法。为了解决这种冲突,接口需要重写接口中的方法。有几种方法可以解决冲突——都涉及到在接口中重写冲突的方法:
-
您可以用抽象方法覆盖冲突的方法。
-
您可以用默认方法重写冲突的方法,并提供新的实现。
-
您可以用默认方法覆盖冲突的方法,并调用超接口的方法之一。
让我们在SingerPlayer界面中解决冲突。清单 21-32 包含了一个接口声明,它用一个抽象的getRate()方法覆盖了getRate()方法。任何实现SingerPlayer接口的类都必须为getRate()方法提供一个实现。
// SingerPlayer.java
package com.jdojo.interfaces;
public interface SingerPlayer extends Singer, Player {
// Override the getRate() method with an abstract method
@Override
double getRate();
}
Listing 21-32 Overriding
the Conflicting Method with an Abstract Method
清单 21-33 中对SingerPlayer接口的声明通过用默认的getRate()方法覆盖getRate()方法来解决冲突,默认的方法只是返回一个值 700.00。任何实现这个SingerPlayer接口的类都将继承getRate()方法的默认实现。
// SingerPlayer.java
package com.jdojo.interfaces;
public interface SingerPlayer extends Singer, Player {
// Override the getRate() method with a default method
@Override
default double getRate() {
return 700.00;
}
}
Listing 21-33Overriding the Conflicting Method with a Default Method
有时一个接口可能想要访问它的超接口的被覆盖的默认方法。Java 8 引入了一种新的语法,用于从一个接口调用直接超接口的被覆盖的默认方法。新语法使用关键字super,如下所示:
<superinterface-name>.super.<superinterface-default-method(arg1, arg2...)>
Tip
使用关键字super,只能访问直接超级接口的默认方法。语法不支持访问超接口的超接口的默认方法。使用这种语法不能访问超接口的抽象方法。
清单 21-34 包含了对SingerPlayer接口的声明,它通过用默认的getRate()方法覆盖getRate()方法来解决冲突。该方法使用Player.super.getRate()调用来调用Player接口的getRate()方法,将该值乘以 3.5,并将其返回。它只是实现了一个规则,即SingerPlayer的报酬至少是Player的 3.5 倍。任何实现SingerPlayer接口的类都将继承getRate()方法的默认实现。
// SingerPlayer.java
package com.jdojo.interfaces;
public interface SingerPlayer extends Singer, Player{
// Override the getRate() method with a default method that calls the
// Player superinterface getRate() method
@Override
default double getRate() {
double playerRate = Player.super.getRate();
double singerPlayerRate = playerRate * 3.5;
return singerPlayerRate;
}
}
Listing 21-34Overriding the Conflicting Method with a Default Method That Calls the Method in the Superinterface
清单 21-35 包含了CharitySingerPlayer接口的代码。它用抽象方法覆盖了setRate()方法,用默认方法覆盖了getRate()方法。getRate()方法调用Player接口的默认getRate()方法
// CharitySingerPlayer.java
package com.jdojo.interfaces;
public interface CharitySingerPlayer extends CharitySinger, Player {
// Override the setRate() method with an abstract method
@Override
void setRate(double rate);
// Override the getRate() method with a default method that calls the
// Player superinterface getRate() method
@Override
default double getRate() {
return Player.super.getRate();
}
}
Listing 21-35Overriding the Conflicting Methods in the CharitySinger Interface
超级接口-子接口关系
接口继承建立了超接口-子接口(也称为*超类型-子类型)*关系。当接口CharitySinger继承Singer接口时,Singer接口称为CharitySinger接口的超接口,CharitySinger接口称为Singer接口的子接口。一个接口可以有多个超级接口,一个接口可以是多个接口的子接口。子接口的引用可以分配给超接口的变量。考虑下面的代码片段来演示超级接口-子接口关系的使用。代码中的注释解释了为什么赋值会成功或失败:
public interface Shape {
// Code goes here
}
public interface Line extends Shape {
// Code goes here
}
public interface Circle extends Shape {
// Code goes here
}
下面是您可以使用这些接口编写的示例代码,并带有解释代码应该做什么的注释:
Shape shape = get an object reference of a Shape...;
Line line = get an object reference of a Line...;
Circle circle = get an object reference of a Circle...;
/* More code goes here... */
shape = line; // Always fine. A Line is always a Shape.
shape = circle; // Always fine. A Circle is always a Shape.
// A compile-time error. A Shape is not always a Line. A Shape may be a Circle.
// Must use a cast to compile.
line = shape;
// OK with the compiler. The shape variable must refer to a Line at runtime.
// Otherwise, the runtime will throw a ClassCastException.
line =(Line) shape;
继承冲突的实现
在 Java 8 之前,一个类不可能从多个超类型继承多个实现(非抽象方法)。默认方法的引入使得一个类从它的超类和超接口继承冲突的实现成为可能。当一个类从多个路径(超类和超接口)继承了具有相同签名的方法时,Java 使用三个简单的规则来解决冲突:
-
超类总是赢:如果一个类从它的超类继承了一个方法(抽象的或具体的)并且从它的一个超接口继承了一个具有相同签名的方法,那么这个超类就赢了。也就是说,类继承了超类的方法,而超接口中的方法被忽略。如果接口中的默认方法在整个类层次结构中不可用,则此规则将该方法视为备用方法。
-
最具体的超级接口胜出:如果第一个规则不能解决冲突,则使用该规则。如果继承的默认方法来自多个超接口,则来自最特定超接口的方法由该类继承。
-
类必须覆盖冲突的方法:如果前两个规则没有解决冲突,就使用这个规则。在这种情况下,开发人员必须在类中重写冲突的方法。
让我们讨论这三个规则适用的不同场景。
超类总是赢
这条规则应用起来很简单。如果一个类继承或声明了一个方法,那么超接口中具有相同签名的方法将被忽略。
示例#1
考虑下面两个类,Employee和Manager:
public abstract class Employee {
private double rate;
public abstract void setRate(double rate);
public double getRate() {
return rate;
}
}
public abstract class Manager extends Employee implements CharitySinger {
// Code goes here
}
Manager类继承自Employee类。以下五种方法可供Manager类继承:
-
CharitySinger.sing()方法抽象 -
默认的
CharitySinger.setRate()方法 -
默认的
CharitySinger.getRate()方法 -
Employee.setRate()方法抽象 -
具体的
Employee.getRate()方法
对于sing()方法没有冲突。因此,Manager类从CharitySinger接口继承了sing()方法。setRate()和getRate()方法有两种选择。这两个方法在超类中是可用的。因此,Manager类从Employee类继承了setRate()和getRate()方法。
实施例 2
“超类总是赢”规则意味着在Object类中声明的方法不能被接口中的默认方法覆盖。以下对Runner接口的声明将不会编译:
// Won't compile
public interface Runner {
void run();
// Not allowed
@Override
default String toString() {
return "WhoCares";
}
}
在我给出这个规则背后的原因之前,让我们假设Runner接口编译。假设一个Thinker类实现了Runner接口,如下所示:
public class Thinker implements Runner {
@Override
public void run() {
System.out.println();
}
// Which method is inherited - Object.toString() or Runner.toString()?
}
Thinker类有两个继承toString()方法的选择:一个来自超类Object,一个来自超接口Runner。记住超类总是获胜,因此,Thinker类从Object类继承了toString()方法,而不是Runner接口。这个论点适用于Object类中的所有方法和所有类。因为接口中的默认方法不会被任何类使用,所以不允许接口用默认方法覆盖Object类的方法。
实施例 3
接口中的默认方法不能声明为 final,原因有两个:
-
默认方法旨在类中被重写。
-
如果在现有接口中添加默认方法,所有实现类如果包含具有相同签名的方法,都应该继续工作。
考虑一个Sizeable接口和一个实现该接口的Bag类:
public interface Sizeable {
int size();
}
public class Bag implements Sizeable {
private int size;
@Override
public int size() {
return size;
}
public boolean isEmpty() {
return (size == 0);
}
// More code goes here
}
Bag类覆盖了Sizeable接口的size()方法。该类包含一个名为isEmpty()的额外的具体方法。这一点没有问题。现在,Sizeable界面的设计者决定给界面添加一个默认的isEmpty()方法,如下所示:
public interface Sizeable {
int size();
// A new default method. Cannot declare it final
default boolean isEmpty() {
return (size() == 0);
}
}
在新的默认isEmpty()方法被添加到Sizeable接口后,Bag类将继续工作。该类简单地覆盖了Sizeable接口的默认isEmpty()方法。如果允许它声明默认的isEmpty()方法 final,它将导致一个错误,因为 final 方法不允许被覆盖。不允许最终默认方法的规则确保了向后兼容性。如果现有类包含方法,并且在该类实现的接口中添加了具有相同签名的默认方法,则该类将继续工作。
最具体的超级接口胜出
此规则试图解决来自多个接口的具有相同签名的冲突方法的继承。如果相同的方法(抽象或默认)通过不同的路径从多个超接口继承,则使用最具体的路径。假设I1是一个带有方法m()的接口。I2是I1的子接口,I2覆盖了方法m()。如果一个类Test实现了两个接口I1和I2,那么它有两个选择来继承m()方法——即I1.m()和I2.m()。在这种情况下,I2.m()被认为是最具体的,因为它覆盖了I1.m()。这些规则可以总结如下:
-
列出从不同超接口获得的具有相同签名的方法的所有选择。
-
从列表中移除已被列表中其他方法覆盖的所有方法。
-
如果你只有一个选择,那就是这个类将继承的方法。
考虑下面的Employee类。它实现了Singer和SingerPlayer接口:
public class Employee implements Singer, SingerPlayer {
// Code goes here
}
继承从Player接口继承的play()方法没有冲突。继承sing()方法没有冲突,因为两个超接口都指向Singer接口中的同一个sing()方法。哪个setRate()方法被Employee类继承了?您有以下选择:
-
Singer.setRate() -
SingerPlayer.setRate()
这两种选择都会导致抽象的setRate()方法。因此,不存在冲突。然而,SingerPlayer.setRate()方法在这种情况下最为特殊,因为它覆盖了Singer.setRate()方法。
哪个getRate()方法被Employee类继承?您有以下选择:
-
Singer.getRate() -
SingerPlayer.getRate()
Singer.getRate()方法已经被SingerPlayer.getRate()方法覆盖。因此,Singer.getRate()作为一个选项被删除,这样你只剩下一个选项,SingerPlayer.getRate()。因此,Employee类从SingerPlayer接口继承了默认的getRate()方法。
该类必须重写冲突的方法
如果前面的两条规则不能解决冲突的方法继承,该类必须重写该方法,并选择它想在该方法中做什么。它可能以一种全新的方式实现该方法,也可能选择调用超接口中的一个方法。可以使用以下语法调用类的一个超接口的默认方法:
<superinterface-name>.super.<superinterface-default-method(arg1, arg2...)>
如果要调用某个类的超类中的某个方法,可以使用以下语法:
<class-name>.super.<superclass-method(arg1, arg2...)>
考虑下面对一个继承自Singer和Player接口的MultiTalented类的声明:
// Won't compile
public abstract class MultiTalented implements Singer, Player {
}
此类声明不会编译。该类继承了sing()、play()和setRate()方法,没有任何冲突。继承getRate()方法有两种选择:
-
Singer.getRate()方法抽象 -
默认的
Player.getRate()方法
两个版本的getRate()方法都不比另一个更具体。在这种情况下,MultiTalented类必须覆盖getRate()方法来解决冲突。下面的MultiTalented类代码将会编译:
public abstract class MultiTalented implements Singer, Player {
// A MultiTalented is paid the rate of a Player plus 200.00
@Override
public double getRate() {
// Get the default rate for a Player from the Player interface
double playerRate = Player.super.getRate();
double rate = playerRate + 200.00;
return rate;
}
}
该类覆盖了getRate()方法来解决冲突。该方法调用Player接口的默认getRate()方法,并执行其他逻辑。这个类仍然被声明为抽象的,因为它没有实现来自Singer和Player接口的抽象方法。
运算符的实例
您可以使用instanceof操作符来评估引用类型变量是否引用特定类的对象或由其类实现的特定接口。它是一个两个操作数的操作符,计算结果是一个boolean值。instanceof操作符的一般语法如下:
<reference-variable> instanceof <reference-type>
考虑下面的代码片段,它定义了两个接口(Generous和Munificent)和四个类(Giver、GenerousGiver、MunificentGiver和StingyGiver):
public interface Generous {
void give();
}
public interface Munificent extends Generous {
void giveALot();
}
public class Giver {
}
public class GenerousGiver extends Giver implements Generous {
@Override
public void give() {
}
}
public class MunificentGiver extends Giver implements Munificent {
@Override
public void give() {
}
@Override
public void giveALot() {
}
}
public final class StingyGiver extends Giver {
public void giveALittle() {
}
}
图 21-2 显示了这些接口和类的类图。
图 21-2
显示接口和类之间关系的类图:慷慨、慷慨、对象、给予者、慷慨给予者、慷慨给予者和吝啬给予者
Java 中的每个表达式都有两种类型,编译时类型和运行时类型。编译时类型也称为静态类型或声明类型。运行时类型也称为动态类型或实际类型。表达式的编译时类型在编译时是已知的。当表达式被实际执行时,表达式的运行时类型是已知的。考虑以下语句:
Munificent john = new MunificentGiver();
这段代码包含一个变量声明Munificent john和一个表达式new MunificentGiver()。变量john的编译时类型是Munificent。表达式new MunificentGiver()的编译时类型是MunificentGiver。在运行时,变量john将引用MunificentGiver类的一个对象,其运行时类型将是MunificentGiver。表达式new MunificentGiver()的运行时类型将与其编译时类型MunificentGiver相同。
操作符执行编译时检查和运行时检查。在编译时,它检查其左侧操作数是否有可能指向其右侧操作数类型的实例。允许左边的操作数指向null引用。如果左边的操作数有可能引用右边的操作数类型,则代码通过编译器检查。例如,以下代码将在运行时编译并打印true:
Munificent john = new MunificentGiver();
if (john instanceof Munificent) {
System.out.println("true");
} else {
System.out.println("false");
}
查看john的编译时类型,即Munificent,编译器确信john将引用null或其类实现Munificent接口的对象。所以编译器不会抱怨john instanceof Munificent表达式。
考虑下面的代码片段,它编译并打印false:
Giver donna = new Giver();
if (donna instanceof Munificent) {
System.out.println("true");
} else {
System.out.println("false");
}
变量donna的编译时类型是Giver。在运行时,它还指向一个Giver类型的对象。也就是它的运行时类型是Giver。当编译器试图编译donna instanceof Munificent表达式时,它会问一个问题:“编译时类型为Giver的变量donna是否可能指向实现Munificent接口的类的对象?”答案是肯定的。你被答案弄糊涂了吗?编译器在计算instanceof运算符时,不会查看前面代码片段中的整个语句Giver donna = new Giver();。它只是查看变量donna的编译时类型,也就是Giver。Giver类本身不实现Munificent接口。然而,Giver类的任何子类都可能实现Munificent接口,变量donna可能引用任何此类的对象。例如,可以编写如下所示的代码:
Giver donna = new MunificentGiver();
在这种情况下,变量donna的编译时类型仍然是Giver类型。然而,在运行时,它将引用一个对象,该对象的类实现了Munificent接口。编译器的工作只是确定一种“可能性”,在运行时可能是true或false。当变量donna引用Giver类的一个对象时,donna instanceof Munificent表达式会在运行时返回false,因为Giver类没有实现Munificent接口。当变量donna引用MunificentGiver类的对象时,donna instanceof Munificent表达式将在运行时返回true,因为MunificentGiver类实现了Munificent接口。
以下代码片段将编译并打印false:
Giver kim = new StingyGiver();
if (kim instanceof Munificent) {
System.out.println("true");
} else {
System.out.println("false");
}
考虑前面代码的一个变体,如下所示:
StingyGiver jim = new StingyGiver();
if (jim instanceof Munificent) { // A compile-time error
System.out.println("true");
} else {
System.out.println("false");
}
这一次,编译器将拒绝编译代码。让我们应用逻辑,并尝试找出代码的问题所在。编译器将生成一个关于jim instanceof Munificent表达式的错误。也就是说,它肯定知道在运行时,变量jim不可能引用一个其类实现了Munificent接口的对象。编译器怎么能如此确定这种可能性?这很容易。您已经将StingyGiver类声明为 final,这意味着它不能被子类化。这意味着编译时类型为StingyGiver的变量jim只能引用类为StingyGiver的对象。编译器也知道StingyGiver类及其祖先类不实现Munificent接口。有了所有这些推理,编译器确定你的程序中有一个逻辑错误,你需要修复它。
如果instanceof操作符在运行时返回true,这意味着它的左边操作数可以安全地转换为右边操作数所表示的类型。通常,当您需要使用instanceof操作符时,您的逻辑如下:
ABC a = null;
DEF d = null;
if (x instanceof ABC) {
// Safe to cast x to ABC type
a = (ABC) x;
} else if (x instanceof DEF) {
// Safe to cast x to DEF type
d = (DEF) x;
}
这可以通过使用 JDK 16 中引入的以下模式匹配语法实例来减少:
if (x instanceof ABC a) {
// a is ABC type variable here
} else if (x instanceof DEF d) {
// d is DEF type variable here
}
如果instanceof操作符的左操作数是null或引用变量,在运行时指向null,则返回false。下面的代码片段也将打印false:
Giver ken = null;
if (ken instanceof Munificent) {
System.out.println("true");
} else {
System.out.println("false");
}
你可以得出结论,如果v instanceof XYZ返回true,你可以安全地假设以下两件事:
-
v不是null。也就是说,v指向内存中的一个有效对象。 -
演员阵容总是会成功的。也就是说,下面的代码保证在没有
ClassCastException的情况下运行:
XYZ x = (XYZ) v;
标记接口
可以声明一个没有成员的接口。请注意,接口可以通过两种方式拥有成员:声明自己的成员或从其超接口继承成员。当一个接口没有成员(声明的或继承的)时,它被称为标记接口。标记接口也称为标签接口。
标记接口有什么用?为什么任何类都要实现标记接口?顾名思义,标记接口用于标记具有特殊含义的类,这些特殊含义可以在特定的上下文中使用。由标记接口添加到类中的含义取决于上下文。标记接口的开发者必须记录接口的含义,接口的消费者将利用其预期的含义。例如,让我们声明一个名为Funny的标记接口,如下所示。这个Funny接口的意义取决于使用它的开发人员:
public interface Funny {
// No code goes here
}
每个接口都定义一个新的类型,标记接口也是如此。因此,您可以声明一个类型为Funny的变量:
Funny simon = an object of a class that implements the Funny interface;
使用类型为Funny的变量simon可以访问什么?你不能使用simon变量访问任何东西,除了Object类的所有方法。您也可以在不实现您的类的Funny接口的情况下做到这一点。通常,标记接口与instanceof操作符一起使用,以检查引用类型变量是否引用了其类实现标记接口的对象。例如,您可以编写如下代码:
Object obj = any java object;
...
if (obj instanceof Funny) {
// obj is an object whose class implements the Funny interface. Display a message on the
// standard output that we are using a Funny object. Or, do something that is intended
// by the developer of the Funny interface
System.out.println("Using a Funny object");
}
Java API 有许多标记接口。Java 类库中的两个标记接口是java.lang.Cloneable和java.io.Serializable。如果一个类实现了Cloneable接口,这意味着该类的开发者打算允许克隆该类的对象。您需要采取额外的步骤来覆盖您的类中的Object的clone()方法,这样就可以在您的类的对象上调用clone()方法,因为clone()方法已经在Object类中被声明为受保护的。即使你的类覆盖了clone()方法,你的类的对象也不能被克隆,直到你的类实现了Cloneable标记接口。您可以看到,实现 Cloneable 接口将一种含义与类相关联,即它的对象可以被克隆。当调用Object类的clone()方法时,Java 会检查对象的类是否实现了Cloneable接口。如果对象的类没有实现Cloneable接口,它会在运行时抛出一个异常。
Java 5 引入了注释。要定义一个注释,可以使用关键字@interface,但它们实际上根本不是接口。它们类似于标记接口,因为它们标记一些东西。它们可以用来将一个含义与任何元素相关联,例如,一个类、一个方法、一个变量、一个包等等。一个 Java 程序的。注释在更多 Java 17 中有详细介绍。您已经多次看到一个注释,即标记方法的@Override 注释。
功能界面
只有一个抽象方法的接口被称为功能接口。您可以用首字母缩写 SAM(单一抽象方法)来记住这一点。静态和默认方法不被认为是将一个接口指定为功能接口。除了我们已经讨论过的步骤之外,不需要额外的步骤来将接口声明为功能性的。作为版本 8 的一部分,Java 引入了函数接口的概念,它可以通过方法引用和 lambda 表达式来实现。本书在前几章已经展示了这些例子,但是没有命名为功能接口,因为我们还没有涉及接口。
Walkable和Swimmable接口是函数接口的例子,因为它们只包含一个抽象方法。Singer接口是非功能性接口的一个例子,因为它包含不止一个抽象方法。可以用@FunctionalInterface注释来注释一个函数接口,编译器会验证被注释的接口真的只包含一个抽象方法;否则,接口声明将不会编译。下面是一个使用@FunctionalInterface注释的功能接口的例子:
@FunctionalInterface
public interface Runner {
public void run();
}
由于该接口中的抽象方法没有参数和 void 返回类型,因此它可以由任何没有参数和不返回值的 lambda 表达式实现。请注意,这里不能使用“var ”,因为您需要告诉 Java 您正在实现哪个函数接口,例如:
Runner r = () -> System.out.println("Running");
函数接口可以有任何类型的方法。例如,作为 JDK 的一部分提供的谓词接口如下所示:
public interface Predicate {
boolean test(Object o);
}
它可以使用 lambda 表达式实现,如下所示(如果给定的对象不为空,该谓词将返回 true):
Predicate p = (Object o) -> o != null;
函数接口可以出现在普通 Java 类型出现的任何地方,比如字段类型、变量类型或参数类型。它们也可以通过方法引用来实现,只要被引用的方法与函数接口的抽象方法的方法签名相匹配。例如,Objects nonNull 方法可以用来表示与上一个示例相同的含义:
Predicate p = Objects::nonNull;
函数接口在 More Java 17 中有更详细的介绍,但是现在你应该对它们的使用有了一个基本的了解。
比较对象
当您有一组对象时,有时您可能希望根据某些标准对它们进行排序。java.lang.Comparable和java.util.Comparator是用于排序对象的两个常用接口。我将在本节中讨论这两种接口。
使用可比接口
如果一个类的对象需要进行比较以便排序,那么这个类就实现了Comparable接口。例如,在对数组或列表中的人员集合进行排序时,您可能想要比较一个Person类的两个对象。用于比较两个对象的标准取决于上下文。例如,当您需要显示许多人时,您可能希望按照他们的姓氏、个人 id、地址或电话号码来显示他们。
由Comparable接口强加给一个类的对象的排序也被称为该类的自然排序。接口Comparable包含一个抽象的compareTo()方法,它接受一个参数。如果被比较的两个对象被认为相等,则该方法返回零;如果对象小于指定的参数,则返回负整数;如果对象大于指定的参数,则返回正整数。Comparable接口是一个通用接口,声明如下:
public interface Comparable<T> {
public int compareTo(T o);
}
String类和包装类(Integer、Double、Float等)。)实现 Comparable 接口。String类的compareTo()方法按字典顺序对字符串进行排序。数字基元类型的所有包装类从数字上比较这两个对象。
比较相同类型的对象是很典型的。下面的类A的类声明使用A作为其泛型类型实现了Comparable<A>接口,声明类A只支持比较其自身类型的对象:
public class A implement Comparable<A> {
public int compareTo(A a) {
// Code goes here
}
}
清单 21-36 包含实现Comparable<ComparablePerson>接口的ComparablePerson类的代码。在compareTo()方法中,首先,根据姓氏比较两个对象。如果姓氏相同,你就比较他们的名字。您已经使用了String类的compareTo()方法来比较两个可比较的人的姓和名。注意,compareTo()方法不处理null值。
// ComparablePerson.java
package com.jdojo.interfaces;
public class ComparablePerson implements Comparable<ComparablePerson> {
private String firstName;
private String lastName;
public ComparablePerson(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
// Compares two persons based on their last names. If last names are
// the same, use first names
@Override
public int compareTo(ComparablePerson anotherPerson) {
int diff = getLastName().compareTo(anotherPerson.getLastName());
if (diff == 0) {
diff = getFirstName().compareTo(anotherPerson.getFirstName());
}
return diff;
}
@Override
public String toString() {
return getLastName() + ", " + getFirstName();
}
}
Listing 21-36A ComparablePerson Class That Implements the Comparable Interface
清单 21-37 包含了通过对数组中的对象进行排序来测试ComparablePerson类的代码。输出显示了ComparablePerson类的对象按照姓氏和名字排序。
// ComparablePersonTest.java
package com.jdojo.interfaces;
import java.util.Arrays;
public class ComparablePersonTest {
public static void main(String[] args) {
ComparablePerson[] persons = new ComparablePerson[] {
new ComparablePerson("John", "Jacobs"),
new ComparablePerson("Jeff", "Jacobs"),
new ComparablePerson("Wally", "Inman")};
System.out.println("Before sorting...");
print(persons);
// Sort the persons list
Arrays.sort(persons);
System.out.println("\nAfter sorting...");
print(persons);
}
public static void print(ComparablePerson[] persons) {
for(ComparablePerson person: persons){
System.out.println(person);
}
}
}
Before sorting...
Jacobs, John
Jacobs, Jeff
Inman, Wally
After sorting...
Inman, Wally
Jacobs, Jeff
Jacobs, John
Listing 21-37A Test Class to Test the ComparablePerson Class and the Comparable Interface
使用比较器接口
我在上一节中解释过的Comparable接口在一个类的对象上强加了一个指定的顺序。有时,您可能希望为该类的对象指定一个不同的顺序,而不是由Comparable接口在该类中指定的顺序。有时你可能想为一个没有实现Comparable接口的类的对象指定一个特定的顺序。例如,您可能希望根据名字和姓氏来指定对ComparablePerson类的对象的排序,这与由Comparable接口的compareTo()方法指定的排序相反,后者是姓氏和名字。Comparator接口允许你在任何类的对象上指定一个定制的顺序。通常,处理对象集合的 Java API 需要一个Comparator对象来指定定制的顺序。Comparator接口是一个通用接口:
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
// Default and static methods are not shown here
}
在 Java 8 中,Comparator接口已经被彻底检修过了。接口中添加了几个静态和默认方法。我们在本章中讨论一些新方法。
通常,您不需要实现Comparator接口的equals()方法。Java 中的每个类都从Object类继承了equals()方法,这在大多数情况下是没问题的。compare()方法有两个参数,它返回一个整数。如果第一个参数小于、等于或大于第二个参数,则它分别返回负整数、零或正整数。清单 21-38 和 21-39 包含了Comparator接口的两个实现:一个基于名字比较两个ComparablePerson对象,另一个基于姓氏。
// LastNameComparator.java
package com.jdojo.interfaces;
import java.util.Comparator;
public class LastNameComparator implements Comparator<ComparablePerson> {
@Override
public int compare(ComparablePerson p1, ComparablePerson p2) {
String lastName1 = p1.getLastName();
String lastName2 = p2.getFirstName();
int diff = lastName1.compareTo(lastName2);
return diff;
}
}
Listing 21-39A Comparator Comparing ComparablePersons Based on Their Last Names
// FirstNameComparator.java
package com.jdojo.interfaces;
import java.util.Comparator;
public class FirstNameComparator implements Comparator<ComparablePerson> {
@Override
public int compare(ComparablePerson p1, ComparablePerson p2) {
String firstName1 = p1.getFirstName();
String firstName2 = p2.getFirstName();
int diff = firstName1.compareTo(firstName2);
return diff;
}
}
Listing 21-38A Comparator Comparing ComparablePersons Based on Their First Names
使用Comparator很容易。创建它的对象,并将其传递给接受对象集合和比较器来比较它们的方法。例如,要对一组ComparablePerson对象进行排序,将该数组和一个FirstNameComparator传递给Arrays类的静态sort()方法:
ComparablePerson[] persons = create and populate the array...
// Sort the persons array based on first name
Comparator fnComparator = new FirstNameComparator();
Arrays.sort(persons, fnComparator);
您可以使用类似的逻辑根据姓氏对数组进行排序:
// Sort the persons array based on last name
Comparator lnComparator = new LastNameComparator();
Arrays.sort(persons, lnComparator);
在 Java 8 之前,如果想先根据名字再根据姓氏对数组进行排序,就需要创建另一个Comparator接口的实现。由于 Java 8 向接口引入了默认方法,您不需要创建一个新的Comparator接口实现。Comparator类包含一个thenComparing()默认方法,声明如下:
default Comparator<T> thenComparing(Comparator<? super T> other)
该方法将一个Comparator作为参数,并返回一个新的Comparator。如果使用原来的Comparator比较的两个对象相等,则使用新的Comparator进行排序。下面的代码片段结合了名和姓Comparator s 来创建一个新的Comparator:
// Sort using first name, then last name
Comparator firstLastComparator = fnComparator.thenComparing(lnComparator);
Arrays.sort(persons, firstLastComparator);
Tip
您可以将对thenComparing()方法的调用链接起来,以创建一个Comparator,对几个嵌套层次进行排序。
Java 8 中的Comparator接口还有一个有用的附加功能:名为reversed()的默认方法。该方法返回一个新的Comparator,它对原来的Comparator进行反向排序。如果要按降序先按名字再按姓氏对数组进行排序,可以按如下方式进行:
// Sort using first name, then last name in reversed order
Comparator firstLastReverseComparator = firstLastComparator.reversed();
Arrays.sort(persons, firstLastReverseComparator);
比较器不能很好地处理null值。通常,他们会抛出一个NullPointerException。Java 8 向Comparator接口添加了以下两个有用的、空友好的、方便的静态方法:
-
static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) -
static <T> Comparator<T> nullsLast(Comparator<? super T> comparator)
这些方法接受一个Comparator并返回一个空友好的Comparator,将空值放在第一个或最后一个。您可以按如下方式使用这些方法:
// Sort using first name, then last name, placing null values first
Comparator nullFirstComparator = Comparator.nullsFirst(firstLastComparator);
Arrays.sort(persons, nullFirstComparator);
清单 21-40 使用这个类的对象对ComparablePerson类的对象进行排序。如输出所示,这一次您可以根据名字和姓氏对可比较人员的列表进行排序。如果您想以任何其他顺序对ComparablePerson的对象列表进行排序,您需要使用Comparator接口的一个对象,它强加了想要的顺序。
// ComparablePersonTest2.java
package com.jdojo.interfaces;
import java.util.Arrays;
import java.util.Comparator;
public class ComparablePersonTest2 {
public static void main(String[] args) {
ComparablePerson[] persons = new ComparablePerson[]{
new ComparablePerson("John", "Jacobs"),
new ComparablePerson("Jeff", "Jacobs"),
new ComparablePerson("Wally", "Inman")};
System.out.println("Original array...");
print(persons);
// Sort using first name
Comparator<ComparablePerson> fnComparator = new FirstNameComparator();
Arrays.sort(persons, fnComparator);
System.out.println("\nAfter sorting on first name...");
print(persons);
// Sort using last name
Comparator<ComparablePerson> lnComparator = new LastNameComparator();
Arrays.sort(persons, lnComparator);
System.out.println("\nAfter sorting on last name...");
print(persons);
// Sort using first name, then last name
Comparator<ComparablePerson> firstLastComparator
= fnComparator.thenComparing(lnComparator);
Arrays.sort(persons, firstLastComparator);
System.out.println("\nAfter sorting on first, then last name...");
print(persons);
// Sort using first name, then last name in reversed order
Comparator<ComparablePerson> firstLastReverseComparator
= firstLastComparator.reversed();
Arrays.sort(persons, firstLastReverseComparator);
System.out.println("\nAfter sorting on first, then last name in reversed...");
print(persons);
// Sort using first name, then last name using null first
Comparator<ComparablePerson> nullFirstComparator
= Comparator.nullsFirst(firstLastComparator);
ComparablePerson[] personsWithNulls = new ComparablePerson[]{
new ComparablePerson("John", "Jacobs"),
null,
new ComparablePerson("Jeff", "Jacobs"),
new ComparablePerson("Wally", "Inman"),
null};
Arrays.sort(personsWithNulls, nullFirstComparator);
System.out.println("\nAfter sorting on first, then last name "
+ "using null first...");
print(personsWithNulls);
}
public static void print(ComparablePerson[] persons) {
for (ComparablePerson person : persons) {
System.out.println(person);
}
}
}
Original array...
Jacobs, John
Jacobs, Jeff
Inman, Wally
After sorting on first name...
Jacobs, Jeff
Jacobs, John
Inman, Wally
After sorting on last name...
Inman, Wally
Jacobs, John
Jacobs, Jeff
After sorting on first, then last name...
Jacobs, Jeff
Jacobs, John
Inman, Wally
After sorting on first, then last name in reversed...
Inman, Wally
Jacobs, John
Jacobs, Jeff
After sorting on first, then last name using null first...
null
null
Jacobs, Jeff
Jacobs, John
Inman, Wally
Listing 21-40A Test Class That Uses a Comparator Object to Sort ComparablePerson Objects
多态:一个对象,多个视图
多态指的是一个对象呈现多种形式的能力。我使用术语“视图”而不是术语“表单”术语“视图”可以更好地理解接口上下文中的多态。让我们重新表述一下多态的定义:它是一个对象提供其不同视图的能力。接口允许您创建多态对象。考虑清单 21-24 中显示的Turtle类声明。它实现了Swimmable和Walkable接口。您创建了一个Turtle对象,如图所示:
Turtle turti = new Turtle("Turti");
因为Turtle类实现了Walkable和Swimmable接口,所以可以将turti对象视为Walkable或Swimmable:
Walkable turtiWalkable = turti;
Swimmable turtiSwimmable = turti;
因为 Java 中的每个类都继承自Object类,所以您也可以将turti对象视为一个Object:
Object turiObject = turti;
图 21-3 显示了同一Turtle物体的四个不同视图。注意,只有一个对象,它属于Turtle类。当你从不同的方向(上、前、后、左、右等)看房子时。),你得到的是同一个房子的不同看法。然而,只有一所房子。当你从房子的正面看它时,你看不到它的其他视图,例如,后视图或左视图。像房子一样,Java 对象可以展示自己的不同视图,这被称为多态。
图 21-3
多态:一个对象,多个视图。乌龟物体的四种不同视图
是什么定义了 Java 对象的特定视图,以及如何获得该对象的视图?视图是对外部人员可用的东西(用技术术语来说,是对客户或类的用户)。在一个类型(一个类或一个接口)中定义的一组方法(客户端可以访问)定义了该类型对象的视图。例如,Walkable类型定义了一个方法:walk()。如果您获得一个对象的Walkable视图,这意味着您只能访问该对象的walk()方法。类似地,如果您有一个对象的Swimmable视图,那么您只能访问该对象的swim()方法。拥有一个Turtle对象的Turtle视图怎么样?Turtle类定义了三种方法:bite()、walk()和swim()。它也从Object类继承方法。因此,如果您有一个对象的Turtle视图,您可以访问Turtle类中可用的所有方法(直接声明或从其超类和超接口继承)。Java 中的每个类都直接或间接地继承自Object类。因此,Java 中的每个对象至少有两个视图:一个视图由Object类中可用(声明或继承)的方法集定义,另一个视图由Object类中定义的方法集定义。当您使用对象的Object视图时,您只能访问Object类的方法。
通过使用不同类型的引用变量访问一个对象,可以获得该对象的不同视图。例如,要获得一个Turtle对象的Walkable视图,您可以执行以下任一操作:
Turtle t = new Turtle("Turti");
Walkable w2 = t; // w2 gives Walkable view of the Turtle object
Walkable w3 = new Turtle(); // w3 gives Walkable view of the Turtle object
了解了可以支持不同视图的 Java 对象之后,让我们看看instanceof操作符的用法。它用于测试一个对象是否支持特定的视图。考虑以下代码片段:
Object anObject = get any object reference...;
if(anObject instanceof Walkable) {
// anObject has a Walkable view
Walkable w = (Walkable) anObject;
// Now access the Walkable view of the object using w
} else {
// anObject does not have a Walkable view
}
anObject变量指的是一个对象。instanceof操作符用于测试anObject变量引用的对象是否支持Walkable视图。注意,仅仅在一个类中定义一个walk()方法并不能为该类的对象定义一个Walkable视图。该类必须实现Walkable接口并实现walk()方法,以使其对象拥有Walkable视图。对象的视图与其类型同义。回想一下,在一个类上实现一个接口给了该类的对象一个额外的类型(即,一个额外的视图)。一个类的对象可以有多少个视图?没有限制。一个类的对象可以有以下视图:
-
由其类类型定义的视图
-
由其类的所有超类(直接或间接)定义的视图
-
由其类或超类(直接或间接)实现的所有接口定义的视图
动态绑定和接口
当使用接口类型的变量调用方法时,Java 使用动态绑定(也称为运行时绑定或后期绑定)。考虑以下代码片段:
Walkable john = a Walkable object reference...
john.walk();
变量john有两种类型:编译时类型和运行时类型。它的编译时类型就是它声明的类型,也就是Walkable。编译器知道变量的编译时类型。当代码john.walk()被编译时,编译器必须根据编译时可用的所有信息来验证这个调用是否有效。编译器为john.walk()方法调用添加了类似如下的指令:
invokeinterface #5, 1; //InterfaceMethod com/jdojo/interfaces/Walkable.walk:()V
前面的指令声明对接口类型Walkable的变量进行了john.walk()方法调用。变量john在运行时引用的对象是它的运行时类型。编译器不知道变量 john 的运行时类型。变量john可以指Person类、Turtle类、Duck类或任何其他实现Walkable接口的类的对象。当执行john.walk()时,编译器不会声明应该使用walk()方法的哪个实现。运行时决定调用walk()方法的实现,如下所示:
-
It gets the information about the class of the object to which the variable
johnrefers. For example, consider the following snippet of code:Walkable john = new Person("John"); // john refers to a Person object john.walk();这里,变量
john在运行时引用的对象的类类型是Person。 -
它在上一步确定的类中寻找
walk()方法实现。如果在那个类中没有找到walk()方法的实现,运行时会递归地在祖先类中寻找walk()方法的实现。 -
如果在前面的步骤中找到了
walk()方法的实现,那么一找到就执行。也就是说,如果在变量john所引用的对象的类中找到了walk()方法的实现,那么运行时将执行该方法的实现,而不再在它的祖先类中寻找该方法。 -
如果在类层次结构中没有找到
walk()方法的实现,则搜索由该类实现的超接口的继承层次结构。如果使用前面描述的在接口中查找方法的最具体的规则找到了一个walk()方法,如果它是一个默认方法,则调用该方法。如果找到多个默认的walk()方法,就会抛出一个IncompatibleClassChangeError。如果找到一个抽象的walk()方法,抛出一个AbstractMethodError。 -
如果仍然没有找到
walk()方法的实现,就会抛出一个NoSuchMethodError。如果所有的类都是一致编译的,您应该不会得到这个错误。
摘要
接口是由类实现的规范。接口可能包含静态常量、抽象方法、默认方法、静态方法和嵌套类型的成员。接口不能有实例变量。无法实例化接口。
没有成员的接口称为标记接口。只有一个抽象方法的接口称为函数接口,可以通过方法引用或 lambda 表达式实现。
类实现接口。关键字implements在类声明中用于实现接口。实现接口的类继承接口的所有成员,静态方法除外。如果该类从实现的接口继承抽象方法,它需要重写它们并提供一个实现,或者该类应该声明自己是抽象的。实现接口的类是实现接口的子类型,而实现接口是类的超类型。如果一个类从多个具有相同签名的超类型(超类或超接口)继承了相同的方法,在这种情况下,超类的方法优先;如果所有方法都继承自超接口,则使用最具体的方法;如果仍然有多个候选项,该类必须重写方法来解决冲突。
一个接口可以从其他接口继承。关键字extends在接口声明中用于指定所有继承的接口。继承该接口的接口称为超级接口,接口本身称为子接口。子接口继承其超接口的所有成员,除了它们的静态方法。如果一个接口从多个超接口继承了具有相同签名的方法组合 default-default 或 default-abstract,则可能会发生冲突。冲突分两步解决:使用最特定的候选项;如果有多个最具体的候选,接口必须覆盖冲突的方法。
在 Java 8 之前,如果不破坏现有代码,就不可能在接口发布后对其进行更改。在 Java 8 中,您可以向现有接口添加默认和静态方法。在 Java 9 之前,接口中的所有方法都是隐式公共的,不允许拥有私有方法。Java 允许你在一个接口中拥有私有方法。
当使用接口类型的变量调用抽象或默认方法时,使用动态绑定。当调用接口的静态方法时,使用静态绑定。请注意,只能使用一种语法来调用接口的静态方法:
InterfaceName.staticMethodName(arg1, arg2...)
EXERCISES
-
Java 中的接口是什么?什么是标记接口?什么是功能界面?
-
你用什么关键字来实现一个类的接口?
-
一个类可以实现多少个接口?
-
在接口声明中使用什么关键字来继承其他接口?
-
可以在接口中声明实例变量吗?
-
哪个版本的 Java SE 允许在接口中拥有私有方法?
-
接口中哪种方法可以被声明为私有的?接口中可以有抽象的私有方法吗?如果不是,解释你的答案。
-
你在一个类中实现什么接口来实现该类对象的自然排序?你用什么接口为类的对象实现自定义排序?
-
描述以下接口声明无法编译的原因,并建议一个修复方法:
public interface Choices { int YES; int NO = 1; private int CANCEL = 2; } -
下面的接口声明有什么问题?
```java
public interface ScheduledJob {
public void run() {
System.out.println("Running the job...");
}
}
```
11. 考虑下面这个名为Greeting :
```java
interface Greeting {
void sayHello();
}
```
的接口声明,创建一个名为`Greeter`的类,它以这样一种方式实现`Greeting`接口,当执行下面的代码片段时,它在标准输出上打印:
```java
Greeting g = new Greeter();
g.sayHello();
```
12. 下面的接口声明不编译。描述原因并建议解决方法:
```java
public final interface Colorable {
public void color();
}
```
13. 下面的接口声明有效吗?像Sensitive接口这样的接口有什么特别的名字?
```java
public interface Sensitive {
// No code goes here
}
```
14. 下面的接口声明会编译吗?如果没有,给出理由:
```java
@FunctionalInterface
public interface Runner {
public void run();
}
```
15. 以下对Printer接口的声明是有效的函数接口声明吗?描述你的理由,它如何符合或不符合功能接口的定义:
```java
@FunctionalInterface
public interface Printer {
public void print();
public default void sayHello() {
System.out.println("Hello");
}
}
```
16. 考虑下面的声明:
```java
public interface Greeting {
default void greet() {
System.out.println("Hello");
}
}
public class EnglishGreeting implements Greeting {
}
public class HispanicGreeting implements Greeting {
@Override
public void greet() {
System.out.println("Ola");
}
}
```
当执行下面的代码片段时,输出会是什么?
```java
Greeting usGreeting = new EnglishGreeting();
Greeting mxGreeting = new HispanicGreeting();
usGreeting.greet();
mxGreeting.greet();
```
17. Consider the following partial declaration of an Item class:
```java
public class Item implements Comparable<Item> {
private String name;
private double price;
/* Your code goes here */
}
```
通过添加所需的构造器来完成`Item`类,以允许项目名称和价格的初始值。还要为两个实例变量添加 getters 和 setters。添加所需的方法,因此该类实现了`Comparable<Item>`接口。对项目进行排序的自然顺序是根据它们的名称。
18. 创建一个定制的比较器类——一个实现Comparator<Item>接口的类。comparator 类将先按价格,然后按名称对Item类的对象进行排序。
- 考虑下面对
Greeting接口和Greeter类的声明:
```java
public interface Greeting {
default void greet() {
System.out.println("Namaste");
}
}
public class Greeter implements Greeting {
@Override
public void greet() {
/* Calls the greet() method of the Greeting interface here */
System.out.println("Hello");
}
}
```
通过添加一条语句作为`Greeter`类的`greet()`方法中的第一条语句来完成该方法中的代码。该语句应该调用`Greeting`接口的`greet()`方法。当执行下面的代码片段时,它应该打印出`"Namaste"`和`"Hello"`——每个单词占一行:
```java
Greeting g = new Greeter();
g.greet();
```
预期的输出如下:
```java
Namaste
Hello
```