第5 章 进一步讨论对象和类
5.1 详细说明类是如何定义的,解释类的特性及它的几个要素。
解:Java程序设计就是定义类的过程,Java程序中的所有代码都包含在类中。类可
以看作是数据的集合及操作这些数据所需的方法的整合。
Java中的类分两种,一种是系统预定义的类,这些类组成Java类库。Java类库是一
组由软件供应商编写好的程序模块,完成常用的基本功能和任务,可由程序编写人员直接
调用。正是因为有了这些类库,程序员才有很好的辅助工具,不必将精力浪费在一些简单
常见的功能实现上。基本类库提供的这些功能,使得程序员站在了一个较高的起点上,可
以把主要精力关注在更加复杂的工作上。这些定义好的类根据实现功能的不同,划分成
不同的集合,每个集合称为一个包。Sun公司提供的JDK中共有43个大包。
除去系统预定义的类之外,还有一种是用户程序自己定义的类,当然这其中又包括其
他程序员定义的类和自己定义的类。这些类都显式或隐式地派生于Java中某个预定义
的类。不论是预定义的类,还是程序员自己定义的类,每个类中一般都包含属性和方法。
属性即是数据,属性值表明一个对象的状态;方法决定类有哪些可利用的手段,即可通过
哪些函数来操作这些数据。
类的具体格式如下: 
修饰符class 类名[extends 父类名]{ 
修饰符类型成员变量1; 
修饰符类型成员变量2; 
…… 
修饰符类型成员方法1(参数列表){ 
类型局部变量; 
方法体 
} 
修饰符类型成员方法2(参数列表){ 
类型局部变量; 
方法体 
} 
…… 
}
类定义的第一行是类头,class关键字表明这里定义的是一个类。class前的修饰符
允许有多个,用来限定所定义的类的使用方式。
类名是用户为该类所起的名字,它应该是一个合法的标识符,并尽量遵从命名约定。
extends是关键字。如果所定义的类是从某一个父类派生而来,那么,父类的名字应
写在extends之后。如果不写的话,则隐式表明继承自Object类。Java不允许多重继
承,因此如果有父类的话,只能有一个父类。
类头后面的部分称为类体,类体用一对大括号括起来,含有两部分,一部分是数据
·83·

成员变量,另一部分是成员方法。数据成员变量可以含有多个,这是类的静态属性,表
明类的实例目前所处的状态。类的不同实例对应各自不同的属性值,因此有些属性值
可用来标识不同的实例。成员变量前面的类型是该变量的类型。成员方法也可以有
多个,其前面的类型是方法返回值的类型。方法对应类的行为和操作。方法体是要执
行的真正语句。在方法体中还可以定义该方法内使用的局部变量,这些变量只在该方
法内有效。
类可以是public的,表明任何对象都可以使用或扩展这个类;也可以是friendly的, 
表明它可以被同一个包中的对象使用。类还可以是final的,表明它不可以再有子类,与
之相对的,使用abstract修饰的类必须要有子类。
5.2 给出3个类的定义: 
class ParentClass {} 
class SubClass1 extends ParentClass {} 
class SubClass2 extends ParentClass {} 
并分别定义3个对象: 
ParentClass a=new ParentClass (); 
SubClass1 b=new SubClass1(); 
SubClass2 c=new SubClass2(); 
若执行下面的语句: 
a=b; 
b=a; 
b=(SubClass1)c; 
会有什么结果? 分别从下面的选项中选择正确的答案。
(1)编译时出错。
(2)编译时正确,但执行时出错。
(3)执行时完全正确。
解:3行类定义分别定义了3个类,一个父类ParentClass及它的两个子类SubClass1和
SubClass2。后面的3行则分别为每个类说明了一个实例,其中,a是ParentClass类的实
例,b是SubClass1类的实例,c是SubClass2类的实例。因为SubClass1和SubClass2都
是派生于ParentClass的子类,所以b和c也同时是ParentClass类的实例。
Java中允许使用对象之父类类型的一个变量指示该对象,称为转换对象(casting)。
关于转换对象的使用,遵从对象引用的赋值兼容原则。所谓对象引用的赋值兼容原则是
指允许把子类的实例赋给父类的引用,但不允许把父类的实例赋给子类的引用。实际编
程时,可以使用instanceof运算符来判明一个引用指向的是哪个类的实例。如果父类的
引用指向的是子类实例,就可以转换该引用,恢复对象的全部功能。
本题中,可以进行下面的测试: 
boolean tagb1=b instanceof ParentClass; 
boolean tagc1=c instanceof ParentClass; 
tagb1和tagc1的值都是true,表明b和c是子类实例的同时,也是父类的实例。反过来, 
·84·

