在本章中,我们将深入探讨带有接口的契约式编程。我们将更好地理解接口如何作为类型工作。我们将:
- 使用接收接口作为参数的方法
- 带有接口和类的 Downcast
- 理解装箱和拆箱
- 将接口类型的实例视为不同的子类
- 利用 Java 9 中接口中的默认方法
在上一章中,我们创建了以下五个接口:DrawableInComic
、DrawableInGame
、Hideable
、Powerable
和Fightable
。然后,我们创建了以下实现不同接口的类,其中许多类也继承自超类:SpiderDog
、WonderCat
、HideableWonderCat
、PowerableWonderCat
和FightableWonderCat
。
在 JShell 中运行以下命令以检查我们创建的所有类型:
/types
下面的屏幕截图显示了在 JShell 中执行上一个命令的结果。JShell 列举了我们在会话中创建的五个接口和五个类。
当我们使用接口时,我们使用它们来指定参数类型,而不是使用类名。多个类可能实现单个接口,因此,不同类的实例可能符合特定接口的参数。
现在,我们将创建前面提到的类的其他实例,并使用接口名而不是类名调用指定其所需参数的方法。当我们使用接口作为方法中参数的类型时,我们将了解在幕后发生了什么。
在下面的代码中,前两行创建了名为teddy
和winston
的SpiderDog
类的两个实例。然后,代码为teddy
调用drawSpeechBalloon
方法的两个版本。对该方法的第二次调用将winston
作为DrawableInComic
参数传递,因为winston
是SpiderDog
的实例,而SpiderDog
是实现DrawableInComic
实例的类。样本的代码文件包含在java_9_oop_chapter_09_01
文件夹中的example09_01.java
文件中。
SpiderDog teddy = new SpiderDog("Teddy");
SpiderDog winston = new SpiderDog("Winston");
teddy.drawSpeechBalloon(
String.format("Hello, my name is %s", teddy.getNickName()));
teddy.drawSpeechBalloon(winston, "How do you do?");
winston.drawThoughtBalloon("Who are you? I think.");
下面的代码创建了名为oliver
的WonderCat
类的实例。构造函数中为nickName
参数指定的值为"Oliver"
。下一行调用drawSpeechBalloon
方法让新实例在漫画中引入Oliver
,然后teddy
调用drawSpeechBalloon
方法并将oliver
作为DrawableInComic
参数传递,因为oliver
是WonderCat
的实例,而WonderCat
是实现DrawableInComic
实例的类。因此,我们也可以在需要DrawableInComic
参数时使用WonderCat
的实例。样本的代码文件包含在java_9_oop_chapter_09_01
文件夹中的example09_01.java
文件中。
WonderCat oliver =
new WonderCat("Oliver", 10, "Mr. Oliver", 0, 15, 25);
oliver.drawSpeechBalloon(
String.format("Hello, my name is %s", oliver.getNickName()));
teddy.drawSpeechBalloon(oliver,
String.format("Hello %s", oliver.getNickName()));
下面的代码创建了名为misterHideable
的HideableWonderCat
类的实例。构造函数中为nickName
参数指定的值为"Mr. Hideable"
。下一行检查以oliver
为参数的isIntersectingWith
方法调用是否返回true
。该方法需要一个DrawableInComic
参数,因此我们可以使用oliver
。该方法将返回true
,因为两个实例的x
和y
字段具有相同的值。if
块内的行调用misterHideable
的setLocation
方法。然后,代码调用show
方法。样本的代码文件包含在java_9_oop_chapter_09_01
文件夹中的example09_01.java
文件中。
HideableWonderCat misterHideable =
new HideableWonderCat("Mr. Hideable", 310,
"Mr. John Hideable", 67000, 15, 25, 3);
if (misterHideable.isIntersectingWith(oliver)) {
misterHideable.setLocation(
oliver.getX() + 30, oliver.getY() + 30);
}
misterHideable.show();
下面的代码创建了名为merlin
的PowerableWonderCat
类的实例。构造函数中为nickName
参数指定的值为"Merlin"
。接下来的几行调用setLocation
和draw
方法。然后,代码调用以misterHideable
作为Hideable
参数的useSpellToHide
方法。该方法需要一个Hideable
参数,因此我们可以使用misterHideable
,它是之前创建的HideableWonderCat
实例,实现Hideable
接口。然后,对misterHideable
的show
方法进行调用,使具有三只眼睛的Hideable
再次出现。样本的代码文件包含在java_9_oop_chapter_09_01
文件夹中的example09_01.java
文件中。
PowerableWonderCat merlin =
new PowerableWonderCat("Merlin", 35,
"Mr. Merlin", 78000, 30, 40, 200);
merlin.setLocation(
merlin.getX() + 5, merlin.getY() + 5);
merlin.draw();
merlin.useSpellToHide(misterHideable);
misterHideable.show();
下面的代码创建了一个名为spartan
的FightableWonderCat
类的实例。构造函数中为nickName
参数指定的值为"Spartan"
。接下来的几行调用setLocation
和draw
方法。然后,代码以misterHideable
作为参数调用unsheathSword
方法。该方法需要一个Hideable
参数,因此我们可以使用misterHideable
,之前创建的HideableWonderCat
实例实现Hideable
接口。样本的代码文件包含在java_9_oop_chapter_09_01
文件夹中的example09_01.java
文件中。
FightableWonderCat spartan =
new FightableWonderCat("Spartan", 28,
"Sir Spartan", 1000000, 60, 60, 100, 50);
spartan.setLocation(
spartan.getX() + 30, spartan.getY() + 10);
spartan.draw();
spartan.unsheathSword(misterHideable);
最后,代码为misterHideable
调用drawThoughtBalloon
和drawSpeechBalloon
方法。我们可以调用这些方法,因为misterHideable
是HideableWonderCat
的一个实例,这个类从它的超类WonderCat
继承了DrawableInComic
接口的实现。
对drawSpeechBalloon
方法的调用将spartan
作为DrawableInComic
参数传递,因为spartan
是FightableWonderCat
的实例,而FightableWonderCat
也是一个从其超类WonderCat
继承DrawableInComic
接口实现的类。因此,我们也可以在需要DrawableInComic
参数时使用FightableWonderCat
的实例,如下面几行所述。样本的代码文件包含在example09_01.java
文件的java_9_oop_chapter_09_01
文件夹中。
misterHideable.drawThoughtBalloon(
"I guess I must be friendly...");
misterHideable.drawSpeechBalloon(
spartan, "Pleased to meet you, Sir!");
当我们在 JShell 中执行前面解释的所有代码片段后,我们将看到以下文本输出:
Teddy -> Hello, my name is Teddy
Teddy -> message: Winston, How do you do?
Winston -> ***Who are you? I think.***
Oliver -> Meow
Teddy -> message: Oliver, Hello Oliver
Moving WonderCat Mr. John Hideable to x:45, y:55
My name is Mr. John Hideable and you can see my 3 eyes.
Moving WonderCat Mr. Merlin to x:35, y:45
Drawing WonderCat Mr. Merlin at x:35, y:45
Mr. Merlin uses his 200 spell power to hide the Hideable with 3 eyes.
My name is Mr. John Hideable and you can see my 3 eyes.
Moving WonderCat Sir Spartan to x:90, y:70
Drawing WonderCat Sir Spartan at x:90, y:70
Sir Spartan unsheaths his sword.
Sword power: 100\. Sword weight: 50.
The sword targets a Hideable with 3 eyes.
Mr. Hideable thinks: 'I guess I must be friendly...'
Spartan ==> Mr. Hideable --> Pleased to meet you, Sir!
DrawableInComic
接口以destination
作为DrawableInComic
类型的参数,定义方法的drawSpeechBalloon
方法要求之一,该参数与接口定义的类型相同。下面是我们示例代码中调用此方法的第一行:
teddy.drawSpeechBalloon(winston, "How do you do?");
我们调用了在SpiderDog
类中实现的方法,因为teddy
是SpiderDog
的实例。我们将一个SpiderDog
实例winston
传递给destination
参数。该方法使用destination
参数作为实现DrawableInComic
接口的实例。因此,每当我们引用destination
变量时,我们只能看到DrawableInComic
类型定义了什么。
我们可以很容易地理解,当 Java 将一个类型从其原始类型向下转换为目标类型(例如类所符合的接口)时,会发生什么。在这种情况下,SpiderDog
被降级为DrawableInComic
。如果我们在 JShell 中输入以下代码并按下Tab键,JShell 将枚举名为winston
的SpiderDog
实例的成员:
winston.
JShell 将显示以下成员:
drawSpeechBalloon( drawThoughtBalloon( equals(
getClass() getNickName() hashCode()
nickName notify() notifyAll()
speak( think( toString()
wait(
当我们要求 JShell 列出成员时,它将包括从java.lang.Object
继承的以下成员:
equals( getClass() hashCode() notify() notifyAll()
toString() wait(
删除之前输入的代码(winston.
。如果我们在 JShell 中输入以下代码并按下Tab键,括号中作为winston
变量前缀的DrawableInComic
接口类型将强制向下转换为DrawableInComic
接口类型。因此,JShell 只会为名为winston
的SpiderDog
实例枚举DrawableInComic
接口中需要的成员:
((DrawableInComic) winston).
JShell 将显示以下成员:
drawSpeechBalloon( drawThoughtBalloon( equals(
getClass() getNickName() hashCode()
notify() notifyAll() toString()
wait(
让我们来看看当我们进入 HORT T0 时,结果的差异,并按下了 OutT8TAB TAL T9 键,以及最新的结果。最后列表中显示的成员不包括SpiderDog
类中定义但DrawableInComic
接口中不需要的两种方法:speak
和think
。因此,当 Java 将winston
降级到DrawableInComic
时,我们只能处理DrawableInComic
接口所需的成员。
如果我们使用任何支持自动完成功能的 IDE,当我们使用自动完成功能而不是按 JShell 中的Tab键时,我们会注意到成员枚举中的相同差异。
现在我们将分析另一种情况,在这种情况下,我们将实例向下转换到它实现的一个接口。DrawableInGame
接口定义isIntersectingWith
方法的方法需求,其中otherDrawableInGame
作为DrawableInGame
类型的参数,与接口定义的类型相同。下面是我们示例代码中调用此方法的第一行:
if (misterHideable.isIntersectingWith(oliver)) {
我们调用了在WonderCat
类中定义的方法,因为misterHideable
是HideableWonderCat
的一个实例,它从WonderCat
类继承了isIntersectingWith
方法的实现。我们将一个WonderCat
实例oliver
传递给otherDrawableInGame
参数。该方法将otherDrawableInGame
参数用作实现DrawableInGame
实例的实例。因此,每当我们引用otherDrawableInGame
变量时,只能看到DrawableInGame
类型定义了什么。在这种情况下,WonderCat
被降级为DrawableInGame
。
如果我们在 JShell 中输入以下代码并按下Tab键,JShell 将枚举名为oliver
的WonderCat
实例的成员:
oliver.
JShell 将为oliver
显示以下成员:
age draw() drawSpeechBalloon(
drawThoughtBalloon( equals( fullName
getAge() getClass() getFullName()
getNickName() getScore() getX()
getY() hashCode() isIntersectingWith(
nickName notify() notifyAll()
score setLocation( toString()
wait( x y
删除之前输入的代码(oliver.
。如果我们在 JShell 中输入以下代码并按下Tab键,括号中作为oliver
变量前缀的DrawableInGame
接口类型将强制向下转换为DrawableInGame
接口类型。因此,JShell 只会枚举名为oliver
的WonderCat
实例的成员,这些成员是DrawableInGame
实例中必需的成员:
((DrawableInComic) oliver).
JShell 将显示以下成员:
draw() equals( getClass()
getFullName() getScore() getX()
getY() hashCode() isIntersectingWith(
notify() notifyAll() setLocation(
toString() wait(
让我们来看看当我们进入 HORT T0 时,结果的差异,并按下了 To.T4TAB TAL T5 键,以及最新的结果。当 Java 将oliver
下推到DrawableInGame
时,我们只能处理DrawableInGame
接口所需的成员。
我们可以使用类似的语法将前面的表达式强制转换为原始类型,即WonderCat
类型。如果我们在 JShell 中输入以下代码并按下Tab键,JShell 将再次枚举名为oliver
的WonderCat
实例的所有成员:
((WonderCat) ((DrawableInGame) oliver)).
JShell 将显示以下成员,即当我们在没有任何类型强制转换的情况下输入oliver.
并按下选项卡键时,JShell 枚举的所有成员:
age draw() drawSpeechBalloon(
drawThoughtBalloon( equals( fullName
getAge() getClass() getFullName()
getNickName() getScore() getX()
getY() hashCode() isIntersectingWith(
nickName notify() notifyAll()
score setLocation( toString()
wait( x y
在第 7 章成员继承与多态性中,我们研究了多态性。下一个示例并不代表最佳实践,因为多态性是使其工作的方法。然而,我们将编写一些不代表最佳实践的代码,只是为了进一步了解类型转换。
下面几行在 JShell 中创建了一个名为doSomethingWithWonderCat
的方法。我们将使用此方法来理解如何将使用接口类型接收的实例视为不同的子类。样本的代码文件包含在java_9_oop_chapter_09_01
文件夹中的example09_02.java
文件中。
// The following code is just for educational purposes
// and it doesn't represent a best practice
// We should always take advantage of polymorphism instead
public void doSomethingWithWonderCat(WonderCat wonderCat) {
if (wonderCat instanceof HideableWonderCat) {
HideableWonderCat hideableCat = (HideableWonderCat) wonderCat;
hideableCat.show();
} else if (wonderCat instanceof FightableWonderCat) {
FightableWonderCat fightableCat = (FightableWonderCat) wonderCat;
fightableCat.unsheathSword();
} else if (wonderCat instanceof PowerableWonderCat) {
PowerableWonderCat powerableCat = (PowerableWonderCat) wonderCat;
System.out.println(
String.format("Spell power: %d",
powerableCat.getSpellPower()));
} else {
System.out.println("This WonderCat isn't cool.");
}
}
doSomethingWithWonderCat
方法在wonderCat
参数中接收WonderCat
实例。该方法评估许多使用instanceof
关键字的表达式,以确定wonderCat
参数中接收的实例是HideableWonderCat
、FightableWonderCat
还是PowerableWonder
的实例。
如果wonderCat
是HideableWonderCat
的实例或HideableWonderCat
的任何潜在子类的实例,则代码声明一个名为hideableCat
的HideableWonderCat
局部变量,以保存对HideableWonderCat
的wonderCat
引用。然后,代码调用hideableCat.show
方法。
如果wonderCat
不是HideableWonderCat
的实例,则代码计算下一个表达式。如果wonderCat
是FightableWonderCat
的实例或FightableWonderCat
的任何潜在子类的实例,则代码声明一个名为fightableCat
的FightableWonderCat
局部变量,以保存对FightableWonderCat
的wonderCat
的引用。然后,代码调用fightableCat.unsheathSword
方法。
如果wonderCat
不是FightableWonderCat
的实例,则代码计算下一个表达式。如果wonderCat
是PowerableWonderCat
的实例或PowerableWonderCat
的任何潜在子类的实例,则代码声明一个名为powerableCat
的PowerableWonderCat
局部变量,以保存对PowerableWonderCat
的wonderCat
的引用。然后,代码使用powerableCat.getSpellPower()
方法返回的结果打印法术强度值。
最后,如果最后一个表达式的计算结果为false
,则表示wonderCat
实例只属于WonderCat
,代码打印一条消息,指示WonderCat
不酷。
如果我们必须执行类似于此方法中显示的代码的操作,我们必须利用多态性,而不是使用instanceof
关键字根据实例所属的类运行代码。请记住,我们使用这个示例来了解有关类型转换的更多信息。
现在我们将对 JShell 中最近编码的doSomethingWithWonderCat
方法进行许多调用。我们将使用在声明此方法之前创建的WonderCat
及其子类的实例调用此方法。对于wonderCat
参数,我们将使用以下值调用doSomethingWithWonderCat
方法:
misterHideable
:HideableWonderCat
类的实例spartan
:FightableWonderCat
类的实例merlin
:PowerableWonderCat
类的实例oliver
:WonderCat
类的实例
以下四行使用前面枚举的参数调用 JShell 中的doSomethingWithWonderCat
方法。样本的代码文件包含在java_9_oop_chapter_09_01
文件夹中的example09_02.java
文件中。
doSomethingWithWonderCat(misterHideable);
doSomethingWithWonderCat(spartan);
doSomethingWithWonderCat(merlin);
doSomethingWithWonderCat(oliver);
下面的屏幕截图显示了在 JShell 中为前几行生成的输出。每个调用都会触发不同的类型转换并调用 typecasted 实例的方法:
SpiderDog
和WonderCat
类都实现了DrawableInComic
接口。从WonderCat
类继承的所有类都继承DrawableInComic
接口的实现。想象一下,我们必须向DrawableInComic
接口添加一个新的方法需求,并且我们将创建实现该接口新版本的新类。我们将添加一个新的drawScreamBalloon
方法,绘制一个尖叫气球,也称为尖叫气泡,并带有一条消息。
我们将在SpiderDog
类中添加新方法的实现。但是,假设我们不能对实现DrawableInComic
接口WonderCat
的其中一个类中的代码进行更改。我们有一个大问题,因为一旦我们更改了DrawableInComic
接口的代码,Java 编译器就会为WonderCat
类生成一个编译错误,我们将无法编译这个类及其子类。
在这个场景中,Java 8 中引入的以及 Java 9 中可用的接口的默认方法非常有用。我们可以为drawScreamBalloon
方法声明一个默认实现,并将其包含在新版本的DrawableInComic
接口中。这样,WonderCat
类及其子类将能够使用接口中提供的方法的默认实现,并且它们将符合接口中指定的要求。
下面的 UML 图显示了名为drawScreamBalloon
的默认方法的DrawableInComic
接口的新版本和覆盖默认方法的SpiderDog
类的新版本。请注意,drawScreamBalloon
方法是唯一一个不使用斜体文本的方法,因为它不是抽象方法。
以下几行显示了声明新版本的DrawableInComic
接口的代码,该接口包括对drawScreamBalloon
方法的方法要求以及默认实现。注意方法返回类型前面的default
关键字,表示我们正在声明一个默认方法。默认实现调用drawSpeechBalloon
方法,实现接口的每个类都将声明该方法。这样,默认情况下,实现此接口的类在收到绘制尖叫气球的请求时将绘制语音气球。
样本的代码文件包含在example09_03.java
文件的java_9_oop_chapter_09_01
文件夹中。
public interface DrawableInComic {
String getNickName();
void drawSpeechBalloon(String message);
void drawSpeechBalloon(DrawableInComic destination, String message);
void drawThoughtBalloon(String message);
default void drawScreamBalloon(String message) {
drawSpeechBalloon(message);
}
}
在我们创建新版本的接口之后,JShell 将把所有包含实现DrawableInComic
接口的类实例引用的变量重置为null
。因此,我们将无法使用正在创建的实例来测试接口中的更改。
以下几行显示了使用新的drawScreamBalloon
方法的SpiderDog
类的新版本代码。新行将高亮显示。样本的代码文件包含在example09_03.java
文件的java_9_oop_chapter_09_01
文件夹中。
public class SpiderDog implements DrawableInComic {
protected final String nickName;
public SpiderDog(String nickName) {
this.nickName = nickName;
}
protected void speak(String message) {
System.out.println(
String.format("%s -> %s",
nickName,
message));
}
protected void think(String message) {
System.out.println(
String.format("%s -> ***%s***",
nickName,
message));
}
protected void scream(String message) {
System.out.println(
String.format("%s screams +++ %s +++",
nickName,
message));
}
@Override
public String getNickName() {
return nickName;
}
@Override
public void drawSpeechBalloon(String message) {
speak(message);
}
@Override
public void drawSpeechBalloon(DrawableInComic destination,
String message) {
speak(String.format("message: %s, %s",
destination.getNickName(),
message));
}
@Override
public void drawThoughtBalloon(String message) {
think(message);
}
@Override
public void drawScreamBalloon(String message) {
scream(message);
}
}
SpiderDog
类使用调用受保护的scream
方法的新版本覆盖drawScreamBalloon
方法的默认实现,该方法使用特定格式打印收到的message
,该格式包括nickName
值作为前缀。这样,这个类就不会使用DrawableInComic
接口中声明的默认实现,而是使用自己的实现。
在下面的代码中,第一行创建名为rocky
的SpiderDog
类的新版本实例,以及名为maggie
的FightableWonderCat
类的新版本实例。然后,代码调用drawScreamBalloon
方法,并为创建的两个实例发送消息:rocky
和maggie
。样本的代码文件包含在java_9_oop_chapter_09_01
文件夹中的example09_03.java
文件中。
SpiderDog rocky = new SpiderDog("Rocky");
FightableWonderCat maggie =
new FightableWonderCat("Maggie", 2,
"Mrs. Maggie", 5000000, 10, 10, 80, 30);
rocky.drawScreamBalloon("I am Rocky!");
maggie.drawScreamBalloon("I am Mrs. Maggie!");
当我们调用rocky.drawScreamBalloon
时,Java 执行SpiderDog
类中声明的该方法的重写实现。当我们调用maggie.drawScreamBalloon
时,Java 执行DrawableInComic
接口中声明的默认方法,因为WonderCat
和FightableWonderCat
类都没有覆盖该方法的默认实现。别忘了FightableWonderCat
是WonderCat
的一个子类。以下屏幕截图显示了在 JShell 中执行前几行的结果:
- 默认方法允许我们声明:
- 当实现接口的类没有声明构造函数时,Java 将使用的接口的默认构造函数。
- 在为实现接口的类的实例执行任何方法之前将调用的方法。
- 当实现接口的类没有提供自己的方法实现时,Java 将使用接口中方法的默认实现。
- 考虑到我们有一个现有的接口,很多类都实现,所有的类都没有错误地编译。如果我们将默认方法添加到此接口:
- 实现接口的类在为新方法需求提供实现之前不会编译。
- 实现接口的类在为新的构造函数需求提供实现之前不会编译。
- 实现接口的类将编译。
- 以下哪个关键字允许我们确定实例是否是实现特定接口的类的实例:
instanceof
isinterfaceimplementedby
implementsinterface
- 以下哪一个代码段强制将
winston
变量向下转换到DrawableInComic
接口:(winston as DrawableInComic)
((DrawableInComic) < winston)
((DrawableInComic) winston)
- 以下哪一个代码段强制将
misterHideable
变量向下转换为HideableWonderCat
类:(misterHideable as HideableWonderCat)
((HideableWonderCat) < misterHideable)
((Hid``eableWonderCat) misterHideable)
在本章中,您了解了当方法接收到接口类型的参数时会发生什么。我们使用了接收接口类型参数的方法,并对接口和类进行了降级。我们了解如何将对象视为不同兼容类型的实例,以及这样做时会发生什么。JShell 让我们很容易理解当我们使用类型转换时会发生什么。
我们利用了接口中的默认方法。我们可以向接口添加新方法,并提供默认实现,以避免破坏无法编辑的现有代码。
现在您已经了解了使用接口的高级场景,我们已经准备好使用 Java 9 中的泛型最大化代码重用,这是我们将在下一章中讨论的主题。