学习目标:对父类的行为进行扩展,实现父类同一个行为具有不同的子类实现方式。
多态性
继承可以直接实现代码的复用,功能的扩展性是指继承后的类在父类的基础上增加新的行为,或者对父类的行为进行扩展,实现父类同一个行为具有不同的子类实现方式。
对象是类的实例化,多态性是指继承于同一父类的对象在运行过程中,表现出不同的行为。
案例1:在父类的基础上增加新的行为(方法)。
PUnit6项目unit包的Product类及其子类的继承结构如下图所示:

图中的纸质图书类,电子图书类、视频类和音频类均继承于Product类,被继承的Product称为父类,继承的纸质图书类,电子图书类、视频类和音频类称为子类,子类继承父类的所有属性和方法。现在要求EBook类除了输出Product类的公有属性外,还要输出EBook类的私有属性。从案例代码中可以看出,EBook类的父类Product类提供了输出公有属性的行为,代码如下:
//输出产品属性
public void outProduct()
{
System.out.println("产品名称:" + name);
System.out.println("产品价格:" + String.valueOf(price));
System.out.println("产品作者:" + author);
System.out.println("产品摘要:" + summary);
}
EBook类虽然可以调用父类的outProduct()方法输出公有属性,但无法输出EBook类的私有属性。这种情况下,可以在EBook类增加outEBook()方法,用于输出EBook类的私有属性,代码如下:
//输出电子图书属性
public void outEBook()
{
//调用父类的outProduct()输出公有属性
outProduct();
System.out.println("电子图书的格式:" + formation);
System.out.println("电子图书的文件大小:" + String.valueOf(filesize));
}
代码首先调用父类的outProduct()方法输出公有属性,然后再输出该类的私有属性。
在EBook类添加一个新的构造方法,用于初始化Ebook类的属性formation和filesize。
// 电子图书类的构造方法
public EBook(String inName, double inPrice, String inAuthor, String inSummary,String formation,long filesize) {
super(inName, inPrice, inAuthor, inSummary);
this.formation = formation;
this.filesize = filesize;
}
在PUnit6项目unit包下,新建EbookTest类,代码如下:
public static void main(String[] args) {
//实例化EBook对象
EBook ebook = new EBook("三国演义",21.2,"罗贯中","展现三国历史风云","PDF格式",310);
//调用outEBook()方法
ebook.outEBook();
}
程序执行结果如下图所示:

案例2:扩展父类的行为
案例1要求EBook类除了输出父类Product类的公有属性外,还要求输出EBook类的私有属性,例1给出的解决方案是在EBook类增加outEBook()方法,该方法首先调用父类的outProduct()方法输出父类的公有属性,然后再输出EBook类的私有属性。
其实,还有一种解决方案,在EBook类中重写父类的outProduct(),这样当EBook对象调用outProduct()方法时,其父类的outProduct()方法被忽略,而执行EBook类的outProduct()方法。代码如下:
//重写父类的outProduct()方法
@Override
public void outProduct()
{
//输出父类的公有属性
System.out.println("产品名称:" + name);
System.out.println("产品价格:" + String.valueOf(price));
System.out.println("产品作者:" + author);
System.out.println("产品摘要:" + summary);
//输出EBook类的私有属性
System.out.println("电子图书的格式:" + formation);
System.out.println("电子图书的文件大小:" + String.valueOf(filesize));
}
EBook类重写父类的outProduct()方法时,需要在outProduct()方法声明前面添加“@Override”注解,编译器会验证“@Override”注解下面的方法名称,在父类中是否有相同的方法名称。如果没有相同的方法名称,编译器会给出报错信息。
“@Override”是Java语言提供的注解,注解为编译器提供编译信息。例如编译器遇到“@Override”注解时,它会检查用于注解的方法是否是重写方法,如果发现其父类没有该方法时,会报编译错误。
在重写方法声明前面不添加“@Override”注解时,如果方法名称写错了,编译器不会给出报错信息,编译器会认为这是一个类方法。
修改PUnit6项目unit包内EbookTest类的代码:
public static void main(String[] args) {
//实例化EBook对象
EBook ebook = new EBook("三国演义",21.2,"罗贯中","展现三国历史风云","PDF格式",310);
//调用outProduct()方法
ebook.outProduct();
}
程序执行结果如下图所示:

