版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tonny_guan/article/details/82769092
《Java从小白到大牛》纸质版已经上架了!!!
类的继承性是面向对象语言的基本特性,多态性前提是继承性。Java支持继承性和多态性。这一章讨论Java继承性和多态性。
为了了解继承性,先看这样一个场景:一位面向对象的程序员小赵,在编程过程中需要描述和处理个人信息,于是定义了类Person,如下所示:
//Person.java文件 package com.a51work6; import java.util.Date; public class Person { // 名字 private String name; // 年龄 private int age; // 出生日期 private Date birthDate; public String getInfo() { return "Person [name=" + name + ", age=" + age + ", birthDate=" + birthDate + "]"; } }
一周以后,小赵又遇到了新的需求,需要描述和处理学生信息,于是他又定义了一个新的类Student,如下所示:
//Student.java文件 package com.a51work6; import java.util.Date; public class Student { // 所在学校 public String school; // 名字 private String name; // 年龄 private int age; // 出生日期 private Date birthDate; public String getInfo() { return "Person [name=" + name + ", age=" + age + ", birthDate=" + birthDate + "]"; } }
很多人会认为小赵的做法能够理解并相信这是可行的,但问题在于Student和Person两个类的结构太接近了,后者只比前者多了一个属性school,却要重复定义其他所有的内容,实在让人“不甘心”。Java提供了解决类似问题的机制,那就是类的继承,代码如下所示:
//Student.java文件 package com.a51work6; import java.util.Date; public class Student extends Person { // 所在学校 private String school; }
Student类继承了Person类中的所有成员变量和方法,从上述代码可以见继承使用的关键字是extends,extends后面的Person是父类。
如果在类的声明中没有使用extends关键字指明其父类,则默认父类为Object类,java.lang.Object类是Java的根类,所有Java类包括数组都直接或间接继承了Object类,在Object类中定义了一些有关面向对象机制的基本方法,如equals()、toString()和finalize()等方法。
提示 一般情况下,一个子类只能继承一个父类,这称为“单继承”,但有的情况下一个子类可以有多个不同的父类,这称为“多重继承”。在Java中,类的继承只能是单继承,而多重继承可以通过实现多个接口实现。也就是说,在Java中,一个类只能继承一个父类,但是可以实现多个接口。
提示 面向对象分析与设计(OOAD)时,会用到UML图,其中类图非常重要,用来描述系统静态结构。Student继承Person的类图如图12-1所示。类图中的各个元素说明如图12-2所示,类用矩形表示,一般分为上、中、下三个部分,上部分是类名,中部分是成员变量,下部分是成员方法。实线+空心箭头表示继承关系,箭头指向父类,箭头末端是子类。UML类图中还有很多关系,如图12-3所示,如图虚线+空心箭头表示实线关系,箭头指向接口, 箭头末端是实线类。
当子类实例化时,不仅需要初始化子类成员变量,也需要初始化父类成员变量,初始化父类成员变量需要调用父类构造方法,子类使用super关键字调用父类构造方法。
下面看一个示例,现有父类Person和子类Student,它们类图如图12-4所示。
父类Person代码如下:
//Person.java文件 package com.a51work6; import java.util.Date; public class Person { // 名字 private String name; // 年龄 private int age; // 出生日期 private Date birthDate; // 三个参数构造方法 public Person(String name, int age, Date d) { this.name = name; this.age = age; birthDate = d; } public Person(String name, int age) { // 调用三个参数构造方法 this(name, age, new Date()); } ... }
子类Student代码如下:
//Student.java文件 package com.a51work6; import java.util.Date; public class Student extends Person { // 所在学校 private String school; public Student(String name, int age, Date d, String school) { super(name, age, d); ① this.school = school; } public Student(String name, int age, String school) { // this.school = school;//编译错误 super(name, age); ② this.school = school; } public Student(String name, String school) { // 编译错误 ③ // super(name, 30); this.school = school; } }
在Student子类代码第①行和第②行是调用父类构造方法,代码第①行super(name, age, d)语句是调用父类的Person(String name, int age, Date d)构造方法,代码第②行super(name, age)语句是调用父类的Person(String name, int age)构造方法。
提示 super语句必须位于子类构造方法的第一行。
代码第③行构造方法由于没有super语句,编译器会试图调用父类默认构造方法(无参数构造方法),但是父类Person并没有默认构造方法,因此会发生编译错误。解决这个编译错误有三种办法:
子类继承父类后,有子类中有可能声明了与父类一样的成员变量或方法,那么会出现什么情况呢?
子类成员变量与父类一样,会屏蔽父类中的成员变量,称为“成员变量隐藏”。示例代码如下:
//ParentClass.java文件 package com.a51work6; class ParentClass { // x成员变量 int x = 10; ① } class SubClass extends ParentClass { // 屏蔽父类x成员变量 int x = 20; ② public void print() { // 访问子类对象x成员变量 System.out.println("x = " + x); ③ // 访问父类x成员变量 System.out.println("super.x = " + super.x); ④ } }
调用代码如下:
//HelloWorld.java文件 package com.a51work6; public class HelloWorld { public static void main(String[] args) { //实例化子类SubClass SubClass pObj = new SubClass(); //调用子类print方法 pObj.print(); } }
运行结果如下:
x = 20 super.x = 10
上述代码第①行是在ParentClass类声明x成员变量,那么在它的子类SubClass代码第②行也声明了x成员变量,它会屏蔽父类中的x成员变量。那么代码第③行的x是子类中的x成员变量。如果要调用父类中的x成员变量,则需要super关键字,见代码第④行的super.x。
如果子类方法完全与父类方法相同,即:相同的方法名、相同的参数列表和相同的返回值,只是方法体不同,这称为子类覆盖(Override)父类方法。
示例代码如下:
//ParentClass.java文件 package com.a51work6; class ParentClass { // x成员变量 int x; protected void setValue() { ① x = 10; } } class SubClass extends ParentClass { // 屏蔽父类x成员变量 int x; @Override public void setValue() { // 覆盖父类方法 ② // 访问子类对象x成员变量 x = 20; // 调用父类setValue()方法 super.setValue(); } public void print() { // 访问子类对象x成员变量 System.out.println("x = " + x); // 访问父类x成员变量 System.out.println("super.x = " + super.x); } }
调用代码如下:
//HelloWorld.java文件 package com.a51work6; public class HelloWorld { public static void main(String[] args) { //实例化子类SubClass SubClass pObj = new SubClass(); //调用setValue方法 pObj.setValue(); //调用子类print方法 pObj.print(); } }
运行结果如下:
x = 20 super.x = 10
上述代码第①行是在ParentClass类声明setValue方法,那么在它的子类SubClass代码第②行覆盖父类中的setValue方法,在声明方法时添加@Override注解,@Override注解不是方法覆盖必须的,它只是锦上添花,但添加@Override注解有两个好处:
1. 提高程序的可读性。
2. 编译器检查@Override注解的方法在父类中是否存在,如果不存在则报错。
注意 方法重写时应遵循的原则:
在面向对象程序设计中多态是一个非常重要的特性,理解多态有利于进行面向对象的分析与设计。
发生多态要有三个前提条件:
下面通过一个示例理解什么多态。如图12-5所示,父类Figure(几何图形)类有一个onDraw(绘图)方法,Figure(几何图形)它有两个子类Ellipse(椭圆形)和Triangle(三角形),Ellipse和Triangle覆盖onDraw方法。Ellipse和Triangle都有onDraw方法,但具体实现的方式不同。
具体代码如下:
//Figure.java文件 package com.a51work6; public class Figure { //绘制几何图形方法 public void onDraw() { System.out.println("绘制Figure..."); } } //Ellipse.java文件 package com.a51work6; //几何图形椭圆形 public class Ellipse extends Figure { //绘制几何图形方法 @Override public void onDraw() { System.out.println("绘制椭圆形..."); } } //Triangle.java文件 package com.a51work6; //几何图形三角形 public class Triangle extends Figure { // 绘制几何图形方法 @Override public void onDraw() { System.out.println("绘制三角形..."); } }
调用代码如下:
//HelloWorld.java文件 package com.a51work6; public class HelloWorld { public static void main(String[] args) { // f1变量是父类类型,指向父类实例 Figure f1 = new Figure(); ① f1.onDraw(); //f2变量是父类类型,指向子类实例,发生多态 Figure f2 = new Triangle(); ② f2.onDraw(); //f3变量是父类类型,指向子类实例,发生多态 Figure f3 = new Ellipse(); ③ f3.onDraw(); //f4变量是子类类型,指向子类实例 Triangle f4 = new Triangle(); ④ f4.onDraw(); } }
上述带代码第②行和第③行是符合多态的三个前提,因此会发生多态。而代码第①行和第④行都不符合,没有发生多态。
运行结果如下:
绘制Figure... 绘制三角形... 绘制椭圆形... 绘制三角形...
从运行结果可知,多态发生时,Java虚拟机运行时根据引用变量指向的实例调用它的方法,而不是根据引用变量的类型调用。
有时候需要在运行时判断一个对象是否属于某个引用类型,这时可以使用instanceof运算符,instanceof运算符语法格式如下:
obj instanceof type
其中obj是一个对象,type是引用类型,如果obj对象是type引用类型实例则返回true,否则false。
为了介绍引用类型检查,先看一个示例,如同12-6所示的类图,展示了继承层次树,Person类是根类,Student是Person的直接子类,Worker是Person的直接子类。
继承层次树中具体实现代码如下:
//Person.java文件
package com.a51work6; public class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } } //Worker.java文件 package com.a51work6; public class Worker extends Person { String factory; public Worker(String name, int age, String factory) { super(name, age); this.factory = factory; } @Override public String toString() { return "Worker [factory=" + factory + ", name=" + name + ", age=" + age + "]"; } } //Student.java文件 package com.a51work6; public class Student extends Person { String school; public Student(String name, int age, String school) { super(name, age); this.school = school; } @Override public String toString() { return "Student [school=" + school + ", name=" + name + ", age=" + age + "]"; } }
调用代码如下:
//HelloWorld.java文件 package com.a51work6; public class HelloWorld { public static void main(String[] args) { Student student1 = new Student("Tom", 18, "清华大学"); ① Student student2 = new Student("Ben", 28, "北京大学"); Student student3 = new Student("Tony", 38, "香港大学"); ② Worker worker1 = new Worker("Tom", 18, "钢厂"); ③ Worker worker2 = new Worker("Ben", 20, "电厂"); ④ Person[] people = { student1, student2, student3, worker1, worker2 }; ⑤ int studentCount = 0; int workerCount = 0; for (Person item : people) { ⑥ if (item instanceof Worker) { ⑦ workerCount++; } else if (item instanceof Student) { ⑧ studentCount++; } } System.out.printf("工人人数:%d,学生人数:%d", workerCount, studentCount); } }
上述代码第①行和第②行创建了3个Student实例,代码第③行和第④行创建了两个Worker实例,然后程序把这5个实例放入people数组中。
代码第⑥行使用for-each遍历people数组集合,当从people数组中取出元素时,元素类型是People类型,但是实例不知道是哪个子类(Student和Worker)实例。代码第⑦行item instanceof Worker表达式是判断数组中的元素是否是Worker实例;类似地,第⑧行item instanceof Student表达式是判断数组中的元素是否是Student实例。
输出结果如下:
工人人数:2,学生人数:3
在5.7节介绍过数值类型相互转换,引用类型可以进行转换,但并不是所有的引用类型都能互相转换,只有属于同一颗继承层次树中的引用类型才可以转换。
在上一节示例上修改HelloWorld.java代码如下:
//HelloWorld.java文件 package com.a51work6; public class HelloWorld { public static void main(String[] args) { Person p1 = new Student("Tom", 18, "清华大学"); Person p2 = new Worker("Tom", 18, "钢厂"); Person p3 = new Person("Tom", 28); Student p4 = new Student("Ben", 40, "清华大学"); Worker p5 = new Worker("Tony", 28, "钢厂"); … } }
上述代码创建了3个实例p1、p2、p3、p4和p5,它们的类型都是Person继承层次树中的引用类型,p1和p4是Student实例,p2和p5是Worker实例,p3是Person实例。首先,对象类型转换一定发生在继承的前提下,p1和p2都声明为Person类型,而实例是由Person子类型实例化的。
表12-1归纳了p1、p2、p3、p4和p5这5个实例与Worker、Student和Person这3种类型之间的转换关系。
表 12-1 类型转换
对 象 | Person类型 | Worker类型 | Student类型 | 说明 |
---|---|---|---|---|
p1 | 支持 | 不支持 | 支持(向下转型) | 类型:Person实例:Student |
p2 | 支持 | 支持(向下转型) | 不支持 | 类型:Person实例:Worker |
p3 | 支持 | 不支持 | 不支持 | 类型:Person实例:Person |
p4 | 支持(向上转型) | 不支持 | 支持 | 类型:Student实例:Student |
p5 | 支持(向上转型) | 支持 | 不支持 | 类型:Worker实例:Worker |
作为这段程序的编写者是知道p1本质上是Student实例,但是表面上看是Person类型,编译器也无法推断p1的实例是Person、Student还是Worker。此时可以使用instanceof操作符来判断它是哪一类的实例。
引用类型转换也是通过小括号运算符实现,类型转换有两个方向:将父类引用类型变量转换为子类类型,这种转换称为向下转型(downcast);将子类引用类型变量转换为父类类型,这种转换称为向上转型(upcast)。向下转型需要强制转换,而向上转型是自动的。
下面通过示例详细说明一下向下转型和向上转型,在HelloWorld.java的main方法中添加如下代码:
// 向上转型 Person p = (Person) p4; ① // 向下转型 Student p11 = (Student) p1; ② Worker p12 = (Worker) p2; ③ // Student p111 = (Student) p2; //运行时异常 ④ if (p2 instanceof Student) { Student p111 = (Student) p2; } // Worker p121 = (Worker) p1; //运行时异常 ⑤ if (p1 instanceof Worker) { Worker p121 = (Worker) p1; } // Student p131 = (Student) p3; //运行时异常 ⑥ if (p3 instanceof Student) { Student p131 = (Student) p3; }
上述代码第①行将p4对象转换为Person类型,p4本质上是Student实例,这是向上转型,这种转换是自动的,其实不需要小括号(Person)进行强制类型转换。
代码第②行和第③行是向下类型转换,它们的转型都能成功。而代码第④、⑤、⑥行都会发生运行时异常ClassCastException,如果不能确定实例是哪一种类型,可以在转型之前使用instanceof运算符判断一下。
在前面的学习过程中,为了声明常量使用过final关键字,在Java中final关键字的作用还有很多,final关键字能修饰变量、方法和类。下面详细说明。
final修饰的变量即成为常量,只能赋值一次,但是final所修饰局部变量和成员变量有所不同。
final修饰变量示例代码如下:
//FinalDemo.java文件 package com.a51work6; class FinalDemo { void doSomething() { // 没有在声明的同时赋值 final int e; ① // 只能赋值一次 e = 100; ② System.out.print(e); // 声明的同时赋值 final int f = 200; ③ } //实例常量 final int a = 5; // 直接赋值 ④ final int b; // 空白final变量 ⑤ //静态常量 final static int c = 12;// 直接赋值 ⑥ final static int d; // 空白final变量 ⑦ // 静态代码块 static { // 初始化静态变量 d = 32; ⑧ } // 构造方法 FinalDemo() { // 初始化实例变量 b = 3; ⑨ // 第二次赋值,会发生编译错误 // b = 4; ⑩ } }
上述代码第①行和第③行是声明局部常量,其中第①行只是声明没有赋值,但必须在使用之前赋值(见代码第②行),其实局部常量最好在声明的同时初始化。
代码第④、⑤、⑥和⑦行都声明成员常量。代码第④和⑤行是实例常量,如果是空白final变量(见代码第⑤行),则需要在构造方法中初始化(见代码第⑨行)。代码第⑥和⑦行是静态常量,如果是空白final变量(见代码第⑦行),则需要在静态代码块中初始化(见代码第⑧行)。
另外,无论是那种常量只能赋值一次,见代码第⑩行为b常量赋值,因为之前b已经赋值过一次,因此这里会发生编译错误。
final修饰的类不能被继承。有时出于设计安全的目的,不想让自己编写的类被别人继承,这是可以使用final关键字修饰父类。
示例代码如下:
//SuperClass.java文件 package com.a51work6; final class SuperClass { } class SubClass extends SuperClass { //编译错误 }
在声明SubClass类时会发生编译错误。
final修饰的方法不能被子类覆盖。有时也是出于设计安全的目的,父类中的方法不想被别人覆盖,这是可以使用final关键字修饰父类中方法。
示例代码如下:
//SuperClass.java文件 package com.a51work6; class SuperClass { final void doSomething() { System.out.println("in SuperClass.doSomething()"); } } class SubClass extends SuperClass { @Override void doSomething() { //编译错误 System.out.println("in SubClass.doSomething()"); } }
子类中的void doSomething()方法试图覆盖父类中void doSomething()方法,父类中的void doSomething()方法是final的,因此会发生编译错误。
通过对本章的学习,首先介绍了Java中的继承概念,在继承时会发生方法的覆盖、变量的隐藏。然后介绍了Java中的多态概念,广大读者需要熟悉多态发生的条件,掌握引用类型检查和类型转换。最后还介绍了final关键字。
https://edu.csdn.net/combo/detail/594
http://www.zhijieketang.com/group/5
进入百度阅读电子书
进入图灵社区电子书
UML是Unified Modeling Language的缩写,既统一标准建模语言。它集成了各种优秀的建模方法学发展而来的。UML图常用的有例图、协作图、活动图、序列图、部署图、构件图、类图、状态图。