父类的实例不是子类的实例,例如下面的测试: 
boolean taga2=a instanceof SubClass1; 
boolean taga3=a instanceof SubClass2; 
taga2和taga3的值都是false。b和c是不同子类的实例,因此如下测试: 
boolean tagb3=b instanceof SubClass2; 
boolean tagc2=c instanceof SubClass1; 
将出现编译错误。下面针对题目中的3条语句分别进行测试。
(1)执行a=b;时,a指向父类的实例,b指向子类的实例。由于是将子类实例赋给父
类实例,因此编译及执行都正确。该语句执行子类中的方法,如果子类中没有重写父类中
的方法,则执行父类中的方法。例如,下面程序的父类和子类中都定义了value成员和
getValue()方法,将子类的实例赋给父类引用后,此时a的值是子类的实例,再执行语句: 
taga2=a instanceof SubClass1; 
则taga2的值应为true。在给a分配的内存中既包括子类中value的值,也含有父类中
value的值。调用a.getValue()方法时,先在子类中查找这个方法是否存在,如果有,则返
回子类中value的值1;若没有,则查找父类中的同名方法,并返回父类中的值0。
完整的测试代码如下: 
import java.util.*; 
class ParentClass 
{ public ParentClass() //父类的构造方法 
{ value=0; 
} 
public int getValue() //父类的求值方法,返回父类中value 的值 
{ return value; 
} 
public void setValue(int y) //给父类的属性value 赋值 
{ value=y; 
} 
private int value; 
}c
lass SubClass1 extends ParentClass 
{ public SubClass1() //子类1 的构造方法,value 值为1 
{ value=1; 
} 
public int getValue() //子类的同名求值方法,返回子类1 中value 的值 
{ return value; 
} 
public int getClassValue1() //子类的特殊求值方法,返回classvalue1 的值 
{ return classvalue1; 
} 
private int value; 
private int classvalue1=11; 
} 
·85· 
源代码

cl as s Su bC la s s2 e xt en ds P ar en t Cl as s 
{ public SubClass2() //子类2 的构造方法,value 值为2 
{ value=2; 
} 
public int getValue() //子类的同名求值方法,返回子类2 中value 的值 
{ return value; 
} 
public int getClassValue2() //子类的特殊求值方法,返回classvalue2 的值 
{ return classvalue2; 
} 
private int value; 
private int classvalue2=22; 
}
public class Test2 
{ public static void main(String[] args) 
{ ParentClass a=new ParentClass (); //父类实例 
SubClass1 b=new SubClass1(); //子类1 的实例 
SubClass2 c=new SubClass2(); //子类2 的实例 
a=b; //a 指向子类1 的实例 
System.out.println("a="+a.getValue()); //返回子类1 中的属性值 
} 
}
程序的执行结果如下: 
a=1 
(2)执行b=a;时,由于是将父类的实例赋给子类的变量,因此会出现编译错误,错误
类型是变量类型不匹配。
(3)执行b=(SubClass1)c;时,由于b和c是不同类的实例,因此也会出现编译错
误,错误类型是变量类型不能转换。
【拓展思考】
(1)new运算符执行什么动作? 
解:new运算符创建指定类的一个新实例(对象)。然后调用类的构造方法设置新生
成的对象。
(2)null引用的作用是什么? 
解:null引用是不指向任何对象的引用。用保留字null来检查空引用,以避免对空
引用的访问。
(3)什么是别名? 
解:如果两个引用指向同一个对象,则它们互为别名。通过一个引用改变对象的状
态,也就改变了另一个引用指向的对象,因为实际上只有一个对象。仅当对象再没有被引
用指向时,才被垃圾收集所标记。
·86·