案例2给出的子类重写父类的方法,就是面向对象的多态概念。在程序运行过程中,子类的行为代替了父类的行为。父类Product类有输出属性的方法outProduct(),而它的子类EBook类、Video类、PaperBook重写了outProduct()方法。换言之,对于不同的出版物产品,每个子类都会重写父类的一些方法,来实现自己的行为,这就构成了对象的多态性。
多态也就是多种表现形态,方法的重载和方法的重写是Java多态性的不同表现。方法重载是一个类中多态性的表现,方法重写是父类与子类之间多态性的一种表现。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载。如果在子类中定义某方法与其父类有相同的名称和参数,就称为方法的重写。
多态是面向对象编程的一大特征,利用多态特征编程,可以让应用程序具有良好的扩展性。通过子类对父类方法的重写和类方法的重载,可以在不改变原有代码的情况下扩展程序的功能。
父类与子类的类型转换
如果在程序中实例化了一个子类对象,是否能将指向该子类对象引用的子类变量,显示转换为父类类型的变量呢?或者指向父类对象引用的父类变量,能不能显示转换为子类类型的变量呢?
案例3:实例化EBook对象,将指向Ebook对象引用的变量显示转换为Product类型的变量,然后使用Product类型的变量分别调用父类和子类方法。验证子类类型变量转换为父类类型变量后的使用。
在PUnit5项目unit包内新建EBookTest1类,main方法代码如下:
public static void main(String[] args) {
//实例化EBook对象
EBook ebook = new EBook("三国演义",21.2,"罗贯中","展现三国历史风云","PDF格式",310);
// 转换为Product类型
Product product = (Product)ebook;
// 调用outProduct()方法
product.outProduct();
}
在main()方法中,声明了Ebook类型变量ebook,ebook指向实例化的EBook对象的引用。在main()方法中也声明了Product类型的变量product,然后使用显示转换将ebook强制转换为Product类型并赋值给product变量。
转换后的product变量虽然指向ebook对象的引用,但由于product变量类型是Product类型,因此product变量只能访问子类继承父类的成员变量和方法,无法访问子类自身的成员变量和方法。如果子类重写了父类的方法,会调用子类的方法。
程序执行结果如下图所示:

将子类类型显示转换为父类类型,这种转换称为“向上转型”。子类类型转换为父类类型后,不能访问子类新增加的成员变量和方法。例如,在上面的案例代码中,父类变量product虽然指向其子类的对象引用,但不能访问EBook类的formation和filesize属性。
既然子类类型可以转换为父类类型。那么,父类类型是否也能转换为子类类型?
案例4:实例化Product对象,将Product类型的变量显示转换为Ebook类型的变量。
在PUnit5项目unit包内新建EbookTest2类,main方法代码如下:
public static void main(String[] args) {
// 实例化Product对象
Product product = new Product("三国演义",21.2,"罗贯中","展现三国历史风云");
// 转换为EBook类型
EBook ebook = (EBook)product;
// 输出图书名称
System.out.println(ebook.getName());
// 调用outProduct()方法
ebook.outProduct();
}
执行上面的代码,程序会出错。因此父类类型变量指向父类对象引用时,该变量不能显示转换为子类类型变量。

