接口
目标
- 了解接口的用途
- 学习定义接口的语法
- 知道如何实现接口
- 学习使用接口和将接口引用赋给变量的语法
接口实战
下面这个简短视频实现了本单元的所有 学习目标。我将展示接口是什么,如何定义并实现接口和引用分配规则的工作原理。
接口:它们有什么好处?
从上一单元已经知道,根据设计,抽象方法指定一个契约(通过方法名、参数和返回类型)但未提供可重用的代码。当在抽象类的一个子类中实现行为的方式与另一个子类中不同时,抽象方法(在抽象类上定义)就会很有用。
当您在应用程序中看到一组可分组到一起的常见行为(想想 java.util.List),但它们存在两个或多个实现时,可以考虑使用接口 定义该行为,这正是 Java 语言提供此特性的原因。但是,这个高级特性很容易被滥用、混淆和变成最讨厌的形式(我亲自验证过),所以使用接口时需要小心谨慎。
以这种方式考虑接口可能有所帮助:它们像仅包含抽象方法的抽象类;它们仅定义契约,而不定义实现。
定义一个接口
定义接口的语法很简单:
public interface InterfaceName {
returnType methodName(argumentList);
}
接口声明看起来像类声明,但使用了 interface 关键字。可以将接口命名为您想要的任何(符合语言规则的)名称,但根据约定,接口名称像类名称一样。
接口中定义的方法没有方法主体。接口的实现者负责提供方法主体(与抽象方法一样)。
接口分层结构的定义也与类一样,但一个类可实现任意多个想要的接口。请记住,一个类仅可继承一个类。如果一个类继承了另一个类并实现了一个或多个接口,这些接口会在继承的类后列出,类似这样:
public class Manager extends Employee implements BonusEligible, StockOptionRecipient {
// And so on
}
接口完全不需要拥有任何主体。例如,下面的定义完全可以接受:
public interface BonusEligible {
}
一般而言,这些接口被称为标记接口,因为它们将一个类标记为实现该接口,但未提供任何特殊的显式行为。
了解这一点后,实际定义接口就变得非常简单:
public interface StockOptionRecipient {
void processStockOptions(int numberOfOptions, BigDecimal price);
}
实现接口
要在类上定义一个接口,则需要实现 该接口,这意味着提供一个方法主体来进一步提供履行接口契约的行为。可以使用 implements 关键字实现接口:
public class ClassName extends SuperclassName implements InterfaceName {
// Class Body
}
假设您在 Manager 类上实现了 StockOptionRecipient 接口,如清单 1 所示:
清单 1. 实现一个接口
public class Manager extends Employee implements StockOptionRecipient {
public Manager() {
}
public void processStockOptions (int numberOfOptions, BigDecimal price) {
log.info("I can't believe I got " + number + " options at $" +
price.toPlainString() + "!");
}
}
实现该接口时,需要提供该接口上的一个或多个方法的行为。必须使用与接口上的签名匹配的签名来实现这些方法,还需要添加 public 访问修饰符。
抽象类可声明它将实现某个特定的接口,但不需要实现该接口上的所有方法。抽象类不需要提供它们声称要实现的所有方法的实现。但是,第一个具体类(即第一个可实例化的类)必须实现分层结构没有实现的所有方法。
备注:实现接口的具体类的所有子类,不需要提供它们自己对该接口的实现(因为接口上的方法已由超类实现)。
在 Eclipse 中生成接口
如果您认为一个类应实现一个接口,Eclipse 可轻松地为您生成正确的方法签名。只需更改类签名来实现该接口即可。Eclipse 在该类下画了一条红色的波浪线,将它标记为错误,因为该类没有提供接口上的方法。单击类名,按 Ctrl + 1,Eclipse 会为您提供 “快速修复” 建议。在这些建议中,选择 Add Unimplemented Methods,Eclipse 会为您生成这些方法,将它们放在源文件的底部。
使用接口
接口定义了一种新的引用 数据类型,您可以使用该类型在您将引用类的任何地方引用接口。此能力可在您声明一个引用变量或从一种类型转换为另一种时使用,如清单 2 所示。
清单 2. 将一个新的 Manager 实例赋给一个 StockOptionEligible 引用
package com.makotojava.intro;
import java.math.BigDecimal;
import org.junit.Test;
public class ManagerTest {
@Test
public void testCalculateAndAwardStockOptions() {
StockOptionEligible soe = new Manager();// perfectly valid
calculateAndAwardStockOptions(soe);
calculateAndAwardStockOptions(new Manager());// works too
}
public static void calculateAndAwardStockOptions(StockOptionEligible soe) {
BigDecimal reallyCheapPrice = BigDecimal.valueOf(0.01);
int numberOfOptions = 10000;
soe.awardStockOptions(numberOfOptions, reallyCheapPrice);
}
}
如您所见,将一个新的 Manager 实例赋给一个 StockOptionEligible 引用是有效的,将一个新的 Manager 实例传递给一个想要 StockOptionEligible 引用的方法也是有效的。
赋值:接口
可以将来自实现某个接口的类的引用赋给一个接口类型的变量,但要遵守一些规则。在 清单 2 中,可以看到将一个 Manager 实例赋给 StockOptionEligible 变量引用是有效的。原因是 Manager 类实现了该接口。但是,以下赋值无效:
Manager m = new Manager();
StockOptionEligible soe = m; //okay
Employee e = soe; // Wrong!
因为 Employee 是 Manager 的超类型,所以此代码可能初看起来没有问题,但其实不然。为什么存在问题?因为 Manager 实现了 StockOptionEligible 接口,而 Employee 未实现。
像这样的赋值应遵守在 第 16 单元:继承 中看到的赋值规则。而且与类一样,仅能将一个接口引用赋给一个具有相同类型或超接口类型的变量。