5.3 什么是抽象类? 它如何定义? 下面的哪些定义是正确的? 
(1) 
class alarmclock { 
abstract void alarm(); 
}
(2) 
abstract alarmclock { 
abstract void alarm(); 
}
(3) 
class abstract alarmclock { 
abstract void alarm(); 
}
(4) 
abstract class alarmclock { 
abstract void alarm(); 
}
(5) 
abstract class alarmclock { 
abstract void alarm(){ 
System.out.println("alarm!") 
}; 
}
解:如果一个方法只有方法的声明,而没有方法的实现,则称为抽象方法(abstract 
method)。含有抽象方法的类通常称为抽象类(abstractclass)。在Java中可以通过
abstract关键字把一个类定义为抽象类,每一个未被定义具体实现的抽象方法也应标记
为abstract。抽象类是表示一般概念的类。在抽象类中可定义公共特征及方法签名,然
后由其子类来继承它们。
在抽象类中可以包括被它的所有子类共享的公共行为,也包括被它的所有子类共享
的公共属性。因为抽象类中含有抽象方法,所以不能将抽象类作为模板来创建对象,必须
生成抽象类的一个非抽象的子类后才能创建实例。这是因为一个实例的任何方法都必须
已被具体实现了。抽象类可以包含常规类能够包含的任何元素,当然也包括构造方法,因
为子类可能需要继承这种方法。抽象类中当然包含抽象方法,这种方法只有方法的声明, 
而没有方法的实现。这些方法将在抽象类的子类中被具体实现。只有实现了所有抽象方
法的子类才能创建对应的实例。除了抽象方法,抽象类中当然也可以包含非抽象方法,反
之,不能在非抽象的类中声明抽象方法。也就是说,只有抽象类才能具有抽象方法。
根据抽象方法和抽象类的定义和规则,再来分析题目中的5个语句说明。第1个示
例中,alarm()方法被定义为抽象方法,那么方法所在的类也必须被声明为抽象类型;第2 
个示例中,虽然方法和类都被说明是抽象的,但缺少class关键字;第3个示例中,各关键
·87·

字的次序不正确,abstract关键字要放在clas 
之前;第5个示例中,虽然alarm()方法被
说明为抽象方法,但方法体不为空,这显然有矛盾。只有第4个示例的说明才是正确的。

【拓展思考】

父类的所有成员都能被子类继承吗? 请解释理由。
解:如果父类的成员是私有可见的,则不能被子类继承。这意味着在子类中不能通
过名字来直接引用父类的私有成员。但对子类来说,这样的成员确实存在,可以间接
引用。
什么叫方法重载? 什么叫方法重写? 它们之间的区别是什么?
5.4
解:同一个类中,可以定义同名的多个方法。它们的不同之处在于参数列表不同,这
其中包括参数的个数不同或是对应的参数类型不完全相同。这就是方法的重载
(overload)。当对该类实例调用相应的方法时,系统将依据所带参数的个数及类型从同
名的多个方法中来选择参数列表满足要求的方法。不只如此,在不同的类中也可以定义
有相同方法名的方法。

使用类的继承关系,可以从已有的类产生一个新类,在原有特性基础上,增加新的特
性。原类及新类分别称为父类及子类。如果父类中原有的方法不能满足子类的要求,可
以在子类中对父类的方法重新编写代码。这称为方法重写(overide),也称为方法的隐
藏,意思是子类中看不到父类中的实现代码。子类中定义的方法所用的名字、返回类型及
参数表和父类中方法使用的完全一样,从逻辑上看就是子类中的成员方法隐藏了父类中
的同名方法。

从对方法重载和方法重写的分析可以看出,它们之间的区别主要有以下几点。

.方法重载时参数列表必须不同,才能使系统区别出到底调用哪个方法,而方法重
写时参数列表可以相同。
.方法重载时,方法的返回类型可能不同,也可能相同;而方法重写时,子类中方法
的返回类型和父类中同名方法的返回类型完全一样。
.方法重载多出现在同一个类中,方法重写必须是在父子类中
。
【拓展思考
】
(1)什么是多态? 
解:多态是指引用变量在不同时刻指向不同类型对象的一种能力。通过这样的引
用,调用的方法可以在不同的时刻,根据对象引用的类型与不同的方法绑定。

(2)继承如何支持多态? 
解:在Java中,使用父类声明的引用变量可以指向子类的对象。两个类包含有相同
签名的方法时,父类引用是多态的。