如果父类类型变量指向子类对象引用时,可以使用显示转换将父类类型转换为子类类型,该转换称为“向下转型”。“向下转型”时,需要确保该父类类型变量指向的对象引用是子类的一个实例。
案例5:实例化EBook对象,将EBook对象的引用赋值给Product类型的变量,然后将该变量显示转换为Ebook类型的变量。
在PUnit5项目unit包内新建EbookTest3类,main方法代码如下:
public static void main(String[] args) {
// 实例化Product对象
Product product = new EBook("三国演义",21.2,"罗贯中","展现三国历史风云","PDF格式",310);
// 判断product是否是EBook的实例
if( product instanceof EBook )
{
// 类型转换
EBook ebook = (EBook)product;
// 调用outProduct()方法
ebook.outProduct();
}
}
在进行父类向子类的转换时,一个好的习惯是通过instanceof运算符来判断父类变量指向的对象引用是否是该子类的一个实例。
动态绑定
动态绑定是Java多态的一种表现形式,指的是代码在执行期间而不是编译时判断引用对象的实际类型,并根据其实际的类型,调用其相应的方法。
举个例子,家用电器包含电视、电脑、冰箱等电器设备,我们可以把家用电器抽象为类(Equipment),电视(ColorTv)、电脑(Computer)、冰箱(Icebox)作为家用电器类的子类。家用电器类提供support()方法,表示家用电器已通电,但通电后,电视、电脑、冰箱有不同的表现形式,可以用重写父类support()方法的技术来解决这个问题。
在程序运行过程中,让Equipment类型的变量指向Equipment子类的对象引用,当调用support()方法时,Java虚拟机会根据其实际的实例对象,调用该实例对象的support()方法。这就是java 的动态绑定,动态绑定是“向上转型”。
对象的动态绑定需要满足如下条件:
(1)类与类之间有继承关系;
(2)子类重写了父类的方法;
(3)父类变量指向子类对象;
(4)父类变量调用重写的方法。
案例6:建立电器类(父类),然后分别建立电视机类(子类)和计算机类(子类)。再建立电器管理类,负责调度电器通电操作。最后建立测试类,体验动态绑定的效果。
在PUnit6项目新建equipment包,在equipment包下新建Equipment类,代码如下:
package equipment;
public class Equipment {
// 通电操作
public void support()
{
System.out.println("通电功能!");
}
}
Equipment类是电器类的父类,它提供了support()方法,用于电器的通电操作。它的子类需要重写该方法。
在equipment包下新建ColorTv类,该类继承Equipment类。代码如下:
package equipment;
public class ColorTv extends Equipment {
//重写父类的support()方法
@Override
public void support()
{
System.out.println("电视机通电,看电视");
}
}
ColorTv类是Equipment类的子类,该类重写了父类的support()方法。
在equipment包下新建Computer类,该类继承Equipment类。代码如下:
package equipment;
public class Computer extends Equipment {
//重写父类的support()方法
@Override
public void support()
{
System.out.println("电脑通电,编写程序");
}
}
Computer类是Equipment类的子类,该类重写了父类的support()方法。
在equipment包下新建Discriminate类,该类是电器管理类,负责调度电器通电操作。代码如下:
package equipment;
public class Discriminate {
// 通电操作,参数类型为Equipment
public void through(Equipment equi)
{
equi.support();
}
}
Discriminate类提供了through()方法,用于电器类通电操作,传入参数类型是Equipment类型,该参数指向Equipment子类实例对象的引用。
在equipment包下新建EquipmentTest类,该类是测试类。代码如下:
public class EquipmentTest {
public static void main(String[] args) {
// 实例化Discriminate对象
Discriminate dis = new Discriminate();
// 调用dis对象的through()方法,实例化Computer对象
dis.through(new Computer());
// 调用dis对象的through()方法,实例化ColorTv对象
dis.through(new ColorTv());
}
}
EquipmentTest类是测试类,在EquipmentTest类的main()方法中,实例化Discriminate对象,并调用Discriminate对象的through()方法。through()方法需要传入Equipment类型的参数,传入的参数可以是Equipment子类实例对象的引用,这里采用了“向上转型”。
程序执行结果为:

动态绑定的优点是当需要扩展程序功能,添加新的类时,不需要修改原来已经存在的类结构,只需要增加新的类即可。
案例7:增加冰箱类。
在equipment包下新建Icebox类,该类继承Equipment类。代码如下:
package equipment;
public class Icebox extends Equipment {
//重写父类的support()方法
@Override
public void support()
{
System.out.println("冰箱通电,可以冰冻事物");
}
}
Icebox类是新添加的冰箱电器类,该类重写了父类的support()方法。
在equipment包下新建EquipmentTest1类,该类是测试类,用于测试新增加的Icebox类。代码如下:
package equipment;
public class EquipmentTest1 {
public static void main(String[] args) {
// 实例化Discriminate对象
Discriminate dis = new Discriminate();
// 调用dis对象的through()方法,实例化Computer对象
dis.through(new Computer());
// 调用dis对象的through()方法,实例化ColorTv对象
dis.through(new ColorTv());
// 调用dis对象的through()方法,实例Icebox对象
dis.through(new Icebox());
}
}
程序执行结果如下图所示:

由案例2可以看出,动态绑定可以很容易扩展程序功能,而无需改动原有的代码。
多态和动态绑定实际上是让“做什么”和“怎么做”分离了,本文中的家用电器类(Equipment)只负责基础通电功能(support),而(support)的具体实现由电视(ColorTv)、电脑(Computer)、冰箱(Icebox)子类负责。因此,只要电器类通电,则其电视、电脑、冰箱就开始运行,至于运行的内容,则有电视、电脑、冰箱内部负责。