JAVA编程讲义.继承和多态
前面我们学习了类、对象、包等面向对象编程的基本概念,初步了解了面向对象程序设计的基本知识,接下来我们继续学习面向对象编程的重要知识:继承和多态。通过类的继承机制,可以使用已有的类为基础派生出新类,无需编写重复的程序代码,很好地实现程序代码复用。多态是面向对象编程中继封装和继承之后的另一大特征,它具体是指同一个行为具有多个不同表现形式或形态的能力。使用多态机制,可以提高程序的抽象程度和简洁性,最大程度地降低类和程序模块间的耦合性,并提高程序的可扩展性和可维护性。从Java语言的底层逻辑上看,封装和继承是为实现多态做准备的。
6.1 类的继承
继承描述了类的所属关系,多个类通过继承可形成一个关系体系,进而在原有类的基础上派生出新的类,扩展新的功能,从而实现了代码的复用。采用继承机制来设计软件系统中的类,可以提高程序的抽象程度,降低程序维护工作量,提高开发效率。
6.1.1 继承的概念
在现实生活中,“继承”是指一个对象直接使用另一对象的属性和方法,也可以指按照法律或遵照遗嘱接受死者的财产、职务、头衔、地位等。我们先利用现实生活中的例子来说明继承的含义。如图6.1所示,卡车(Truck类)和公交车(Bus类)都属于汽车(Car类),它们都有发动机(engine)和轮子(wheel),都可以行驶(run)和刹车(stop),但是卡车类新增了载重量(capacity)、拉货(load)和卸货(unload)方法,公共车类新增了载客量(capacity)、报站(busstop)和停靠(dock)方法。
在面向对象程序设计中,一个新类从已经存在的类中获得成员变量或成员方法,这种现象称为继承。提供继承信息的类被称为父类(超类、基类),得到继承信息的类被称为子类(派生类)。一个父类可以同时拥有多个子类,但Java语言不支持多重继承,所以一个类只能有一个父类。父类是所有子类的公共成员变量和成员方法的集合,而子类是父类的特例,子类继承父类的成员变量和成员方法,可以修改父类的成员变量或重写父类的方法,也可以增加新的成员变量或成员方法。采用继承机制来设计系统中的类,可以提高程序的抽象程度,更加接近于人类的思维方式。
继承机制的显著特点之一是可以实现代码复用。以第5章的Dog类为例,我们能够以它为样板而设计新的Cat类,代码如下:
1 public class Cat {
2 private String name;
3 private int age;
4 public void show(){
5 System.out.println("猫咪"+name+"在吃咸鱼");
6 }
7 }
仔细观察这段代码我们会发现,在Dog类和Cat类中有很多重复代码, 假设我们后续增加Pig类、Monkey类等类,重复代码还不断增加。这时候,我们就可以提取这些类的相同的成员变量和成员方法,设计一个Anima(动物类),使Dog类、Cat类等继承Animal类。
6.1.2 继承的使用
在Java语言中,类的继承通过extends关键字来实现,在定义类时通过extends关键字指出新定义类的父类,表示在两个类之间建立了继承关系。新定义的类称为子类,它可以从父类那里继承所有非private(私有)的成员。Java继承的语法格式如下:
class 子类A extends 父类B{
// 代码块
}
该语法表示子类A派生于父类B,如果类B又是某个类的子类,则类A同时也是该类的间接子类。如果没有extends关键字,则该类默认为java.lang.Object类的子类。Java语言中的所有类都是直接或间接地继承java.langObject类得到的,所以之前所有例子中的类均是java.lang.Object类的子类。
接下来,通过案例来演示如何通过继承Animal类来派生Dog类、Cat类,从而形成类的继承体现,实现代码复用,如例6-1所示。
例6-1 Demo0601.java
1 package com.aaa.p0601; // 需要先建立包
2
3 class Animal { // 定义父类Animal类
4 public String name;
5 public int age;
6 public void show() {
7 System.out.println("名字是" + name + ",年龄:" + age);
8 }
9 }
10 class Dog extends Animal { // 定义子类Dog类
11 String color;
12 public void eat(){
13 System.out.println(color + "色的狗狗在啃骨头");
14 }
15 }
16 class Cat extends Animal{ // 定义子类Cat类
17 }
18 public class Demo0601{
19 public static void main(String[] args) {
20 Dog d = new Dog();
21 d.name = "旺财";
22 d.age = 3;
23 d.color = "黑色";
24 d.show();
25 d.eat();
26 }
27 }
程序的运行结果如下:
名字是旺财,年龄:3
黑色的狗狗在啃骨头
例6-1中,Dog类通过extends关键字继承了Animal类,它就是Animal的子类,Cat类同样如此。从程序运行结果中可发现,Dog类虽然没有定义name、age成员变量和show()成员方法,但却能访问这些成员,说明子类可以继承父类所有的成员。
编程技巧: 使用继承时,先定义父类(超类、基类),再定义子类(派生类),很好地体现了面向对象的思想。
6.2 方法重写
在继承机制中,当父类中的方法无法满足子类需求或子类具有特有的功能的时候,就需要在子类中重写父类的方法(也称为方法覆盖)。准确地说,方法重写建立在继承关系之上,具体是指子类从父类中继承方法时,如果在子类中有定义名称、参数个数、参数类型均与父类中的方法完全一致,但方法内容不同,即子类修改了父类中方法的实现,此时创建的子类对象调用这个方法时,程序会调用子类的方法来执行,即子类的方法重写了从父类继承过来的同名方法。
当子类重写父类方法的时候,可以使用与父类相同的方法名及参数列表,也可以执行不同的功能。子类既可以隐藏和访问父类的方法,也可以覆盖继承父类的方法,体现了Java语言的优越性和灵活性。
接下来,通过案例来演示方法重写。以Animal为父类,Dog类、Cat类继承Animal类,Dog类重写了父类的show()方法,如例6-2所示。
例6-2 Demo0602.java
1 package com.aaa.p0602;
2
3 class Animal { // 定义父类Animal类
4 public String name;
5 public int age;
6 public Animal(){
7 System.out.println("调用了动物类的构造方法Animal()");
8 }
9 public void show() {
10 System.out.println("父类名字是" + name + ",年龄:" + age);
11 }
12 }
13 class Dog extends Animal { // 定义子类Dog类
14 String color;
15 public Dog(){
16 System.out.println("调用了狗类的构造方法Dog()");
17 }
18 public void eat(){
19 System.out.println(color + "色的狗狗在啃骨头");
20 }
21 public void show() {
22 System.out.println("狗狗名字是" + name + ",颜色是" + color);
23 }
24 }
25 class Cat extends Animal { // 定义子类Cat类
26 }
27 public class Demo0602{
28 public static void main(String[] args) {
29 Dog d = new Dog();
30 d.name = "旺财";
31 d.age = 2;
32 d.color = "黑色";
33 d.show();
34 d.eat();
35 Cat c = new Cat();
36 c.show();
37 }
38 }
程序的运行结果如下:
调用了动物类的构造方法Animal()
调用了狗类的构造方法Dog()
名字是旺财,颜色是黑色
黑色色的狗狗在啃骨头
调用了动物类的构造方法Animal()
父类名字是null,年龄:0
例6-2中,Dog类继承了Animal类的show()方法,但在子类Dog中对父类的show()进行了重写,Cat类只是继承了Animal类。从程序运行结果中可发现,第29行代码直接调用Dog()构造方法,理应输出“调用了狗类的构造方法Dog()”,但是先输出“调用了动物类的构造方法Animal()”,这是因为在执行子类的构造方法前,会首先调用父类中无参的构造方法,其目的是为了继承自父类的成员进行初始化操作。 第33行代码在调用Dog类对象的show()方法时,只会调用子类重写的方法,并不会调用父类的show()方法。Cat类中没有重写父类的show()方法,第36行代码中的Cat类对象仍然调用的是父类的show()方法,同时由于没有给Cat类对象的成员变量name和age赋值,所以显示的名字是默认值“null”,年龄也是默认值“0”。
方法重写时必须注意如下事项:
在方法重写时必须考虑权限,即被子类重写的方法不能拥有比父类方法更加严格的访问权限。
构造方法不能被继承,不能被重写。
在第5章我们已经学习过方法重载,现将方法重载与方法重写的主要区别列于表6.1。
表6.1 方法重载和方法重写的区别
方法重载 |
方法重写 |
参数类型、参数个数、参数类型的顺序不同 |
参数个数、参数类型、参数类型的顺序完全相同 |
返回值类型可以相同,可以不相同 |
返回值类型完全相同 |
访问权限修饰符可相同,可不同 |
子类重写父类方法时,修饰符权限要大于等于父类修饰符权限,父类被private修饰的方法不能被重写 |
注意:重写方法所抛出的异常不能比原方法更多,如果抛出比父类方法更多的异常,则在编译时会报错。有关异常的知识点将在后续章节讲解。
6.3 super关键字
当子类继承父类后,有时候需要访问父类的一些成员变量或方法,这时候可以使用super关键字来调用这些成员变量或方法。super关键字的用法有如下3种:
在子类的构造方法中,访问父类的构造方法。语法格式如下:
super([参数列表])
在子类的成员方法中,访问父类的成员变量。语法格式如下:
super.成员变量
在子类的成员方法中,访问父类的成员方法。语法格式如下:
super.成员方法([实参列表])
6.3.1 super访问父类构造方法
通过例6-2可以发现,程序中即使没有指明子类来调用父类的构造方法,但在程序执行时还是会先调用父类中的无参构造方法,以便进行成员初始化操作。但是,如果父类中有多个构造方法,如何才能调用父类中的某个特定构造方法呢?
Java语言出于安全性的考虑,对于对象的初始化要求是非常严格的。比如,Java要求一个父类的对象要在子类运行前完全初始化。
super关键字可以用于在子类构造方法中调用父类的构造方法。如果在子类的构造方法中没有明确调用父类的构造方法,则在执行子类的构造方法时会自动调用父类的默认无参构造方法;如果在子类的构造方法中调用了父类的构造方法,则调用语句必须出现在构造方法的第一行。
接下来,通过案例来演示super关键字访问父类构造方法,如例6-3所示。
例6-3 Demo0603.java
1 package com.aaa.p060301;
2
3 class Animal { // 动物类
4 private String name;
5 private int age;
6 public Animal(){
7 System.out.println("调用了动物类的无参构造方法");
8 }
9 public Animal(String name,int age){
10 System.out.println("调用Animal类的有参构造方法");
11 this.name = name;
12 this.age = age;
13 }
14 public void show() {
15 System.out.println("名字:" + name + ",年龄:" + age);
16 }
17 }
18 class Dog extends Animal { // 动物类的子类Dog类
19 String color;
20 public Dog(){ // 子类无参构造方法
21 System.out.println("调用了狗类的无参构造方法");
22 }
23 public Dog(String name,int age,String c){ // 子类有参构造方法
24 super(name,age); // 调用父类的有参构造方法
25 color = c;
26 System.out.println("调用了狗类的有参构造方法");
27 System.out.println(color + "的狗狗");
28 }
29 }
30 public class Demo0603 {
31 public static void main(String[] args) {
32 Dog d1 = new Dog();
33 d1.show();
34 System.out.println("----------------------");
35 Dog d2 = new Dog("花花",8,"黄色");
36 d2.show();
37 }
38 }
程序的运行结果如下:
调用了动物类的无参构造方法
调用了狗类的无参构造方法
名字:null,年龄:0
----------------------
调用Animal类的有参构造方法
调用了狗类的有参构造方法
黄色的狗狗
名字:花花,年龄:8
例6-3运行结果显示,Dog类继承自Animal类,Animal类有两个成员变量name和age,一个无参构造方法和一个有参构造方法,在子类Dog类的有参构造方法中(第24行)使用super(name,age)将参数name和age传递到父类Animal的有参构造方法内,因此只要子类的有参构造方法被调用,其父类对应的有参构造方法也会被调用。第32行代码调用Dog类的无参构造方法Dog(),该构造方法会自动调用父类中对应的无参构造方法Animal(),然后再执行自己的构造方法Dog()。第35行代码调用子类带3个参数的构造方法,该方法通过子类的有参构造方法语句super(name,age)(第24行)调用父类的有参构造方法(第9行)。如果第24行代码不在子类构造方法第一行,则会编译报错“Call to 'super()' must be first statement in constructor body”,中文含义为“对super的调用必须是构造器中的第一个语句”,因为在程序里面先执行的是子类的操作,而后才是父类初始化过程,这明显是不对的。
6.3.2 super访问父类成员变量和方法
在子类中使用super关键字除了可以访问父类的构造方法外,还可以访问父类的成员变量和成员方法,但是super关键字不能访问在子类的成员。
接下来,通过案例来演示super访问父类的成员变量和成员方法,如例6-4所示。
例6-4 Demo0604.java
1 package com.aaa.p060302;
2
3 class Animal {
4 protected String name;
5 protected int age;
6 public Animal(){
7 }
8 public Animal(String name,int age){
9 this.name = name;
10 this.age = age;
11 }
12 public void show() {
13 System.out.println("父类名字:" + name + ",年龄:" + age);
14 }
15 }
16 class Dog extends Animal {
17 String color;
18 int age = 10;
19 public Dog(String xm,int age,String c){
20 super.name = xm;
21 super.age = age;
22 color = c;
23 }
24 public void show(){
25 System.out.println("子类Dog中的成员变量age:" + age);
26 super.show(); // 调用父类的show()方法
27 System.out.println("子类Dog," + color + "的狗狗");
28 }
29 }
30 public class Demo0604 {
31 public static void main(String[] args) {
32 Dog d1 = new Dog("花花",11,"黄色");
33 d1.show();
34 }
35 }
程序的运行结果如下:
子类Dog中的成员变量age:10
父类名字:花花,年龄:11
子类Dog,黄色的狗狗
例6-4运行结果显示,子类Dog的构造方法没有使用super关键字来调用父类的有参构造方法,在父类Animal的第4行和第5行代码中将成员变量name和age声明为protected的,可以在子类构造方法中对父类的成员变量进行访问(第20行和第21行)。同样,由于父类的show()方法声明为public的(第12行),所以可以在子类的普通方法中使用super关键字来调用(第26行)。
想一想:在定义父类的时候,可以使用super关键字吗?
6.4 final关键字
final是Java中的一个重要的关键字,可以修饰变量(成员变量或局部变量)、方法以及类。被final修饰的变量(成员变量或局部变量)称为常量,常量不能再次改变其引用。被final修饰的方法称为final方法,final方法不能被子类重写。被final修饰的类称为final类,final类通常是完整的,不能被继承。
6.4.1 final修饰局部变量
使用final关键字修饰的局部变量,只能被赋值一次。如果再次对该局部变量进行赋值,则程序在编译时会报错,如例6-5所示。
例6-5 Demo0605.java
1 package com.aaa.p060401;
2
3 public class Demo0605 {
4 public static void main(String[] args) {
5 final String JAVA_VERSION = "15.0.1";
6 JAVA_VERSION = "17.0.1";
7 System.out.println("当前JAVA版本号:" + JAVA_VERSION);
8 }
9 }
程序编译报错,提示“Cannot assign a value to final variable 'JAVA_VERSION'”,中文含义为“无法为最终变量JAVA_VERSION分配值”,出现错误的原因是,final修饰的变量为常量,只能初始化一次,初始化后不能再修改。具体程序代码是第5行代码将当前Java版本号15.0.1赋值给了常量字符串JAVA_VERSION,因此第6号代码再次赋值时会编译报错。将第5行代码修改如下:
final String JAVA_VERSION;
程序的运行结果下:
当前JAVA版本号:17.0.1
再次运行程序可以发现,第5行代码的常量JAVA_VERSION只做声明,在第6行代码赋值一次,程序运行不会报错。
6.4.2 final修饰成员变量
对于成员变量来说,一旦使用final关键字,也是一样不能改变,和final修饰成员变量不同点是final修饰的成员变量必须声明的时候赋值或者在构造方法中对成员变量赋值,但是只能二者选一,如果没有直接赋值,那就必须保证所有重载的构造方法最终都会对final的成员变量进行了赋值。
接下来,通过实例演示使用final修饰成员变量,如例6-6所示。
例6-6 Demo0606.java
1 package com.aaa.p060402;
2
3 class Father {
4 final int CODE; // 使用final修饰成员变量
5 public void show() {
6 System.out.println("当前常量代码CODE为:"+this.CODE);
7 }
8 }
9 public class Demo0606 {
10 public static void main(String[] args) {
11 Father f = new Father(); // 创建Father对象
12 f.show();
13 }
14 }
程序编译报错,提示“Variable 'CODE' might not have been initialized”,中文含义为“变量 CODE 未在默认构造器中初始化”,出现错误的原因是,Java虚拟机不会为final修饰的变量默认初始化。
因此,使用final修饰成员变量时,需要在声明时进行赋值,或者在构造方法中进行初始化。下面在构造方法中初始化final修饰的成员变量,修改第4行代码如下:
final CODE= 1024;
或者修改类Fater的代码如下:
1 class Father {
2 final int CODE; // 使用final修饰成员变量
3 String name;
4 Father(){
5 CODE=1024;
6 }
7 Father(String n, int code){
8 name=n;
9 CODE = code;
10 }
11 public void show() {
12 System.out.println("当前常量代码CODE为:"+this.CODE);
13 }
14 }
两种修改方式均可,运行程序之后的结果如下:
当前常量代码CODE为:1024
注意:对于父类中使用final修饰的成员变量,子类是可以继承的,但不能修改该成员变量的值。
6.4.3 final修饰成员方法
使用final关键字修饰的方法,表示子类不能重写此方法,称为最终方法。对于一些比较重要且不希望被子类重写的成员方法,可以使用final关键字对其进行修饰。
接下来,通过实例演示使用final修饰成员方法,如例6-7所示。
例6-7 Demo0607.java
1 package com.aaa.p060403;
2 class Father {
3 public final void show() {
4 System.out.println("final修饰show()方法");
5 }
6 }
7 class Son extends Father {
8 public void show() { // 重写父类方法
9 System.out.println("重写父类show()方法");
10 }
11 }
12 public class Demo0607 {
13 public static void main(String[] args) {
14 Father f = new Father(); // 创建Father类对象
15 f.show();
16 }
17 }
程序编译报错,提示“'show()' cannot override 'show()' in 'com.aaa.p060403.Father'; overridden method is final”,中文含义为“show()无法覆盖com.aaa.p060403.Father中的show(),被覆盖的方法为final”,出现错误的原因是被final修饰的成员方法不能再被子类重写。
知识点拨:当一个常数或方法需要在程序里反复使用的时候,我们就可以把它定义为static final,这样内存就不用重复地申请和释放空间。用static final来修饰的成员变量或成员方法,可理解为“全局常量”或“全局方法”。
6.4.4 final修饰类
如果一个类被final所修饰,则说明这个类不能被其他类所继承,即该类不可能有子类,这种类被称为最终类。
接下来,通过实例来演示使用final修饰类,如例6-8所示。
例6-8 Demo0608.java
1 package com.aaa.p060404;
2
3 final class Father {
4 }
5 class Son extends Father { // 编译报错
6 }
7 public class Demo0608 {
8 public static void main(String[] args) {
9 Son s = new Son(); // 创建Son对象
10 }
11 }
程序编译报错,提示“'Cannot inherit from final 'com.aaa.p060404.Father'”,中文含义为“无法从最终类com.aaa.p060404.Father进行继承”,出现错误的原因是被final所修饰的类为最终,不能被子类继承。
注意:所有已被private修饰的私有方法以及所有包含在final类中的方法,都默认是最终方法。
6.5 多态
“多态”这个词来源于希腊语,意思是“多种形式”,最早应用于生物学,指同在一个生物群体,各个体之间存在的生理学、形态学、和生化学的差异。在Java中,多态是指允许同一程序指令在不同的上下文中实现不同的操作。具体来讲,当一个方法名作为一个指令时,根据执行该方法的对象类型,可能产生不同的动作。
面向对象程序设计中包括两种形式的多态,分别是编译时多态和运行时多态。编译时多态是通过重载技术实现的,即在一个类中相同的方法名可用来定义不同的方法。运行时多态是基于继承机制建立的,是在运行时动态产生的多态性,下面主要对运行时多态进行讲解,本书中将运行时多态简称为多态。
多态是面向对象的重要特性,它可以提高程序的抽象程度和可扩展性,最大程度地降低类和程序模块间的耦合度。
6.5.1 为什么需要多态
下面通过一个现实生活中的例子来认识一下多态,这个例子模拟的是主人喂养宠物,代码如下:
1 class Pet{ // 宠物类
2 private String name = "无名"; // 昵称
3 private int health = 100; // 健康值
4 public void eat(){}
5 }
6 class Dog extends Pet{ // 狗类继承自宠物类
7 public void eat(){ // 重写eat()方法
8 System.out.println("狗狗在吃饭...");
9 }
10 }
11 class Cat extends Pet{
12 public void eat(){ // 猫类继承自宠物类
13 System.out.println("猫咪在吃饭...");
14 }
15 }
16 class Master{
17 private String name;
18 public void feed(Dog dog){
19 dog.eat();
20 }
21 public void feed(Cat cat){
22 cat.eat();
23 }
24 }
在上述代码中,Pet类是父类,Dog类和Cat类是子类,并且重写了父类的eat方法。Master类分别为Dog类和Cat类定义了喂食的方法,这两个方法构成了方法重载,可以实现主人喂养宠物的功能。但是,假如主人以后要喂养更多的不同种类的宠物时该怎么办呢?比如说,现在主人要喂养鹦鹉,我们除了需要先新增一个Parrot类(鹦鹉类)之外,还必须在Master类中新增一个给鹦鹉喂食的方法,当需要删除某个宠物类时,则需要进行删除相关宠物类的代码。那么,有没有更好的解决办法呢?这时候就可以使用多态进行代码优化。
6.5.2 多态的概念
编程是一个将具体世界进行抽象化的过程,多态是抽象化的一种体现,即将一系列具体事物的共同点抽象出来, 再通过这个抽象的事物, 与不同的具体事物进行对话。对不同类的对象发出相同的消息,将会有不同的行为。例如,公司规定所有员工在九点钟开始工作,而不需要由专业领导对财务人员说:“开始财务工作”、对行政人员说“开始行政工作”……。使用专业术语来定义多态就是:同一个引用类型,使用不同的实例可以执行不同的操作,即父类引用子类对象。
下面通过代码来理解一下多态,参考代码如下:
Pet p = new Dog();
p.eat();
…
在上述代码中,我们将子类对象赋值给一个父类对象,这就是所谓的父类引用子类对象,或者说一个父类的引用指向了一个子类对象。代码在执行时调用的都是子类中重写过的eat()方法,而不是父类中的eat()方法,这就是多态。
6.5.3 多态的实现
同一个父类派生出的多个子类可被当作同一种类型,相同的代码就可以处理所有不同的类型。在6.5.1节中,Pet类是父类,Dog类和Cat类是子类,针对父类中的eat()方法,子类都重写了这个方法,针对6.5.2节中的代码“p.eat()”,当前赋值的是Dog类的对象,当然也可以是Cat类或其他Pet类的子类对象,即可能会得到多种运行结果,具体的结果取决于程序运行时父类对象p所指向对象的类型。
接下来,通过实例来演示多态的具体实现,如例6-9所示。
例6-9 Demo0609.java
1 package com.aaa.p060503;
2 class Pet{ // 宠物类
3 private String name = "无名"; // 昵称
4 private int health = 100; // 健康值
5 public void eat(){}
6 }
7 // Dog类、Cat类重写父类的eat方法
8 class Dog extends Pet{ // 狗类继承自宠物类
9 public void eat(){ // 重写eat()方法
10 System.out.println("狗狗在啃骨头...");
11 }
12 }
13 class Cat extends Pet{
14 public void eat(){ // 猫类继承自宠物类
15 System.out.println("猫咪在吃鱼干...");
16 }
17 }
18 // 父类引用子类对象
19 class Master{
20 private String name; // 主人的姓名
21 // 通过传递参数实现父类引用子类对象
22 public void feed(Pet p){
23 p.eat(); // 主人喂宠物,具体类型由传入的类型决定
24 }
25 }
26 public class Demo0801 {
27 public static void main(String[] args) {
28 Pet d = new Dog();
29 Pet c = new Cat();
30 Master m=new Master();
31 m.feed(d);
32 m.feed(c);
33 }
34 }
程序的运行结果如下:
狗狗在啃骨头...
猫咪在吃鱼干...
通过程序结果可以发现,使用多态解决了主人喂养宠物的问题。这时不管主人将来喂养多少种宠物,我们只需要新增子类继承Pet类并重写eat()方法就可以了,而Master类中始终只需要一个feed()方法。
使用多态可以提高代码的可扩展性和可维护性,使用父类作为方法形参是实现多态的常用方式。这里要注意的是,在进行方法调用时必须调用子类重写过的父类的方法,子类中独有的方法是不能通过父类引用调用到的。
6.5.4 对象类型转换
对象的类型转换是指子类与父类之间的转换,主要包括如下两种:
向上转型:指从子类到父类的转换。
向下转型:指从父类到子类的转换。
1.向上转型
类的继承关系使子类具有父类的成员变量和成员方法,这意味着父类的成员可以在它的派生子类中使用,也就是说子类的对象也是父类的对象,子类对象既可以作为该子类的类型也可以作为父类的类型。将一个子类对象的引用转换为该子类的父类引用,称为向上转型。向上转型的语法格式如下:
父类|接口 对象名 = new 子类();
参考代码如下:
Pet p = new Cat();
p.eat(); // 调用子类的eat()方法
向上转型,体现的是“is a”关系。例如,一个Cat类的对象(猫)是一个Pet类(宠物)的对象,这是没有问题的,因为Cat类继承了父类Pet的成员变量和成员方法,这意味着Pet类的对象p调用eat()方法时,可以完成相应的操作。
由于子类通常包含比父类更多的成员变量和成员方法,因此任何一个从父类派生出的各种子类都可以作为父类的类型对待。也就是说,从一个特殊、具体的类型到一个通用、抽象类型的转换,肯定是安全的。例6-9就是通过向上转型实现的多态,本处不再赘述。
2.向下转型
向下转型也称为对象的强制类型转换,是指将父类对象类型的变量强制转换为子类类型。向上转型使父类对象可以指向子类对象,但通过父类对象只能访问父类中定义的成员变量和方法,子类特有的部分成员被隐藏,不能被访问。只有将父类对象强制转换为具体的子类类型,才能访问子类的特有成员。向下转型的语法格式如下:
子类 对象名 = (子类)父类对象;
参考代码如下所示。
Pet p = new Pet();
Dog d1= new Dog ();
Dog d2;
// d2 = (Dog)p; 这种写法转换失败,因为p指向的类型是Pet,d2的引用类型为Dog
// 下面的写法正确,首先将p的引用指向d2,此时的p的类型变成了Dog, 所以同类型之间可以转换
p = d1;
d2 = (Dog)p;
向下转型可以调用子类类型中所有的成员,不过需要注意的是,如果父类引用对象指向的是子类对象,那么在向下转型的过程中是安全的,也就是编译是不会出错误。但是,如果父类引用对象是父类本身,那么在向下转型的过程是不安全的,编译不会出错,但是运行时会出现Java 强制类型转换异常。
针对这种情况,Java语言中提供了instanceof关键字,可以先通过instanceof关键字来判断某个对象是否属于某种数据类型(类或接口),当前面的判断结果为true时再执行转换,其语法格式如下:
引用类型变量 instanceof 类名|接口
接下来,通过实例来演示向下转型,我们对例6-9的main()方法进行修改,具体如例6-10所示。
例6-10 Demo0610.java
1 package com.aaa.p060504;
2
3 class Pet{ // 宠物类
4 private String name = "无名"; // 昵称
5 private int health = 100; // 健康值
6 public void eat(){}
7 }
8 class Dog extends Pet{ // 狗类继承自宠物类
9 public void eat(){ // 重写eat()方法
10 System.out.println("狗狗在吃啃骨头...");
11 }
12 }
13
14 class Cat extends Pet{
15 public void eat(){ // 猫类继承自宠物类
16 System.out.println("猫咪在吃鱼干...");
17 }
18 }
19 class Master{
20 private String name; // 主人的姓名
21 public void feed(Pet p){
22 p.eat(); // 主人喂宠物,具体类型,由传入的类型决定
23 }
24 }
25 public class Demo0610 {
26 public static void main(String[] args) {
27 Pet p = new Dog(); // 向上转型
28 if (p instanceof Dog) { // 判断对象是否是Dog类型
29 Dog o = (Dog) p; // 向下转型
30 o.eat();
31 }else if(p instanceof Cat){ // 判断对象是否是Cat类型
32 Cat c=(Cat) p; // 向下转型
33 c.eat();
34 }
35 }
36 }
程序的运行结果如下:
狗狗在啃骨头...
例6-10中,通过程序运行结果可发现,instanceof能准确判断出对象是否是某个类的实例,有效地防止了运行时异常的发生,提高了程序的健壮性。
注意:在使用instanceof 运算符时,该运算符前面的操作数的编译时类型要么与后面的类型相同,要么与后面的类型具有继承关系,否则会引起编译错误。
6.6 Object类
在Java语言中,Object类是所有类的父类,该类是java.lang类库中的一个类,所有的类都是直接或者间接地继承该类。如果一个类没有使用extends关键字,则默认该类是Object的子类,所以说Object类是所有类的源。也就是说,以下两种类的定义的最终效果是完全相同的:
class Dog{ }
class Dog extends Object{ }
Object类中提供了很多方法,下面讲解常用的3个方法,如表6.2所示。
表6.2 Object类的常用方法
常用方法 |
功能说明 |
public boolean equals(Object obj) |
判断两个对象所指向的是否为同一个对象 |
public final Class getClass() |
返回运行getClass()方法的对象所属的类 |
public String toString() |
取得对象信息,返回该对象的字符串表示 |
6.6.1 equals()方法
在前面章节,我们学习了使用比较运算符“= =”比较两个变量是否相等。除此之外,还可以使用equals()方法比较两个变量是否相等。
接下来,通过实例来演示如何使用equals()方法比较两个对象是否相等,如例6-11所示。
例6-11 Demo0611.java
1 package com.aaa.p060601;
2 class Cat{
3 String name;
4 public Cat(String name){
5 this.name=name;
6 }
7 }
8 public class Demo0611 {
9 public static void main(String[] args) {
10 Cat cat1 = new Cat("机器猫");
11 Cat cat2 = new Cat("大脸猫");
12 System.out.println("cat1.equals(cat2)是"+cat1.equals(cat2));
13 System.out.println("cat1==cat2是"+(cat1==cat2));
14 }
15 }
程序的运行结果如下:
cat1.equals(cat2)是false
cat1==cat2是false
从程序运行结果中可发现,equals()方法与直接使用“==”运算符检测两个对象结果相同,这是由于equals()方法的默认实现就是用“==”运算符检测两个引用变量是否指向同一对象,即比较的是对象在计算机中的地址。
6.6.2 getClass()方法
getClass()方法是Object类里所定义的方法,而Object类是所有类的父类,所以在任何类中均可调用这个方法。getClass()方法的功能是返回运行时的对象所属的类。一个iava.lang.Class对象代表了Java应用程序运行时所加载的类或接口的实例,Class对象由JVM自动产生,每当一个类被加载时,JVM就自动为其生成一个Class对象。由于Class类没有构造方法,所以可以通过Object类的getClass()方法来取得对象对应的Class对象。在取得Class对象之后,就可以通过Class对象的一些方法来获取类的基本信息。
接下来,通过实例来演示getClass()方法的使用,如例6-12所示。
例6-12 Demo0612.java
1 package com.aaa.p060602;
2
3 class Cat{
4 private String name;
5 public Cat(String name){
6 this.name = name;
7 }
8 }
9 public class Demo0612 {
10 public static void main(String[] args) {
11 Cat cat = new Cat("机器猫");
12 Class obj = cat.getClass(); // 使用对象cat调用getClass()方法
13 System.out.println("对象cat所属的类为:" + obj);
14 }
15 }
程序的运行结果如下:
对象cat所属的类为:class com.aaa.p0611.Cat
从程序运行结果中可发现,Cat类的对象cat指向新的对象,并去调用getClass()方法,该方法继承自Object类,虽然在Cat类中没有定义它,但是可以由对象cat调用。getClass()方法返回的是Class类型,所以需要定义一个Class类型变量obj接收。
注意:例6-12第12行代码中的Class的首字母“C”为大写。
6.6.3 toString()方法
toString()方法的功能是将调用该方法的对象内容转换为字符串,并返回该字符串,返回内容由该对象所属类名、@和对象十六进制形式的内存地址组成,如例6-13所示。
例6-13 Demo0613.java
1 package com.aaa.p060603;
2
3 class Cat {
4 private String name;
5 private int age;
6 public Cat (String name, int age) {
7 this.name = name;
8 this.age = age;
9 }
10 }
11 public class Demo0613 {
12 public static void main(String[] args) {
13 Cat cat = new Cat("机器猫");
14 // 调用对象的toString方法
15 System.out.println(cat.toString());
16 // 直接打印对象
17 System.out.println(cat);
18 }
19 }
程序的运行结果如下:
com.aaa.p0612.Cat@7ef20235
com.aaa.p0612.Cat@7ef20235
例6-13中,默认打印了对象信息,从程序运行结果中可发现,直接打印对象和打印对象的toString()方法返回值相同,也就是说对象输出一定会调用Object类的toString()方法。
6.7 本章小结
Java通过extends关键字实现类的继承,子类可以继承父类的成员变量和成员方法。同时,Java是一门单继承的语言。通过继承机制,能够很好地实现程序代码复用,避免编写重复的程序代码。
方法重写是指在子类当中定义名称、参数个数、类型均与父类相同的方法,用于覆盖父类中的方法。
父类有多个构造方法时,如果要调用特定的构造方法,则可在子类的构造方法中,通过super关键字来调用。super关键字可以用来调用父类中的成员变量或成员方法,语法格式为“super.成员变量”、“super.成员方法”; 使用super()来调用父类的构造方法,必须写在构造方法的第一行。
使用final关键字修饰的变量是一个常量,在其他地方不能被修改;使用final关键字修饰的成员方法不能在子类中重写;使用final关键字修饰的类称为最终类,该类不能被继承;
多态是指同一个行为具有多个不同表现形式或形态的能力,它可以提高程序的抽象程度和简洁性,最大程度降低了类和程序模块间的耦合性,并提高程序的可扩展性和可维护性。
在Java中,所有的类继承自Object类。在Object类中,equals()可用来比较两个的变量是否指向同一个对象;getClass()可用来查询对象属于哪个类;toString()方法可将对象的内容转换成字符串并返回。
6.8 理论习题与实践练习
1.填空题
1.1 在Java中,可以使用关键字 实现类的继承。
1.2 如果在子类中需要访问父类的被成员变量,可以通过 关键字来实现。
1.3 一个类不能被其他类继承,可以在class前加______________关键字。
2.选择题
2.1 继承机制解决的如下哪些问题( )
A.封装 B.重写 C.循环 D.重用
2.2 多态使用过程中包含( )
A.向上转型 B.向左转型
C.向右转型 D.向下转型
2.3 下面哪个了是Java中类的祖先类( )
A.Object B.String C.Integer D.Random
2.4 实现类继承时不能用到的关键字是( )
A.extends B.public
C.final D.void
2.5 现有两个类A、B,以下描述中表示B继承自A的是( )
A.class A extends B B.class B implements A
C.class A implements B D.class B extends A
3.思考题
3.1 请简述继承机制?
3.2 请简述final关键字的使用?
3.3 请简述什么是多态机制?
3.4 请简述Object类的常用方法?
4.编程题
4.1在现实生活中,我们常用的打印机有黑白打印机和彩色打印机,它们之间有很多相似的地方,能够找到继承关系。现在要求使用继承来实现这个生活案例。实现思路参考如下:
编写父类Printer(打印机),定义一个方法print()。
编写子类BlackPrinter(黑白打印机),重写print()方法。
编写子类ColorPrinter(彩色打印机),重写print()方法。
- 点赞
- 收藏
- 关注作者
评论(0)