第四章 类的深入解析
4.1 掌握类继承的方法
在面向对象程序设计中,继承表示两个类之间的一种关系,是一种由已有类创建新类的机制。子类不仅可以从父类中继承成员变量和方法,还可以重新定义它们以及扩充新的内容。
在Java中,子类对父类的继承是在类的声明中使用extends关键字来指明的。其一般格式为:
[类的修饰符]class <子类名> extends <父类名> {
…//类体的内容
}
一、成员变量的继承与隐藏
使用继承方法创建新类时,新定义的子类可以从父类继承所有非私有的成员变量和方法作为自己的成员。
实例4-1 成员变量的继承与隐藏示例
【实例描述】
通过创建父类和子类,并在其中声明若干同名和不同名成员变量演示成员变量继承和隐藏的特点。
【技术要点】
基于父类创建子类时,子类可以继承父类的成员变量和成员方法。但是,如果在父类和子类中同时声明了一个同名变量,则这两个变量在程序运行时同时存在。也就是说,子类在使用父类的同名变量时,父类中的同名变量只是被隐藏了。
// VarInherit.java
package Chapter4;
class Person {
String name; // 声明两个成员变量
int age;
// 有参构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 无参构造方法
public Person() {
this.name = "person name";
this.age = 23;
}
// 成员方法,此时显示的是父类中成员变量的结果
void pprint() {
System.out.println("class:Person; " + "Name: " + this.name + "; age: "
+ this.age);
}
}
// 基于Person类定义Student子类
class Student extends Person {
String name; // 在派生类中声明自己的成员变量
int classno; // 声明新成员变量
// 无参构造方法
public Student() {
this.name = "student name";
this.age = 20;
}
// 有参构造方法
public Student(String name, int age, int classno) {
this.name = name;
this.age = age;
this.classno = classno;
}
// 成员方法,此时显示的是子类中成员变量的结果
void sprint() {
System.out.println("class:Student; " + "Name: " + this.name + "; age: "
+ this.age + "; classno: " + this.classno);
}
}
//声明公共类
public class VarInherit {
public static void main(String[] args) {
// 调用无参构造方法创建对象
Student obj1 = new Student();
// 调用有参构造方法创建对象
Student obj2 = new Student("LiXiao", 18, 1);
obj1.pprint(); // 调用父类的成员方法
obj1.sprint(); // 调用子类的成员方法
obj2.pprint(); // 调用父类的成员方法
obj2.sprint(); // 调用子类的成员方法
}
}
程序的运行结果:
class:Person; Name: person name; age: 20
class:Student; Name: student name; age: 20; classno: 0
class:Person; Name: person name; age: 18
class:Student; Name: LiXiao; age: 18; classno: 1
解释说明:
(1)由于Student子类中未定义成员变量age,因此,该成员变量源自其父类,这说明了子类可以继承父类的成员变量。
(2)name变量同时在父类和子类中进行了声明。当我们通过Student obj1 = new Student();语句创建对象obj1时,系统会首先调用父类的无参构造方法,然后再调用子类的无参构造方法,故父类中的name被赋值person name,子类中的name被赋值student name,而公共成员变量age被赋值20。另外,由于子类成员变量classno未被显式赋值,故输出系统自动为其初始化的默认值0。
二、方法的继承与覆盖
子类可以继承父类中所有可以被子类访问的成员方法,但是如果子类重新定义了从父类继承来的方法,此时父类的这个方法在子类中将不复存在,此时称为子类方法覆盖了父类的方法,简称方法覆盖(override)。
实例4-2 方法的继承与覆盖示例
【实例描述】
通过创建父类和子类,并在其中声明若干同名和不同名方法演示方法继承和覆盖的特点。
【技术要点】
基于父类创建子类时,子类可以继承父类的成员方法。但是,如果在父类和子类中同时定义了一个同名、同类型、同参数的方法,则程序运行时父类中的同名方法将被子类中的同名方法覆盖。
// MethodInherit.java
package Chapter4;
// 声明父类
class parentclass {
// 声明成员方法
void pprint() {
this.print();
this.print1(0);
}
// 声明同类型、同名、同参数成员方法
void print() {
System.out.println("父类:同类型、同名、同参数成员方法!");
}
// 声明同类型、同名但参数不同的成员方法
void print1(int a) {
System.out.println("父类:同类型、同名但参数不同的成员方法!");
}
}
// 基于parentclass类定义subclass子类
class subclass extends parentclass {
// 声明成员方法
void sprint() {
this.print();
this.print1();
}
// 声明同类型、同名、同参数成员方法
void print() {
System.out.println("子类:同类型、同名、同参数成员方法!");
}
// 声明同类型、同名但参数不同的成员方法
void print1() {
System.out.println("子类:同类型、同名但参数不同的成员方法!");
}
}
// 声明公共类
public class MethodInherit {
public static void main(String[] args) {
subclass obj = new subclass();
obj.pprint(); // 调用父类的成员方法
obj.sprint(); // 调用子类的成员方法
}
}
解释说明:
(1)使用子类创建对象时,可以直接引用父类中的方法,如利用obj.pprint();语句调用父类中的pprint成员方法。这体现了方法的继承性。
(2)要使子类中的方法完全覆盖父类中的方法,方法的类型、名称和参数必须完全相同(如两个print方法),否则,任何一项不同均不能覆盖(如两个print1方法)。
三、构造方法的继承
当我们基于子类创建一个对象时,系统会首先调用父类的无参构造方法,然后才会执行子类的构造方法。这体现了构造方法的继承性。
实例4-3 构造方法的继承示例
【实例描述】
通过创建父类和子类,并分别为其创建若干构造方法演示构造方法的继承特点。
【技术要点】
基于父类创建子类时,如果我们基于子类创建了一个对象,则程序运行时,系统会首先调用父类的无参构造方法,然后才会执行子类的构造方法。如果希望调用父类的有参构造方法,可使用super关键字。
// ConstructorInherit.java
package Chapter4;
class PersonA {
String name; // 声明两个成员变量
int age;
// 有参构造方法
public PersonA(String name, int age) {
this.name = name;
this.age = age;
}
// 无参构造方法
public PersonA() {
this.name = "person name";
this.age = 23;
}
// 成员方法,此时显示的是父类中成员变量的结果
void pprint() {
System.out.println("class:Person; " + "Name: " + this.name
+ "; age: " + this.age);
}
}
// 基于Person类定义Student子类
class StudentA extends PersonA {
String name; // 在派生类中声明自己的成员变量
int classno; // 声明新成员变量
// 无参构造方法
public StudentA() {
super("xyz", 30); // 调用父类的有参构造方法
this.name = "student name";
this.age = 20;
}
// 有参构造方法
public StudentA(String name, int age, int classno) {
super(name, age); // 调用父类的有参构造方法
this.name = name;
this.age = age;
this.classno = classno;
}
// 成员方法,此时显示的是子类中成员变量的结果
void sprint() {
System.out.println("class:Student; " + "Name: " + this.name
+ "; age: " + this.age + "; classno: " + this.classno);
// 成员方法,利用super命令显示父类成员变量
System.out.println("父类中的name成员变量:" + super.name);
}
}
// 声明公共类
public class ConstructorInherit {
public static void main(String[] args) {
// 调用无参构造方法创建对象
StudentA obj1 = new StudentA();
// 调用有参构造方法创建对象
StudentA obj2 = new StudentA("LiXiao", 18, 1);
obj1.pprint(); // 调用父类的成员方法
obj1.sprint(); // 调用子类的成员方法
obj2.pprint(); // 调用父类的成员方法
obj2.sprint(); // 调用子类的成员方法
}
}
程序的运行结果:
class:Person; Name: xyz; age: 20
class:Student; Name: student name; age: 20; classno: 0
父类中的name成员变量:xyz
class:Person; Name: LiXiao; age: 18
class:Student; Name: LiXiao; age: 18; classno: 1
父类中的name成员变量:LiXiao
构造方法的继承遵循以下原则:
子类无条件地继承父类的无参数的构造方法。
如果子类没有定义构造方法,则它将继承父类无参数的构造方法作为自己的构造方法;如果子类定义了构造方法,则 在创建子类对象时,将先执行来自继承父类的无参数的构造方法,然后再执行自己 的构造方法。
对于父类带参数的构造方法,子类可以通过在自己的构造方法中使用super关键字来调用它,但这个调用语句必须是子 类构造方法中的第一条可执行语句。
四、使用类继承时子类对象和父类对象的特点
使用类继承时,使用子类创建的对象可以赋值给父类对象。此时父类对象虽然名义上调用的仍是父类方法或成员变量,但实际上已变成了子类的方法和成员变量。
// ClassInherit.java
package Chapter4;
// 创建父类Pclass
class Pclass {
void Draw() {
System.out.println("Pclass类,Draw方法!");
}
}
// 创建子类Sclass
class Sclass extends Pclass {
void Draw() {
System.out.println("Sclass类,Draw方法!");
}
void NewDraw() {
System.out.println("Sclass类,NewDraw方法!");
}
}
// 创建公共类
public class ClassInherit {
public static void main(String[] args) {
Pclass obj1 = new Pclass(); // 创建Pclass类对象obj1
Sclass obj2 = new Sclass(); // 创建Sclass类对象obj2
obj1.Draw(); // 调用Obj1的Draw方法
obj2.Draw(); // 调用obj2的Draw方法
obj2.NewDraw(); // 调用obj2的NewDraw方法
obj1 = obj2; // 将子类对象赋值给父类对象
// 调用转换后obj1对象的Draw方法(实际上已是子类对象的Draw方法)
obj1.Draw();
// 但是,即使转换了对象类型,下述语句依然非法,obj1对象
// 只能调用父类的方法
// obj1.NewDraw();
}
}
4.2 掌握类的多态性的使用方法
一、多态性的概念
多态性的实现有两种方式。
(1)方法覆盖实现多态性
此时通过子类对继承父类的方法进行重定义来实现。
(2)方法重载实现多态性
通过定义多个同名的不同方法来实现,系统会根据参数(类型、个数、顺序)的不同来区分不同方法。
二、通过方法覆盖实现多态性
如果我们基于某个父类创建了多个子类,而在子类中分别重定义了父类的某些方法,那么当我们分别基于父类和子类创建多个对象时,就可以利用这些对象分别调用不同的同名方法。
// Shapes.java
package Chapter4;
import java.util.*;
// 创建一个Shape基类
class Shape {
public void draw() {
}
public void erase() {
}
}
// 基于Shape类创建Circle子类
class Circle extends Shape {
public void draw() {
System.out.println("Circle.draw()");
}
public void erase() {
System.out.println("Circle.erase()");
}
}
//基于Shape类创建Square子类
class Square extends Shape {
public void draw() {
System.out.println("Square.draw()");
}
public void erase() {
System.out.println("Square.erase()");
}
}
//基于Shape类创建Triangle子类
class Triangle extends Shape {
public void draw() {
System.out.println("Triangle.draw()");
}
public void erase() {
System.out.println("Triangle.erase()");
}
}
// 创建随机形状发生器类RandomShapeGenerator
class RandomShapeGenerator {
// 创建Random类型rand随机数发生器,其种子为长整型数47
private Random rand = new Random(47);
// 创建Shape类型方法next
public Shape next() {
// rand.nextInt(3)用来产生0——3之间的随机数
switch (rand.nextInt(3)) {
default:
case 0:
return new Circle(); // 返回Circle对象
case 1:
return new Square(); // 返回Square对象
case 2:
return new Triangle(); // 返回Triangle对象
}
}
}
// 定义公共类
public class Shapes {
// 基于RandomShapeGenerator类创建私有静态对象gen
private static RandomShapeGenerator gen = new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] s = new Shape[9]; // 基于Shape类创建对象数组s
// 为对象数组s赋值,其内容应为Circle、Square或Triangle子类对象
for (int i = 0; i < s.length; i++)
s[i] = gen.next();
// 遍历数组s,将其值赋给临时Shape对象shp
for (Shape shp : s)
// 调用shp对象的draw方法。由于shp对象内容分别为Circle、
// Square或Triangle类型,故draw方法也分别隶属于这三种类型
shp.draw();
}
}
解释说明:
基于父类和子类创建对象时,可以将子类对象赋给父类对象,反之则不能。不过,此时父类对象依然只能调用其自身的成员方法(被子类覆盖的方法除外,由于此时只存在子类的覆盖方法,因此,此时父类对象调用的实际是子类对象的覆盖方法),以及访问自身的成员变量。
三、通过重载方法实现多态性
在定义多个重载方法时,除了使它们的方法名相同外,必须采用不同形式的参数,包括参数的个数不同、类型不同或顺序不同。如此一来,调用重载方法时,系统会根据参数的个数、类型或顺序来确定调用哪一个方法。
// MethodOverloaded.java
package Chapter4;
public class MethodOverloaded {
// 两个整数相加,返回整数
int add(int x1, int x2) {
return x1 + x2;
}
// 两个浮点数相加,返回浮点数
double add(double x1, double x2) {
return x1 + x2;
}
public static void main(String[] args) {
// 创建MethodOverloaded类对象obj
MethodOverloaded obj = new MethodOverloaded();
// 分别调用不同的重载方法
System.out.println("int add= " + obj.add(10, 23));
System.out.println("double add= " + obj.add(10.0, 23));
}
}
运行结果:int add= 33
double add= 33.0
4.3 了解抽象类的使用方法
抽象类即为类的抽象,是对相似类的归纳与总结。因此,抽象类中通常只包括了抽象方法(只包含方法声明,而不包含方法体),而方法的具体实现则由其派生出的各子类来完成,这使得程序的功能描述和功能实现得以分离。
(1)抽象类的定义格式通常如下:
public abstract class PlaneGraphcs1 { // 声明抽象类
private String shape; // 声明成员变量
……
public abstract double area(); // 声明抽象方法,并且分号必不可少
……
}
(2)抽象类是不能实例化的,也就是说,不能基于抽象类来创建对象。
(3)抽象类也可以包含普通成员变量和成员方法。但是,抽象方法只能出现在抽象类中。也就是说,非抽象类是不能包含抽象方法的。
抽象类的示例请参考实例4-5。
4.4 了解接口的定义
和抽象类的作用类似,接口也可以把“做什么”和“怎么做”,即程序的功能和实现分离开来。
在Java中,一个类只能直接继承一个父类,但可以同时实现若干个接口。因此,利用接口实际上就获得了多个特殊父类的属性,即实现了多继承。
从某种意义上讲,接口应该属于更严格的抽象类。
接口中只能定义抽象方法,而抽象类中可以定义非抽
象方法。
接口中只能定义public static final类型常量,而不能包
含成员变量。抽象类则不然,它可以包含各种形式的
成员变量和常量。
类可以继承(实现)多个接口,但只能继承一个抽象
父类。
一、接口的定义
接口的定义包括两个部分:接口声明和接口体。声明接口的一般格式如下:
[public] interface 接口名 [extends 父接口名列表]{
//常量声明
//抽象方法声明
}
接口声明中有两个部分是必需的:interface关键字和接口的名字。用public修饰的接口是公共接口,可以被所有的类和接口使用;没有public修饰的接口则只能被同一个包中的其他类和接口使用。
接口也具有继承性,子接口可以继承一个或多个父接口的所有属性和方法。继承多个父接口时,各父接口名以逗号分隔。
二、接口的实现
为了声明一个类来实现一个接口,在类的声明中要包括一条implements语句。此外,由于Java支持接口的多继承,因此可以在implements后面列出要实现的多个接口,这些接口名称之间应以逗号分隔。
class Stock implements StockWatcher, FundWatcher {
public void valueChanged(String tickerSymbol, double newValue) {
if(tickerSymbol.equals(oracleTicker)){…}
else if(tickerSymbol.equals(ciscoTicker)){ …}
}
}
注意:由于Stock类实现了StockWatcher接口,因此它应该提供valueChanged()方法的实现。当一个类实现一个接口中的抽象方法时,这个方法的名字和参数类型、数量和顺序必须与接口中的方法相匹配。 接口的示例请参考实例4-6。
综合实例——学生管理系统
// StudentManager.java
package Chapter4;
abstract class Studentx { // 抽象类
public String id; // 学生学号
public String name; // 学生姓名
public String className; // 班级
public abstract void logIn(); // 注册方法
public abstract void clearOut();// 注销方法
}
// 本科生类
class UnderGraduate extends Studentx {
private String counsellors; // 辅导员
public void logIn() { // 本科生注册
// 注册过程.....
System.out.println("本科生注册,注册成功!");
}
public void clearOut() { // 本科生注销
// 注销过程.....
System.out.println("本科生注销,注销成功!");
}
}
// 研究生类
class Graduate extends Studentx {
private String instrutor; // 导师
private String research; // 研究方向
public void logIn() { // 研究生注册
//注册过程......
System.out.println("研究生注册,注册成功!");
}
public void clearOut() { // 研究生注销
//注销过程....
System.out.println("研究生注销,注销成功!");
}
}
// 学生管理公共类StudentManager
public class StudentManager {
public void add(Studentx s) { // 学生注册
s.logIn();
}
public void delete(Studentx s) { // 学生注销
s.clearOut();
}
public static void main(String[] args) {
StudentManager manager = new StudentManager();
Studentx underGraduate = new UnderGraduate();
Studentx graduate = new Graduate();
manager.add(underGraduate); //本科生注册
manager.delete(underGraduate); //本科生注销
manager.add(graduate); //研究生注册
manager.delete(graduate); //研究生注销
}
}
本章小结
本章进一步介绍了Java语言中面向对象编程的相关知识,主要包括类的继承和多态的实现方式,以及抽象类和接口的定义与使用方法。大家应着重掌握如下一些内容:
在使用子类创建对象时,可以利用子类对象名直接引用父类中的成员变量和成员方法,这被称为成员变量和方法的继承;
使用子类创建对象时,父类的无参构造方法总是优先被执行,这被称为构造方法的继承。当然,我们也可以利用super关键字显式调用父类的其他构造方法;
多态性被称为“一个对外接口,多个内在实现方法”,它可以通过方法覆盖和重载方法来实现。
抽象类除了可以包含抽象方法外,其他性质与普通类完全相同。抽象类自身不能实例化,抽象方法的实现应通过其派生子类来完成;
接口是更严格的抽象类,其中只能包含public static final成员常量和抽象方法;
一个子类只能继承一个抽象类,但可以继承多个接口,这实现了程序设计中的多继承关系。