(3)与多态相关的重写如何实现? 
解:当子类重写父类方法的定义时,这个方法就有了两个版本。用多态引用调用这
个方法时,调用的方法版本取决于所用对象的类型,而非引用变量的类型。

(4)如何使用接口完成多态? 
解:接口名可作为引用类型使用。这样的引用变量可指向实现该接口的任一类的任
一对象。因为所有的类实现同一个接口,有公共的签名,所以可以动态绑定。

(5)单重继承和多重继承之间的差别是什么? 
·88· 


解:在单重继承中,只能从一个父类派生子类;而在多重继承中,一个类可从多个父
类中派生,并继承每个父类的特性。多重继承的问题是,必须解决当两个或多个父类中有
同名的属性或方法时引起的冲突。Java只支持单重继承。
5.5 什么是null引用? 
解:null引用是不指向任何对象的引用。用保留字null来检查空引用,以避免对空
引用的访问。
在Java中,当执行new 为一个对象分配内存时,Java自动初始化所分配的内存空
间。对于引用,即对象类型的任何变量,使用一个特殊的值null进行初始化,它表示引用
不指向任何对象。运行过程中,一旦系统发现使用了这样一个引用时,可以立即停止进一
步的访问,不会带来任何危险。
5.6 this关键字和super关键字在成员方法中的特殊作用是什么? 
解:在Java中,this引用总是指向当前对象,即this引用所在的对象。如果在类的成
员方法中访问类的成员变量,可以使用this关键字指明要操作的对象。在构造方法中, 
this还有一个用法,即作为构造方法的第一个语句,它的形式是this(参数表),这个构造
方法会调用同一个类的另一个构造方法。
如果子类已经重写了父类中的方法,但在子类中还想使用父类中被隐藏的方法,或者
子类中定义了和父类中同名的成员变量,还想使用父类中隐藏的成员变量,可以使用
super关键字。
5.7 仿照书中的例子,构造一个类,并使其具有多个相互调用的构造方法;然后构
造它的子类,在构造方法中利用super关键字来调用父类的构造方法。
解:在习题2.10中,定义了教师类及其子类。父类及子类中有多个相同的属性,对
这些属性的访问方法也存在于父类及子类中。在习题2.10的实现中,父子类中的构造方
法是独立的,它们之间没有关系。实际上,若父子类中有多个相同的属性,子类的构造方
法就可以借用父类的构造方法,以简化代码。除构造方法外,子类的任何成员方法都可以
借用父类同名的成员方法,并在此基础上,增加自己的特殊处理部分。
修改习题2.10 的实现,包括修改其中定义的类及成员方法。先定义一个
SchoolTeacher类,其中包括3个互相调用的构造方法。在此基础上派生ResearchSchoolTeacher 
子类,其构造方法中使用super来调用父类的构造方法,对父类及子类共有的属性进行赋
值。除了可以在构造方法中使用super调用父类的构造方法外,还可以在一般的方法中
调用父类的方法。例如本题中,当输出ResearchSchoolTeacher类实例的信息时,因前
6个属性是与父类实例相同的属性,所以可以借用父类中的输出方法: 
super.print();//使用父类的输出方法
上述语句调用父类的输出方法,先输出前6个属性的值,然后再使用下面的两条语句: 
System.out.print("The SchoolTeacher research field is: "); 
System.out.println(this.getResField());//特殊属性的输出
完成对该类实例resField属性的输出。
程序代码实现如下: 
·89·

