在本章中,我们将学习 Java9 中面向对象编程最令人兴奋的特性之一:多态性。我们将编写许多类的代码,然后在 JShell 中使用它们的实例,以了解对象如何具有多种不同的形式。我们将:
- 创建从抽象超类继承的具体类
- 使用子类的实例
- 理解多态性
- 控制子类是否可以覆盖成员
- 控制类是否可以子类化
- 使用对不同子类的实例执行操作的方法
在上一章的中,我们创建了一个名为VirtualAnimal
的抽象基类,然后编码了以下三个抽象子类:VirtualMammal
、VirtualDomesticMammal
和VirtualHorse
。现在,我们将编写以下三个具体类。每个类代表不同的马品种,是VirtualHorse
抽象类的一个子类。
AmericanQuarterHorse
:该类代表一匹属于美国四分之一马品种的虚拟马。ShireHorse
:该类表示属于夏尔马品种的虚拟马。Thoroughbred
:该类代表纯种品种的虚拟马。
三个具体类将实现从抽象超类继承的以下三个抽象方法:
String getAsciiArt()
:此抽象方法继承自VirtualAnimal
抽象类。String getBaby()
:此抽象方法继承自VirtualAnimal
抽象类。String getBreed()
:此抽象方法继承自VirtualHorse
抽象类。
下面的 UML 图显示了我们将编码的三个具体类的成员:AmericanQuarterHorse
、ShireHorse
和Thoroughbred
。我们不为每个具体类将声明的三个方法使用粗体文本格式,因为它们没有覆盖这些方法;他们正在实现类继承的抽象方法。
首先,我们将创建AmericanQuarterHorse
具体类。以下几行显示了 Java 9 中该类的代码。请注意,class
之前没有abstract
关键字,因此,我们的类必须确保它实现了所有继承的抽象方法。样本的代码文件包含在example07_01.java
文件的java_9_oop_chapter_07_01
文件夹中。
public class AmericanQuarterHorse extends VirtualHorse {
public AmericanQuarterHorse(
int age,
boolean isPregnant,
String name,
String favoriteToy) {
super(age, isPregnant, name, favoriteToy);
System.out.println("AmericanQuarterHorse created.");
}
public AmericanQuarterHorse(
int age, String name, String favoriteToy) {
this(age, false, name, favoriteToy);
}
public String getBaby() {
return "AQH baby ";
}
public String getBreed() {
return "American Quarter Horse";
}
public String getAsciiArt() {
return
" >>\\.\n" +
" /* )`.\n" +
" // _)`^)`. _.---. _\n" +
" (_,' \\ `^-)'' `.\\\n" +
" | | \\\n" +
" \\ / |\n" +
" / \\ /.___.'\\ (\\ (_\n" +
" < ,'|| \\ |`. \\`-'\n" +
" \\\\ () )| )/\n" +
" |_>|> /_] //\n" +
" /_] /_]\n";
}
}
现在我们将创建ShireHorse
具体类。以下几行显示了 Java 9 中该类的代码。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹中的example07_01.java
文件中。
public class ShireHorse extends VirtualHorse {
public ShireHorse(
int age,
boolean isPregnant,
String name,
String favoriteToy) {
super(age, isPregnant, name, favoriteToy);
System.out.println("ShireHorse created.");
}
public ShireHorse(
int age, String name, String favoriteToy) {
this(age, false, name, favoriteToy);
}
public String getBaby() {
return "ShireHorse baby ";
}
public String getBreed() {
return "Shire Horse";
}
public String getAsciiArt() {
return
" ;;\n" +
" .;;'*\\\n" +
" __ .;;' ' \\\n" +
" /' '\\.~~.~' \\ /'\\.)\n" +
" ,;( ) / |\n" +
" ,;' \\ /-.,,( )\n" +
" ) /| ) /|\n" +
" ||(_\\ ||(_\\\n" +
" (_\\ (_\\\n";
}
}
最后,我们将创建Thoroughbred
具体类。以下几行显示了 Java 9 中这个类的代码。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹中的example07_01.java
文件中。
public class Thoroughbred extends VirtualHorse {
public Thoroughbred(
int age,
boolean isPregnant,
String name,
String favoriteToy) {
super(age, isPregnant, name, favoriteToy);
System.out.println("Thoroughbred created.");
}
public Thoroughbred(
int age, String name, String favoriteToy) {
this(age, false, name, favoriteToy);
}
public String getBaby() {
return "Thoroughbred baby ";
}
public String getBreed() {
return "Thoroughbred";
}
public String getAsciiArt() {
return
" })\\-=--.\n" +
" // *._.-'\n" +
" _.-=-...-' /\n" +
" {{| , |\n" +
" {{\\ | \\ /_\n" +
" }} \\ ,'---'\\___\\\n" +
" / )/\\\\ \\\\ >\\\n" +
" // >\\ >\\`-\n" +
" `- `- `-\n";
}
}
正如我们编写的其他子类一样,我们为三个具体类定义了多个构造函数。第一个需要四个参数的构造函数使用super
关键字从基类或超类调用构造函数,即在VirtualHorse
类中定义的构造函数。在超类中定义的构造函数完成其执行后,代码将打印一条消息,指示已创建每个特定具体类的实例。每个类中定义的构造函数打印不同的消息。
第二个构造函数使用this
关键字调用前面解释过的构造函数,并使用收到的参数和false
作为isPregnant
参数的值。
在getBaby
和getBreed
方法的实现中,每个类返回不同的String
。此外,在getAsciiArt
方法的实现中,每个类为虚拟马返回不同的 ASCII 艺术表示。
我们可以使用相同的方法,即具有相同名称和参数的方法,根据调用该方法的类导致不同的事情发生。在面向对象编程中,此功能称为多态性。多态性是一个对象具有多种形式的能力,我们将通过使用先前编码的具体类的实例来了解它的实际作用。
以下几行创建名为american
的AmericanQuarterHorse
类的新实例,并使用其不需要isPregnant
参数的构造函数之一。样本的代码文件包含在example07_01.java
文件的java_9_oop_chapter_07_01
文件夹中。
AmericanQuarterHorse american =
new AmericanQuarterHorse(
8, "American", "Equi-Spirit Ball");
american.printBreed();
以下几行显示了在输入前面的代码后,不同构造函数在 JShell 中显示的消息:
VirtualAnimal created.
VirtualMammal created.
VirtualDomesticMammal created.
VirtualHorse created.
AmericanQuarterHorse created.
AmericanQuarterHorse
中定义的构造函数从其超类(即VirtualHorse
类)调用构造函数。请记住,每个构造函数调用其超类构造函数并打印一条消息,指示已创建类的实例。我们没有五个不同的例子;我们只有一个实例,它调用五个不同类的链式构造函数来执行所有必要的初始化,以创建一个AmericanQuarterHorse
实例。
如果我们在 JShell 中执行下面的行,结果都会显示true
,因为american
属于VirtualAnimal
、VirtualMammal
、VirtualDomesticMammal
、VirtualHorse
和AmericanQuarterHorse
类。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹中的example07_01.java
文件中。
System.out.println(american instanceof VirtualAnimal);
System.out.println(american instanceof VirtualMammal);
System.out.println(american instanceof VirtualDomesticMammal);
System.out.println(american instanceof VirtualHorse);
System.out.println(american instanceof AmericanQuarterHorse);
前面几行的结果意味着AmericanQuarterHorse
类的实例(其引用保存在AmericanQuarterHorse
类型的american
变量中)可以采用以下任何类的实例的形式:
VirtualAnimal
VirtualMammal
VirtualDomesticMammal
VirtualHorse
AmericanQuarterHorse
以下截图显示了在 JShell 中执行前几行的结果:
我们在VirtualHorse
类中对方法进行了编码,并且没有在任何子类中重写该方法。以下是printBreed
方法的代码:
public void printBreed() {
System.out.println(getBreed());
}
代码打印由getBreed
方法返回的String
,该方法与抽象方法在同一个类中声明。继承自VirtualHorse
的三个具体类实现了getBreed
方法,每个类返回不同的String
。当我们调用american.printBreed
方法时,JShell 显示American Quarter Horse
。
以下几行创建了名为zelda
的ShireHorse
类的实例。注意,在本例中,我们使用需要isPregnant
参数的构造函数。正如我们创建AmericanQuarterHorse
类的实例时所发生的那样,JShell 将为每个构造函数显示一条消息,这些构造函数是我们编码的链式构造函数的结果。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹中的example07_01.java
文件中。
ShireHorse zelda =
new ShireHorse(9, true,
"Zelda", "Tennis Ball");
接下来的几行分别调用american
的printAverageNumberOfBabies
和printAsciiArt
实例方法AmericanQuarterHorse
的实例和zelda
的实例方法ShireHorse
的实例。样本的代码文件包含在example07_01.java
文件的java_9_oop_chapter_07_01
文件夹中。
american.printAverageNumberOfBabies();
american.printAsciiArt();
zelda.printAverageNumberOfBabies();
zelda.printAsciiArt();
我们将printAverageNumberOfBabies
和printAsciiArt
方法编码在VirtualAnimal
类中,并且我们没有在其任何子类中重写它们。因此,当我们为american
或Zelda
调用这些方法时,Java 将执行VirtualAnimal
类中定义的代码。
printAverageNumberOfBabies
方法使用getAverageNumberOfBabies
返回的int
值和getBaby
方法返回的String
值生成一个String
,表示虚拟动物的平均婴儿数量。VirtualHorse
类使用返回1
的代码实现继承的getAverageNumberOfBabies
抽象方法。AmericanQuarterHorse
和ShireHorse
类实现了继承的getBaby
抽象方法,其代码返回一个String
,表示虚拟马品种的婴儿:"AQH baby"
和"ShireHorse baby"
。因此,我们对printAverageNumberOfBabies
方法的调用将在每个实例中产生不同的结果,因为它们属于不同的类。
printAsciiArt
方法使用getAsciiArt
方法返回的String
打印代表虚拟马的 ASCII 艺术。AmericanQuarterHorse
和ShireHorse
类实现了继承的getAsciiArt
抽象方法,其代码返回一个带有 ASCII 艺术的String
,该艺术适合于类所代表的每个虚拟马。因此,我们对printAsciiArt
方法的调用将在每个实例中产生不同的结果,因为它们属于不同的类。
下面的屏幕截图显示了在 JShell 中执行前几行的结果。这两个实例为VirtualAnimal
抽象类中编码的两个方法运行相同的代码。但是,每个类为方法提供了不同的实现,这些方法最终被调用以生成结果并导致输出中的差异。
下面的行创建名为willow
的Thoroughbred
类实例,然后调用其printAsciiArt
方法。正如前面所发生的那样,JShell 将为每个构造函数显示一条消息,这些构造函数是我们编码的链式构造函数的结果。样本的代码文件包含在example07_01.java
文件的java_9_oop_chapter_07_01
文件夹中。
Thoroughbred willow =
new Thoroughbred(5,
"Willow", "Jolly Ball");
willow.printAsciiArt();
下面的屏幕截图显示了在 JShell 中执行前几行的结果。新实例来自一个类,该类提供了getAsciiArt
方法的不同实现,因此,我们将看到不同的 ASCII 艺术,这与我们在前两次对其他实例的相同方法调用中看到的不同。
下面的行使用不同数量的参数调用名为willow
的实例的neigh
方法。这样,我们就利用了neigh
方法,我们用不同的参数重载了四次。请记住,我们在VirtualHorse
类中编码了四个neigh
方法,Thoroughbred
类通过其层次结构树从该超类继承重载方法。样本的代码文件包含在example07_01.java
文件的java_9_oop_chapter_07_01
文件夹中。
willow.neigh();
willow.neigh(2);
willow.neigh(2, american);
willow.neigh(3, zelda, true);
american.nicker();
american.nicker(2);
american.nicker(2, willow);
american.nicker(3, willow, true);
下面的屏幕截图显示了在 JShell 中使用不同参数调用neigh
和nicker
方法的结果:
我们为名为willow
的Thoroughbred
实例调用了VirtualHorse
类中定义的neigh
方法的四个版本。调用neigh
方法的第三行和第四行为VirtualDomesticMammal
类型的otherDomesticMammal
参数指定一个值。第三行指定american
为otherDomesticMammal
的值,第四行指定zelda
为同一参数的值。AmericanQuarterHorse
和ShireHorse
混凝土类都是VirtualHorse
的子类,VirtualHorse
是VirtualDomesticMammal
的子类。因此,我们可以使用american
和zelda
作为需要VirtualDomesticMammal
实例的参数。
然后,我们为名为american
的AmericanQuarterHorse
实例调用了VirtualHorse
类中定义的nicker
方法的四个版本。调用nicker
方法的第三行和第四行指定willow
作为类型为VirtualDomesticMammal
的otherDomesticMammal
参数的值。Thoroughbred
混凝土类也是VirtualHorse
的一个子类,VirtualHorse
是VirtualDomesticMammal
的一个子类。因此,我们可以使用willow
作为需要VirtualDomesticMammal
实例的参数。
我们将对VirtualDomesticCat
抽象类及其具体子类MaineCoon
进行编码。然后,我们将对VirtualBird
抽象类、其VirtualDomesticBird
抽象子类和Cockatiel
具体子类进行编码。最后,我们将对VirtualDomesticRabbit
具体类进行编码。在编码这些类时,我们将使用 Java9 特性来决定子类是否可以覆盖特定的成员。
所有的虚拟家猫都必须能够说话,因此,我们将覆盖继承自VirtualDomesticMammal
的talk
方法来打印表示猫喵喵叫的单词:"Meow"
。我们还想提供一种打印"Meow"
特定次数的方法。因此,在这一点上,我们意识到我们可以利用我们在VirtualHorse
类中声明的printSoundInWords
方法。
我们无法在VirtualDomesticCat
抽象类中访问此实例方法,因为它不是从VirtualHorse
继承的。因此,我们将把这个方法从VirtualHorse
类移到它的超类:VirtualDomesticMammal
。
对于不希望在子类中重写的方法,我们将在返回类型之前使用final
关键字。当一个方法被标记为 final 方法时,子类无法重写该方法,如果它们试图重写,Java9 编译器将显示错误。
并不是所有的鸟都能在现实生活中飞翔。然而,我们所有的虚拟鸟都能飞,因此,我们将实现继承的isAbleToFly
抽象方法作为返回true
的最终方法。通过这种方式,我们确保继承自VirtualBird
抽象类的所有类都将始终为isAbleToFly
方法运行此代码,并且它们将无法重写它。
下面的 UML 图显示了我们将要编码的新抽象和具体类的成员。另外,图中显示了printSoundInWords
方法从VirtualHorse
抽象类移动到VirtualDomesticMammal
抽象类。
首先,我们将创建一个新版本的VirtualDomesticMammal
抽象类。我们将在VirtualHorse
抽象类中添加printSoundInWords
方法,并使用final
关键字表示我们不想让子类重写此方法。以下几行显示了VirtualDomesticMammal
类的新代码。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
public abstract class VirtualDomesticMammal extends VirtualMammal {
public final String name;
public String favoriteToy;
public VirtualDomesticMammal(
int age,
boolean isPregnant,
String name,
String favoriteToy) {
super(age, isPregnant);
this.name = name;
this.favoriteToy = favoriteToy;
System.out.println("VirtualDomesticMammal created.");
}
public VirtualDomesticMammal(
int age, String name, String favoriteToy) {
this(age, false, name, favoriteToy);
}
protected final void printSoundInWords(
String soundInWords,
int times,
VirtualDomesticMammal otherDomesticMammal,
boolean isAngry) {
String message = String.format("%s%s: %s%s",
name,
otherDomesticMammal == null ?
"" : String.format(" to %s ", otherDomesticMammal.name),
isAngry ?
"Angry " : "",
new String(new char[times]).replace("\0", soundInWords));
System.out.println(message);
}
public void talk() {
System.out.println(
String.format("%s: says something", name));
}
}
在我们输入前面的行后,JShell 将显示以下消息:
| update replaced class VirtualHorse which cannot be referenced until this error is corrected:
| printSoundInWords(java.lang.String,int,VirtualDomesticMammal,boolean) in VirtualHorse cannot override printSoundInWords(java.lang.String,int,VirtualDomesticMammal,boolean) in VirtualDomesticMammal
| overridden method is final
| protected void printSoundInWords(String soundInWords, int times,
| ^---------------------------------------------------------------...
| update replaced class AmericanQuarterHorse which cannot be referenced until class VirtualHorse is declared
| update replaced class ShireHorse which cannot be referenced until class VirtualHorse is declared
| update replaced class Thoroughbred which cannot be referenced until class VirtualHorse is declared
| update replaced variable american which cannot be referenced until class AmericanQuarterHorse is declared
| update replaced variable zelda which cannot be referenced until class ShireHorse is declared
| update replaced variable willow which cannot be referenced until class Thoroughbred is declared
| update overwrote class VirtualDomesticMammal
JShell 指出,在我们纠正该类的错误之前,不能引用VirtualHorse
类及其子类。该类声明了printSoundInWords
方法,并用VirtualDomesticMammal
类中相同的名称和参数重写了最近添加的方法。我们在新声明中使用了final
关键字,以确保任何子类都不能覆盖它,因此,Java 编译器生成 JShell 显示的错误消息。
现在,我们将创建一个新版本的VirtualHorse
抽象类。以下几行显示了删除printSoundInWords
方法并使用final
关键字确保许多方法不能被任何子类重写的新版本。使用final
关键字来避免要重写的方法的声明在下一行中突出显示。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹中的example07_02.java
文件中。
public abstract class VirtualHorse extends VirtualDomesticMammal {
public VirtualHorse(
int age,
boolean isPregnant,
String name,
String favoriteToy) {
super(age, isPregnant, name, favoriteToy);
System.out.println("VirtualHorse created.");
}
public VirtualHorse(
int age, String name, String favoriteToy) {
this(age, false, name, favoriteToy);
}
public final boolean isAbleToFly() {
return false;
}
public final boolean isRideable() {
return true;
}
public final boolean isHerbivore() {
return true;
}
public final boolean isCarnivore() {
return false;
}
public int getAverageNumberOfBabies() {
return 1;
}
public abstract String getBreed();
public final void printBreed() {
System.out.println(getBreed());
}
public final void printNeigh(
int times,
VirtualDomesticMammal otherDomesticMammal,
boolean isAngry) {
printSoundInWords("Neigh ", times, otherDomesticMammal, isAngry);
}
public final void neigh() {
printNeigh(1, null, false);
}
public final void neigh(int times) {
printNeigh(times, null, false);
}
public final void neigh(int times,
VirtualDomesticMammal otherDomesticMammal) {
printNeigh(times, otherDomesticMammal, false);
}
public final void neigh(int times,
VirtualDomesticMammal otherDomesticMammal,
boolean isAngry) {
printNeigh(times, otherDomesticMammal, isAngry);
}
public final void printNicker(int times,
VirtualDomesticMammal otherDomesticMammal,
boolean isAngry) {
printSoundInWords("Nicker ", times, otherDomesticMammal, isAngry);
}
public final void nicker() {
printNicker(1, null, false);
}
public final void nicker(int times) {
printNicker(times, null, false);
}
public final void nicker(int times,
VirtualDomesticMammal otherDomesticMammal) {
printNicker(times, otherDomesticMammal, false);
}
public final void nicker(int times,
VirtualDomesticMammal otherDomesticMammal,
boolean isAngry) {
printNicker(times, otherDomesticMammal, isAngry);
}
@Override
public final void talk() {
nicker();
}
}
进入前几行后,JShell 将显示以下消息:
| update replaced class AmericanQuarterHorse
| update replaced class ShireHorse
| update replaced class Thoroughbred
| update replaced variable american, reset to null
| update replaced variable zelda, reset to null
| update replaced variable willow, reset to null
| update overwrote class VirtualHorse
我们替换了VirtualHorse
类的定义,子类也进行了更新。重要的是要知道,我们在 JShell 中声明的包含对VirtualHorse
子类实例引用的变量被设置为 null。
final
关键字还有一个附加用法。我们可以在类声明中使用final
作为class
关键字之前的修饰符,以指示 Java 我们想要生成一个最终类,即一个不能扩展或子类化的类。Java9 不允许我们为最终类创建子类。
现在,我们将创建VirtualDomesticCat
抽象类,然后我们将声明一个名为MaineCoon
的具体子类作为最终类。这样,我们将确保没有人能够创建MaineCoon
的子类。下面几行显示了VirtualDomesticCat
抽象类的代码。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹的example07_02.java
文件中。
public abstract class VirtualDomesticCat extends VirtualDomesticMammal {
public VirtualDomesticCat(
int age,
boolean isPregnant,
String name,
String favoriteToy) {
super(age, isPregnant, name, favoriteToy);
System.out.println("VirtualDomesticCat created.");
}
public VirtualDomesticCat(
int age, String name, String favoriteToy) {
this(age, false, name, favoriteToy);
}
public final boolean isAbleToFly() {
return false;
}
public final boolean isRideable() {
return false;
}
public final boolean isHerbivore() {
return false;
}
public final boolean isCarnivore() {
return true;
}
public int getAverageNumberOfBabies() {
return 5;
}
public final void printMeow(int times) {
printSoundInWords("Meow ", times, null, false);
}
@Override
public final void talk() {
printMeow(1);
}
}
VirtualDomesticCat
抽象类将继承自VirtualDomesticMammal
超类的许多抽象方法实现为 final 方法,并用 final 方法重写talk
方法。因此,我们将无法创建覆盖isAbleToFly
方法以返回true
的VirtualDomesticCat
子类。我们不会有能飞的虚拟猫。
以下几行显示了继承自VirtualDomesticCat
的MaineCoon
具体类的代码。我们将MaineCoon
声明为最终类,它重写继承的getAverageNumberOfBabies
方法以返回6
。此外,最后一个类实现了以下继承的抽象方法:getBaby
和getAsciiArt
。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹中的example07_02.java
文件中。
public final class MaineCoon extends VirtualDomesticCat {
public MaineCoon(
int age,
boolean isPregnant,
String name,
String favoriteToy) {
super(age, isPregnant, name, favoriteToy);
System.out.println("MaineCoon created.");
}
public MaineCoon(
int age, String name, String favoriteToy) {
this(age, false, name, favoriteToy);
}
public String getBaby() {
return "Maine Coon baby ";
}
@Override
public int getAverageNumberOfBabies() {
return 6;
}
public String getAsciiArt() {
return
" ^_^\n" +
" (*.*)\n" +
" |-|\n" +
" / \\\n";
}
}
我们没有将任何方法标记为final
,因为 final 类中的所有方法都是隐式 final 的。
然而,当我们在 JShell 之外运行 Java 代码时,最终的类将被创建,我们将无法对其进行子类化。
现在,我们将创建继承自VirtualAnimal
的VirtualBird
抽象类。下面几行显示了VirtualBird
抽象类的代码。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
public abstract class VirtualBird extends VirtualAnimal {
public String feathersColor;
public VirtualBird(int age, String feathersColor) {
super(age);
this.feathersColor = feathersColor;
System.out.println("VirtualBird created.");
}
public final boolean isAbleToFly() {
// Not all birds are able to fly in real-life
// However, all our virtual birds are able to fly
return true;
}
}
VirtualBird
抽象类继承先前声明的VirtualAnimal
抽象类的成员,并添加一个名为feathersColor
的新String
可变字段。新的抽象类声明了一个构造函数,该构造函数需要age
和feathersColor
的初始值来创建该类的实例。构造函数使用super
关键字从基类或超类调用构造函数,即在VirtualAnimal
类中定义的需要age
参数的构造函数。在超类中定义的构造函数完成其执行后,代码设置feathersColor
可变字段的值,并打印一条消息,指示已创建虚拟鸟。
VirtualBird
抽象类将继承的isAbleToFly
方法实现为返回true
的最终方法。我们希望确保我们应用领域中的所有虚拟鸟类都能够飞行。
现在,我们将创建继承自VirtualBird
的VirtualDomesticBird
抽象类。下面几行显示了VirtualDomesticBird
抽象类的代码。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
public abstract class VirtualDomesticBird extends VirtualBird {
public final String name;
public VirtualDomesticBird(int age,
String feathersColor,
String name) {
super(age, feathersColor);
this.name = name;
System.out.println("VirtualDomesticBird created.");
}
}
VirtualDomesticBird
抽象类继承先前声明的VirtualBird
抽象类的成员,并添加一个名为name
的新String
不可变字段。新的抽象类声明了一个构造函数,该构造函数需要age
、feathersColor
和name
的初始值来创建该类的实例。构造函数使用super
关键字从超类调用构造函数,即VirtualBird
类中定义的需要age
和feathersColor
参数的构造函数。在超类中定义的构造函数完成其执行后,代码设置name
不可变字段的值,并打印一条消息,指示已创建虚拟家鸟。
以下几行显示继承自VirtualDomesticBird
的Cockatiel
具体类的代码。我们将Cockatiel
声明为最终类,它实现了以下继承的抽象方法:isRideable
、isHerbivore
、isCarnivore
、getAverageNumberOfBabies
、getBaby
和getAsciiArt
。如前所述,final 类中的所有方法都是隐式 final 的。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
public final class Cockatiel extends VirtualDomesticBird {
public Cockatiel(int age,
String feathersColor, String name) {
super(age, feathersColor, name);
System.out.println("Cockatiel created.");
}
public boolean isRideable() {
return true;
}
public boolean isHerbivore() {
return true;
}
public boolean isCarnivore() {
return true;
}
public int getAverageNumberOfBabies() {
return 4;
}
public String getBaby() {
return "Cockatiel baby ";
}
public String getAsciiArt() {
return
" ///\n" +
" .////.\n" +
" // //\n" +
" \\ (*)\\\n" +
" (/ \\\n" +
" /\\ \\\n" +
" /// \\\\\n" +
" ///| |\n" +
" ////| |\n" +
" ////// /\n" +
" //// \\ \\\n" +
" \\\\ ^ ^\n" +
" \\\n" +
" \\\n";
}
}
以下几行显示继承自VirtualDomesticMammal
的VirtualDomesticRabbit
具体类的代码。我们将VirtualDomesticRabbit
声明为最终类,因为我们不需要额外的子类。在我们的应用领域中,我们将只有一种类型的虚拟家养兔子。最后一个类实现了以下继承的抽象方法:isAbleToFly
、isRideable
、isHerbivore
、isCarnivore
、getAverageNumberOfBabies
、getBaby
和getAsciiArt
。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹中的example07_02.java
文件中。
public final class VirtualDomesticRabbit extends VirtualDomesticMammal {
public VirtualDomesticRabbit(
int age,
boolean isPregnant,
String name,
String favoriteToy) {
super(age, isPregnant, name, favoriteToy);
System.out.println("VirtualDomesticRabbit created.");
}
public VirtualDomesticRabbit(
int age, String name, String favoriteToy) {
this(age, false, name, favoriteToy);
}
public final boolean isAbleToFly() {
return false;
}
public final boolean isRideable() {
return false;
}
public final boolean isHerbivore() {
return true;
}
public final boolean isCarnivore() {
return false;
}
public int getAverageNumberOfBabies() {
return 6;
}
public String getBaby() {
return "Rabbit baby ";
}
public String getAsciiArt() {
return
" /\\ /\\\n" +
" \\ V /\n" +
" | **)\n" +
" / /\n" +
" / \\_\\_\n" +
"*(__\\_\\\n";
}
}
JShell 忽略了final
修饰符,因此,使用final
修饰符声明的类将允许 JShell 中的子类。
在我们声明了所有的新类之后,我们将创建以下两个方法,它们接收VirtualAnimal
实例作为参数,即VirtualAnimal
实例或VirtualAnimal
的任何子类的实例。每个方法调用VirtualAnimal
类中定义的不同实例方法:printAverageNumberOfBabies
和printAsciiArg
。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
void printBabies(VirtualAnimal animal) {
animal.printAverageNumberOfBabies();
}
void printAsciiArt(VirtualAnimal animal) {
animal.printAsciiArt();
}
然后以下几行创建下一个类的实例:Cockatiel
、VirtualDomesticRabbit
和MaineCoon
。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
Cockatiel tweety =
new Cockatiel(3, "White", "Tweety");
VirtualDomesticRabbit bunny =
new VirtualDomesticRabbit(2, "Bunny", "Sneakers");
MaineCoon garfield =
new MaineCoon(3, "Garfield", "Lassagna");
下面的屏幕截图显示了在 JShell 中执行前几行的结果。在输入创建每个实例的代码后,我们将看到不同构造函数在 JShell 中显示的消息。这些消息将使我们能够轻松理解 Java 创建每个实例时调用的所有链式构造函数。
然后,以下几行使用前面创建的实例作为参数调用printBabies
和printAsciiArt
方法。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
System.out.println(tweety.name);
printBabies(tweety);
printAsciiArt(tweety);
System.out.println(bunny.name);
printBabies(bunny);
printAsciiArt(bunny);
System.out.println(garfield.name);
printBabies(garfield);
printAsciiArt(garfield);
这三个实例成为不同方法的VirtualAnimal
参数,即它们采用VirtualAnimal
实例的形式。但是,用于字段和方法的值不是在VirtualAnimal
类中声明的值。对printAverageNumberOfBabies
和printAsciiArt
实例方法的调用会考虑子类中声明的所有成员,因为每个实例都是VirtualAnimal
子类的实例:
将VirtualAnimal
实例作为参数接收的printBabies
和printAsciiArt
方法只能访问VirtualAnimal
类中定义的成员,因为参数类型为VirtualAnimal
。如果需要,我们可以打开在animal
参数中接收的Cockatiel
、VirtualDomesticRabbit
和MaineCoon
实例。但是,我们将在稍后讨论更高级的主题时处理这些场景。
下面的屏幕截图显示了在 JShell 中为名为tweety
的Cockatiel
实例执行前面几行的结果。
下面的屏幕截图显示了对 JShell 中名为bunny
的VirtualDomesticRabbit
实例执行前几行的结果。
下面的屏幕截图显示了对 JShell 中名为garfield
的MaineCoon
实例执行前几行的结果。
现在我们将创建另一个方法,该方法接收一个VirtualDomesticMammal
实例作为参数,即VirtualDomesticMammal
实例或VirtualDomesticMammal
的任何子类的实例。下面的函数调用VirtualDomesticMammal
类中定义的talk
实例方法。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
void makeItTalk(VirtualDomesticMammal domestic) {
domestic.talk();
}
然后,以下两行以VirtualDomesticRabbit
和MaineCoon
实例作为参数调用makeItTalk
方法:bunny
和garfield
。样本的代码文件包含在example07_02.java
文件的java_9_oop_chapter_07_01
文件夹中。
makeItTalk(bunny);
makeItTalk(garfield);
对作为参数接收的VirtualDomesticMammal
实例调用相同的方法会产生不同的结果。VirtualDomesticRabbit
没有重写继承的talk
方法,MaineCoon
类继承了VirtualDomesticCat
抽象类中重写的talk
方法,让家猫喵喵叫。以下屏幕截图显示了 JShell 中两个方法调用的结果:
VirtualAnimal
抽象类声明了两个实例方法,允许我们确定一个虚拟动物比另一个虚拟动物年轻还是年长:isYoungerThan
和isOlderThan
。这两种方法接收一个VirtualAnimal
参数,并返回在实例的age
值和接收实例的age
值之间应用运算符的结果。
下面的行为这三个实例调用了printAge
方法:tweety
、bunny
和garfield
。此方法在VirtualAnimal
类中声明。然后,接下来的几行调用isOlderThan
和isYoungerThan
方法,将这些实例作为参数,以显示比较不同实例年龄的结果。样本的代码文件包含在java_9_oop_chapter_07_01
文件夹中的example07_02.java
文件中。
tweety.printAge();
bunny.printAge();
garfield.printAge();
tweety.isOlderThan(bunny);
garfield.isYoungerThan(tweety);
bunny.isYoungerThan(garfield);
下面的屏幕截图显示了在 JShell 中执行前几行的结果:
- 以下哪一行声明了在任何子类中都不能重写的实例方法:
public void talk(): final {
public final void talk() {
public notOverrideable void talk() {
- 我们有一个名为
Shape
的抽象超类。Circle
类是Shape
的一个子类,是一个具体类。如果我们创建一个名为circle
的Circle
实例,该实例也将是:Shape
的一个实例。Circle
的一个子类。Circle
的抽象超类。
- 在 UML 图中,使用斜体文本格式的类名表示:
- 具体课程。
- 重写从其超类继承的至少一个成员的类。
- 抽象类。
- 以下哪一行声明了不能子类化的类:
public final class Dog extends VirtualAnimal {
public final class Dog subclasses VirtualAnimal {
public final Dog subclasses VirtualAnimal {
- 以下哪一行声明了一个名为
Circle
的具体类,该类可以被子类化,其超类是Shape
抽象类:public final class Shape extends Circle {
public class Shape extends Circle {
public concrete class Shape extends Circle {
在本章中,我们创建了许多抽象和具体的类。我们学会了控制子类是否可以覆盖成员,以及类是否可以被子类化。
我们使用了许多子类的实例,我们了解到对象可以有多种形式。我们在 JShell 中使用了许多实例及其方法,以了解我们编写的类和方法是如何执行的。我们使用的方法对具有公共超类的不同类的实例执行操作。
现在,您已经了解了成员继承和多态性,我们已经准备好使用 Java 9 中接口的契约式编程,这是我们将在下一章中讨论的主题。