import java.lang.*; 
class Date //定义日期类
{ int day; 
int month; 
int year; 
Date (int day, int month, int year) 
{ this.day=day; 
this.month=month; 
this.year=year; 
} 
Date () 
{ this.day=8; 
this.month=11; 
this.year=2012; 
} 
public int getYear() //返回年 
{ return year; 
} 
public int getMonth() //返回月 
{ return month; 
} 
public int getDay() //返回日 
{ return day; 
} 
public void setDate(Date SpeDate) //设置日期 
{ year=SpeDate.getYear(); 
month=SpeDate.getMonth(); 
day=SpeDate.getDay(); 
} 
}
public class SchoolTeacher //定义教师类,这是基类
{ private String name; //教师名字 
private boolean sex; //性别,true 表示男性,false 表示女性 
private Date birth; //教师出生日期 
private String salaryID; //教师工资号 
private String depart; //教师所属系 
private String posit; //教师职称 
String getName() //返回教师名字 
{ return name; 
} 
void setName (String name) //记录教师名字 
{ this.name=name; 
} 
boolean getSex() //返回教师性别 
{ return sex; 
} 
void setSex (boolean sex) //记录教师性别 
{ this.sex=sex; 
} 
Date getBirth() //返回教师出生日期
·90· 
源代码

{ re tu rn b ir th ; 
} 
void setBirth (Date birth) //记录教师出生日期 
{ this.birth=birth; 
} 
String getSalaryID() //返回教师工资号 
{ return salaryID; 
} 
void setSalaryID (String salaryID) //记录教师工资号 
{ this.salaryID=salaryID; 
} 
String getDepart() //返回教师所属系所名 
{ return depart; 
} 
void setDepart (String depart) //记录教师所属系所名 
{ this.depart=depart; 
} 
String getPosit() //返回教师职称 
{ return posit; 
} 
void setPosit (String posit) //记录教师职称 
{ this.posit=posit; 
} 
public SchoolTeacher(String name) //只含一个属性参数的构造方法 
{ this.name=name; 
} 
//含有3 个属性参数的构造方法 
public SchoolTeacher(String name, boolean sex, Date birth) 
{ this(name); //调用只含一个参数的构造方法 
this.sex=sex; //对其余的两个属性赋值 
this.birth=birth; 
} 
//含有全部6 个属性参数的构造方法 
public SchoolTeacher(String name, boolean sex, Date birth, 
String salaryid, String depart, String posit) 
{ this(name, sex, birth); //调用含3 个参数的构造方法 
this.salaryID=salaryid; //对其余的3 个属性赋值 
this.depart=depart; 
this.posit=posit; 
} 
public void print () //输出教师基本信息 
{ System.out.print("The SchoolTeacher name: "); 
System.out.println(this.getName()); 
System.out.print("The SchoolTeacher sex: "); 
if (this.getSex()==false) 
{ System.out.println("女"); 
} 
else 
{ System.out.println("男"); 
} 
·91·

Sy st em .o u t. pr in t( "T h e S c ho ol Te a ch er birth: "); 
System.out.println(this.getBirth().year +"-"+this.getBirth().month 
+"-"+this.getBirth().day ); 
System.out.print("The SchoolTeacher salaryid: "); 
System.out.println(this.getSalaryID()); 
System.out.print("The SchoolTeacher posit: "); 
System.out.println(this.getPosit()); 
System.out.print("The SchoolTeacher depart: "); 
System.out.println(this.getDepart()); 
} 
public static void main (String [] args) 
{ Date dt1=new Date(12, 2, 1985); //创建日期实例,作为教师的出生日期 
Date dt2=new Date(2, 6, 1975); 
Date dt3=new Date(11, 8, 1964); 
Date dt4=new Date(10, 4, 1975); 
Date dt5=new Date(8, 9, 1969); 
//创建两个教师实例,一个为父类的实例,另一个为子类的实例 
SchoolTeacher t1=new SchoolTeacher("zhangsan", false,dt1, "123", 
"CS", "Professor"); 
ResearchSchoolTeacher rt=new ResearchSchoolTeacher("lisi", true, dt2, "421", 
"software engineering", "associate professor", "Software"); 
//分别调用各自的输出方法,输出相应的信息 
System.out.println("-----------------------------------"); 
t1.print(); //输出普通教师的信息 
System.out.println("-----------------------------------"); 
rt.print(); //输出研究系列教师的信息 
System.out.println("-----------------------------------"); 
} 
}
class ResearchSchoolTeacher extends SchoolTeacher //研究系列教师类的定义
{ private String resField; //增加的研究领域属性 
String getResField() //返回研究领域属性 
{ return resField; 
} 
void setResField (String resField) //记录研究领域属性 
{ this.resField=resField; 
} 
public ResearchSchoolTeacher(String name, boolean sex, Date birth, 
String salaryid, String depart, String posit, String resField) 
{ //使用父类的构造方法,对共有的6 个属性进行赋值 
super(name, sex, birth, salaryid, depart, posit); 
this.resField=resField; //特殊属性的赋值 
} 
public void print() 
{ System.out.println("One of Research SchoolTeachers' info is "); 
super.print(); //使用父类的输出方法
·92·