第五章 对象和类
本模块是Java编程语言中讨论面向对象语句及面向对象特征两部分中的第一部分。
第一节 相关问题
讨论—下面的问题与本模块中出现的材料相关:
到目前为止学习的Java编程语言的元素存在于大部分语言中,不管它们是否是面向对象语言。
Java编程语言拥有什么特征使它成为一个面向对象语言?
“面向对象”这个术语真正的含义是什么?
第二节 目 标
学完本模块,你便能:
定义封装、多态性以及继承
使用private及public访问修饰符
开发程序段创建并初始化一个对象
对一个特殊对象调用一个方法
描述构造函数及方法重载
描述this引用的用途
讨论为什么Java应用程序代码是可重复使用的
在一个Java程序中,确认:
package语句
import语句
类、成员函数以及变量
构造函数
重载方法
覆盖方法
父类构造函数
第三节 对象基础
面向对象程序(OOP)语句能使现实世界中的概念在计算机程序中变成模块。它包括构造程序的特征以及组织数据和算法的机制。OOP语言有三个特征:封装、多态性及继承。所有这些特征与类的概念是息息相关的。
5.3.1 抽象数据类型
当数据类型是由数据项组成时,可以定义许多程序段或方法在该类型数据上专门运行。当程序语言定义一个基本类型如整数时,它同时也定义了许多运算方法(如加法、减法、乘法和除法),因而它可以在该类型的实例中运行。
在许多程序语言中,一旦一个集合数据类型已经定义,程序员定义应用函数在该类型的变量上运行,该变量在代码和集合类型(除非可能在命名规则中)之间无任何联系。
有些程序语言,包括Java,允许在数据类型的声明和操作该类型变量的代码的声明之间有紧密的联系。这种联系通常被称为抽象数据类型。
5.3.2 类和对象
Java编程语言中的抽象数据类型概念被认为是class。类给对象的特殊类型提供定义。它规定对象内部的数据,创建该对象的特性,以及对象在其自己的数据上运行的功能。因此类就是一块模板。Objects是在其类模块上建立起来的,很象根据建筑图纸来建楼。同样的图纸可用来建许多楼房,而每栋楼房是它自己的一个对象。
应该注意,类定义了对象是什么,但它本身不是一个对象。在程序中只能有类定义的一个副本,但可以有几个对象作为该类的实例。在Java编程语言中使用new运算符实例化一个对象。
在类中定义的数据类型用途不大,除非有目的地使用它们。方法定义了可以在对象上进行的操作,换言之,方法定义类来干什么。因此Java编程语言中的所有方法都属于一类。不象C++程序,Java软件程序不可能在类之外的全局区域有方法。
看一个类的例子:
class EmpInfo {
String name;
String designation;
String department;
}
这些变量(name, designation和department)被称为类EmpInfo的成员。
实例化一个对象,创建它,然后如下所述对其成员赋值:
EmpInfo employee = new EmpInfo(); //creates instance
employee.name = Robert Javaman "; // initializes
employee.designation = " Manager " ; // the three
employee.department = " Coffee Shop " ; // members
EmpInfo类中的employee对象现在就可以用了。例如:
System.out.println(employee.name + " is " +
employee.designation + " at " +
employee.department);
打印结果如下:
Robert Javaman is Manager at Coffee Shop
如下所述,现在可以在类中放入方法print( )来打印数据了。数据和代码可以封装在一个单个实体中,这是面向对象语言的一个基本特征。定名为print( )的代码段可以被作为一个方法而调用,它是术语“函数”的面向对象的称法。
class EmpInfo {
String name;
String designation;
String department;
void print() {
System.out.println(name + " is " + designation + " at " + department);
}
}
一旦对象被创建并被实例化,该方法就打印出类成员的数据。按下述步骤实现:
EmpInfo employee = new EmpInfo(); // creates instance
employee.name = " Robert Javaman " ; // initializes
employee.designation = " Manager " ; // the three
employee.department = " Coffee Shop " ; // members
employee.print();// prints the details
看看集合数据类型MyDate 和对下一个日期赋值的函数tomorrow( )。
按如下所述在MyDate类型和tomorrow( )方法之间创建一种联系:
public class MyDate {
private int day, month, year;
public void tomorrow() {
// code to increment day
}
}
注—本声明中的“private”一词将在后文描述。
方法不是(作为分离的实体)在数据上运行,而数据(作为对象的一部分)对它本身进行操作。
MyDate d = new MyDate();
d.tomorrow();
这个注释表明行为是由对象而不是在对象上完成的。记住,可以用点记号来指向MyDate类中的字段。
这就意味着“MyDate对象的day字段由变量d.调用。于是,前面的例子“MyDate对象的tomorrow行为由变量d.调用” ,换言之,就是d对象对它本身进行tomorrow()运算。
方法是一个对象的属性并且能作为单个单元的一部分与它所在对象的数据发生密切的相互作用,这个是一个关键的面向对象的概念。(如果与这个概念不同,即,方法是分离的实体,从外部引入,作用在数据上。)message passing(消息传递)这个术语通常用来表达这样一个概念,即:指示一个对象在它本身数据上做某项工作,一个对象的方法定义了该对象能在它本身数据上做什么。
5.3.3 定义方法
定义方法
方法声明采取这样的格式:
<modifiers> <return_type> <name> ([<argument_list>]) <block> [throws <exception>] {<block>}
public void addDays(int days) {
}
Java编程语言使用一种与其它语言,尤其是C和C++,非常相似的办法来定义方法。其声明采用以下格式:
<modifiers> <return_type> <name> ([<argument_list>]) <block> [throws <exception>] {<block>}
<name>可以是任何合法标识符,并带有用已经使用的名称为基础的某些限制条件。
<return_type>表示方法返回值的类型。如果方法不返回任何值,它必须声明为void(空)。Java技术对返回值是很严格的,例如,如果声明某方法返回一个int值,那么方法必须从所有可能的返回路径中返回一个int(只能在等待返回该int值的上下文中被调用。)
<modifiers>段能承载许多不同的修饰符,包括公共的、受保护的,以及私有的。公共访问修饰符表示方法可以从任何其它代码调用。私有表示方法只可以由该类中的其它方法来调用。受保护将在以后的课程中讨论。
<argument_list>允许将参数值传递到方法中。列举的元素由逗号分开,而每一个元素包含一个类型和一个标识符。
throws <exception>子句导致一个运行时错误(异常)被报告到调用的方法中,以便以合适的方式处理它。非正常的情况在<exception>中有规定。
例如:
public void addDays(int days) {
}
告诉方法的本体,用<block>,来接受表示将天数增加到当前日期中的那个参数。 在这种方法中,值是以标识符days来引用的。
5.3.4 值传递
值传递
Java编程语言只由值传递参数
当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。
对象的内容可以在被调用的方法中改变,但对象的引用是永远不会改变的。
Java编程语言只由值传递参数,也就是说,参数不能由被调用的方法来改变。当一个对象实例作为一个参数传递到方法中时,参数的值就是对象的引用。对象的内容可以在被调用的方法中改变,但对象引用是永远不会改变的。
下面的代码例子可以阐明这一点:
1 public class PassTest {
2
float ptValue;
// Methods to change the current values
public void changeInt (int value) {
value = 55;
}
public void changeStr (String value) {
value = new String ( " different " );
}
public void changeObjValue (PassTest ref) {
ref.ptValue = 99.0f;
}
public static void main (String args[]) {
String str;
int val;
// Create an instance of the class
PassTest pt = new PassTest ();
// Assign the int
val = 11;
// Try to change it
pt.changeInt (val);
// What is the current value?
System.out.println ( " Int value is: " + val);
// Assign the string
str = new String ( " hello " );
// Try to change it
pt.changeStr (str);
// What is the current value?
System.out.println ( " Str value is: " + str);
// Now set the ptValue
pt.ptValue = 101.0f;
// Now change the value of the float
// through the object reference
pt.changeObjValue (pt);
// What is the current value?
System.out.println ( " Current ptValue is: " +
pt.ptValue);
}
}
这个代码输出如下内容:
c:\student\source\> java PassTest
Int value is: 11
Str value is: hello
Current ptValue is: 99.0
字符串对象是不会被changeStr()改变的,但是,PassTest对象的内容被改变了。
5.3.5 this引用
this引用
public class MyDate {
private int day, month, year;
public void tomorrow() {
this.day = this.day + 1;
// wrap around code...
}
}
关键字this是用来指向当前对象或类实例的。这里,this.day指的是当前对象的day字段。
public class MyDate {
private int day, month, year;
public void tomorrow() {
this.day = this.day + 1;
// wrap around code...
}
}
Java编程语言自动将所有实例变量和方法引用与this关键字联系在一起,因此,使用关键字在某些情况下是多余的。下面的代码与前面的代码是等同的。
public class MyDate {
private int day, month, year;
public void tomorrow() {
day = day + 1; // no ` this. ' before ` day '
// wrap around code...
}
}
也有关键字this使用不多余的情况。如,需要在某些完全分离的类中调用一个方法并将当前对象的一个引用作为参数传递时。例如:
Birthday bDay = new Birthday (this);
5.3.6 数据隐藏
数据隐藏
public class Date {
private int day, month, year;
pubic void tomorrow(){
this.day = this.day +1;
// wrap around code…
}
}
public class DateUser {
public static void main(String args[]) {
Date mydate = new MyDate ();
Mydate.day = 21; //illegal!
}
}
在MyDate 类的day,month,year声明中使用关键字private,使从除了在MyDate类本身的方法以外的任何代码中访问这些成员成为不可能。因此,给MyDate类指定一个声明,下述代码是非法的:
public class DateUser {
public static void main(String args[]) {
MyDate d = new MyDate ();
d.day = 21; // illegal!
}
}
防止直接访问数据变量看起来奇怪,但它实际上对使用MyDate类的程序的质量有极大的好处。既然数据的单个项是不可访问的,那么唯一的办法就是通过方法来读或写。因此,如果要求类成员的内部一致性,就可以通过类本身的方法来处理。
思考一下允许从外部对其成员进行自由访问的MyDate类。代码做如下工作将是非常容易的:
MyDate d = new MyDate ();
d.day = 32; // invalid day
d.month = 2; d.day = 30; // plausible but wrong
d.month = d.month + 1; // omit check for wrap round
警告—这些和其它类似的赋值导致了在MyDate对象字段中无效的或不一致的值。这种情形是不可能马上作为问题暴露出来的,但肯定会在某个阶段终止程序。
如果类的数据成员没有暴露(被封装在类里),那么,类的用户会被迫使用方法来修改成员变量。这些方法能进行有效性检查。考虑将下述方法作为MyDate类的一个部分。
public void setDay(int targetDay) {
if (targetDay > this.daysInMonth()) {
System.err.println( " invalid day " + targetDay);
}
else {
this.day = targetDay;
}
}
方法检查就是要看所要求设定的日期是否有效。如果日期无效,方法忽略了请求并打印出了信息。后面将会看到Java编程语言提供一个更有效的机制来处理溢出的方法参数。 但现在,你可以看到Date类对无效日期是受到有效保护的。
注—在上例中,daysInMonth()被指定为Date类中的一个方法。因此,它有一个this值,从这个值上,它可以提取回答询问所要求的month和 year。由于与成员变量一起使用,daysInMonth()方法的this的使用就多余了。
关于如何正确地调用方法,诸如“参数“月份”的值在对象中必须在有效范围内”的规则被叫做不变量(或前置和后置条件)。谨慎使用前置条件测试,可使类更容易再次使用,而且在重用中更可靠,因为如果任何方法被误用,使用该类的程序员能马上发现。
5.3.7 封装
封装
隐藏类的实现细节
迫使用户去使用一个界面去访问数据
使代码更好维护
除了保护对象的数据不被误修改外,在确保所要求的副作用被正确处理的情况下,迫使用户通过方法访问数据能使类的重用变得更简单。比如,在MyDate类的情形之下,考虑如何构造tomorrow()方法。
如果数据完全可以访问,那么类的每一个用户都需要增加day的值,测试当前月份的天数,并处理月终(而且可能是年终)情形。这就毫无意义而且容易出错。仅管这些要求对日期来说是很好理解的,其它数据类型可能具有鲜为人知的相似的限制。通过迫使类的用户去使用所提供的tomorrow()方法,保证每个人每次都能连续地并且正确地处理必要的副作用。
数据隐藏通常指的就是封装。它将类的外部界面与类的实现区分开来,隐藏实现细节。迫使用户去使用外部界面,即使实现细节改变,还可通过界面承担其功能而保留原样,确保调用它的代码还继续工作。这使代码维护更简单。
第四节 重载方法名称
重载方法名称
它可如下所示使用:
public void println(int i)
public void println(float f)
public void println()
参数表必须不同
返回类型可以不同
在某些情况下,可能要在同一个类中写几种做同样工作但带有不同参数的方法。考虑一个简单方法,它试图输出参数的文本表示法。这种方法被称做println()。
现在假设打印每个int,float,String类型需要不同的打印方法。这是合情合理的,因为各种数据类型要求不同的格式,而且可能要求不同的处理。可以分别创建三种方法,即:printInt(),printfloat()和printString()。但这是很乏味的。
Java与其它几种编程语言一样,允许对不止一种方法重用方法名称。清楚地说,只有当某种东西能区分实际上需要哪种方法并去调用它时,它才能起作用。在三种打印方法的情况下,可能在参数的数量和类型的基础上对此进行区分。
通过重用方法名称,可用下述方法结束:
public void println(int i)
public void println(float f)
public void println()
当写代码来调用这些方法中的一种方法时,根据提供的参数的类型选择合适的一种方法。
有两个规则适用于重载方法:
调用语句的参数表必须有足够的不同,以至于允许区分出正确的方法被调用。正常的拓展晋升(如,单精度类型float到双精度类型double)可能被应用,但是这样会导致在某些条件下的混淆。
方法的返回类型可以各不相同,但它不足以使返回类型变成唯一的差异。重载方法的参数表必须不同。
第五节 构造并初始化对象
构造并初始化对象
调用new XXX()来为新对象分配空间会产生如下结果:
新对象的空间被分配并被初始化为0或空值
进行了显式的初始化
构造函数被执行
已经看到,为了给新对象分配空间,如何必须执行调用到new XXX()。有时还会看到参数被放在括号中,如:new Button (“press me”)。使用关键字new会导致下述情况:
首先,新对象的空间被分配并初始化到0或空值。在Java编程语言中,该阶段是不可分的,以确保在分配空间的过程中不会有大小为随机值的对象。
其次,进行显式的初始化。
第三,构造函数被执行,它是一种特殊方法。括号中传递给new的参数被传递给构造函数。
本节讨论这最后两个步骤。
5.5.1 显式成员初始化
显式成员初始化
public class Initialized {
private int x = 5;
private String name = " Fred " ;
private Date created = new Date();
// Methods go here
...
}
如果将简单的赋值表达式放在成员声明中,在对象构造过程中会进行显式成员初始化。
public class Initialized {
private int x = 5;
private String name = " Fred " ;
private Date created = new Date();
// Methods go here
...
}
5.5.2 构造函数
构造函数
方法名称必须完全与类名称相匹配
对于方法,不要声明返回类型
刚刚描述过的显式初始化机制为在一个对象中设定字段的初始值提供了一个简单的办法。但有时确实需要执行一个方法来进行初始化。也许还需要处理可能发生的错误。或者想用循环或条件表达式来进行初始化。或者想把参数传递到构造过程中以便要求构造新对象的代码能控制它创建的对象。
一个新对象的初始化的最终步骤是去调用一个叫做构造函数的方法。
构造函数是由下面两个规则所确认的:
方法名称必须与类名称完全相匹配。
对于方法,不要声明返回类型
构造函数
public class Xyz {
// member variables go there
public Xyz() {
// set up the object.
}
public Xyz(int x) {
// set up the object with a parameter
}
}
public class Xyz {
// member variables
public Xyz() { // No-arg constructor
// set up the object.
}
public Xyz(int x) { //int-arg constructor
// set up the object using the parameter x.
}
}
注—由于采用了方法,因此可以通过为几个构造函数提供不同的参数表的办法来重载构造函数。当发出new Xyz(argument_list)调用的时候,传递到new语句中的参数表决定采用哪个构造函数。
5.5.3 调用重载构造函数
调用重载构造函数
public class Employee {
private String name;
private int salary;
public Employee(String n, int s) {
name = n;
salary = s;
}
public Employee(String n) {
this(n, 0);
}
public Employee() {
this( " Unknown " );
}
}
如果有一个类带有几个构造函数,那么也许会想复制其中一个构造函数的某方面效果到另一个构造函数中。可以通过使用关键字this作为一个方法调用来达到这个目的。
public class Employee {
private String name;
private int salary;
public Employee(String n, int s) {
name = n;
salary = s;
}
public Employee(String n) {
this(n, 0);
}
public Employee() {
this( " Unknown " );
}
}
在第二个构造函数中,有一个字符串参数,调用this(n,0)将控制权传递到构造函数的另一个版本,即采用了一个String参数和一个int参数的构造函数中。
在第三个构造函数中,它没有参数,调用this(“Unknownn”)将控制权传递到构造函数的另一个版本,即采用了一个String参数的构造函数中。
注—对于this的任何调用,如果出现,在任何构造函数中必须是第一个语句。
5.5.4 缺省构造函数
缺省构造函数
每个类中都有
能够用new Xxx()创建对象实例
如果增加一个带参数的构造函数声明,将会使缺省无效
每个类至少有一个构造函数。如果不写一个构造函数,Java编程语言将提供一个。该构造函数没有参数,而且函数体为空。
缺省构造函数能用new Xxx()创建对象实例,反之,你会被要求为每个类提供一个构造函数。
注—如果增加一个带参数的构造函数声明到一个类中,该类以前没有显式构造函数,那么将失去该缺省构造函数。基于这一点,对new Xxx()的调用将会引起编译错误。认识到这一点很重要。
第六节 子 类
5.6.1 is a 关系
Is a 关系
Employee类
public class Employee {
String name;
Date hireDate;
Date dateOfBirth;
}
在编程中,常常要创建某件事的模型(如:一个职员),然后需要一个该基本模型的更专业化的版本。比如,可能需要一个经理的模型。显然经理实际上是一个职员,只是一个带有附加特征的职员。
看看下面的例子中类声明所阐述的:
public class Employee {
String name;
Date hireDate;
Date dateOfBirth;
String jobTitle;
int grade;
...
}
这个例子阐述了在Manager和Employee类之间的数据复制。此外,还可能有许多适用于 Employee和 Manager两者的方法。因此,需要有一种办法从现有类来创建一个新类。这就叫做子类。
5.6.2 Extends关键字
Extends关键字
public class Employee {
String name;
Date hireDate;
Date dateOfBirth;
String jobTitle;
int grade;
...
}
public class Manager extends Employee {
String department;
Employee [] subordinates;
}
在面向对象的语言中,提供了特殊的机制,允许程序员用以前定义的类来定义一个类。如下所示,可用关键字extends来实现:
public class Employee {
String name;
Date hireDate;
Date dateOfBirth;
String jobTitle;
int grade;
...
}
public class Manager extends Employee {
String department;
Employee [] subordinates;
...
}
在这样的安排中,Manager类被定义,具有 Employee 所拥有的所有变量及方法。所有这些变量和方法都是从父类的定义中继承来的。所有的程序员需要做的是定义额外特征或规定将适用的变化。
注--这种方法是在维护和可靠性方面的一个伟大进步。如果在Employee类中进行修改,那么, Manager类就会自动修改,而不需要程序员做任何工作,除了对它进行编译。
注—对一个继承的方法或变量的描述只存在于对该成员进行定义的类的API文档中。当浏览探索一个(子)类的时候,一定要检查父类和其它祖先类中的继承成员。
5.6.3 参数和异类收集
参数和异类收集
具有共同点的类的收集被称做同类收集,如:数组
具有不同对象的收集叫异类收集,如:从各种其它类继承的类的收集
可以创建具有共同类的对象的收集(如数组)。这种收集被称作同类收集。
Java编程语言有一个对象类,因此,由于多态性,它能收集所有种类的元素,正如所有类都扩展类对象一样。这种收集被称作异类收集。
创建一个Manager并慎重地将其引用赋到类型Employee的变量中似乎是不现实的。但这是可能的,而且有很多为什么要取得这种效果的理由。
用这种方法,可以写出一个能接受通用对象的方法,在这种情况下,就是类Employee,并在它的任何子类的对象上正确地运作。然后可以在应用类中产生一个方法,该应用类抽取一个职员,并将它的薪水与某个阈值进行比较来决定该职员的纳税责任。利用多态性,可以做到这些:
// In the Employee class
public TaxRate findTaxRate(Employee e) {
// do calculations and return a tax rate for e
}
// Meanwhile, elsewhere in the application class
Manager m = new Manager();
:
TaxRate t = findTaxRate(m);
这是合法的,因为一个经理就是一个职员。
异类收集就是不相似的东西的收集。在面向对象语言中,可以创建许多东西的收集。所有的都有一个共同的祖先类-Object类。如:
Employee [] staff = new Employee[1024];
staff[0] = new Manager();
staff[1] = new Employee();
甚至可以写出一个排序的方法,它将职员按年龄或薪水排序,而忽略其中一些人可能是经理。
注-每个类都是Object的一个子类,因此,可以用Object数组作为任何对象的容器。唯一不能被增加到Object数组中的唯一的东西就是基本变量(以及包装类,将在模块6中讨论,请注意这一点)。比Object数组更好的是向量类,它是设计来贮存异类收集对象的。
5.6.4 单继承性
单继承性
当一个类从一个唯一的类继承时,被称做单继承性。单继承性使代码更可靠。界面提供多继承性的好处,而且没有(多继承的)缺点。
Java编程语言允许一个类仅能扩展成一个其它类。这个限制被称做单继承性。单继承性与多继承性的优点是面向对象程序员之间广泛讨论的话题。Java编程语言加强了单继承性限制而使代码更为可靠,尽管这样有时会增加程序员的工作。模块6讨论一个被叫做界面(Interface)的语言特征,它允许多继承性的大部分好处,而不受其缺点的影响。
使用继承性的子类的一个例子如图5-1所示:
图5-1 继承
5.6.5 构造函数不能被继承
构造函数不能被继承
子类从超类(父类)继承所有方法和变量
子类不从超类继承构造函数
包含构造函数的两个办法是
使用缺省构造函数
写一个或多个显式构造函数
尽管一个子类从父类继承所有的方法和变量,但它不继承构造函数,掌握这一点很重要。
一个类能得到构造函数,只有两个办法。或者写构造函数,或者根本没有写构造函数,类有一个缺省构造函数。
注-除了子构造函数外,父类构造函数也总是被访问。这将在后面的模块中详细讨论。
5.6.6 多态性
多态性
有能力拥有有许多不同的格式,就叫做多态性。比如,经理类能访问职员类方法。
一个对象只有一个格式
一个变量有许多格式,它能指向不同格式的对象
将经理描述成职员不只是描述这两个类之间的关系的一个简便方法。回想一下,经理类具有父类职员类的所有属性、成员和方法。这就是说,任何在Employee上的合法操作在Manager上也合法。如果Employee 有raiseSalary()和fire()两个方法,那么Manager 类也有。
一个对象只有一个格式(是在构造时给它的)。但是,既然变量能指向不同格式的对象,那么变量就是多态性的。在Java编程语言中,有一个类,它是其它所有类的父类。这就是java.lang.object类。因此,实际上,以前的定义被简略为:
public class Employee extends Object and
public class Manager extends Employee
Object类定义许多有用的方法,包括toString(),它就是为什么Java软件中每样东西都能转换成字符串表示法的原因。(即使这仅具有有限的用途)。
象大多数面向对象语言一样,Java实际上允许引用一个带有变量的对象,这个变量是父类类型中的一个。因此,可以说:
Employee e = new Manager()
使用变量e是因为,你能访问的对象部分只是Employee的一个部分;Manager的特殊部分是隐藏的。这是因为编译者应意识到,e 是一个Employee,而不是一个Manager。因而,下述情况是不允许的:
e.department = " Finance " ; // illegal
注-多态性是个运行时问题,与重载相反,重载是一个编译时问题。
5.6.7 关键字super
关键字super
super被用在类中引用其超类。
super被用来调用超类的成员变量。
超类行为就被调用,就好象对象是超类的组件。
调用行为不必发生在超类中,它能自动向上层类追溯。
关键字super可被用来引用该类中的超类。它被用来引用超类的成员变量或方法。
通常当覆盖一个方法时,实际目的不是要更换现有的行为,而是要在某种程度上扩展该行为。
用关键字super 可获得:
public class Employee {
private String name;
private int salary;
public String getDetails() {
return Name: " + name + "\nSalary: " + salary;
}
}
public class Manager extends Employee {
private String department;
public String getDetails() {
return super.getDetails() + // call parents'
// method
"\nDepartment: " + department;
}
}
请注意,super.method()格式的调用,如果对象已经具有父类类型,那么它的方法的整个行为都将被调用,也包括其所有副面效果。该方法不必在父类中定义。它也可以从某些祖先类中继承。
5.6.8 Instanceof 运算符
Instanceof 运算符
public class Employee extends Object
public class Manager extends Employee
public class Contractor extends Employee
public void method(Employee e) {
if (e instanceof Manager) {
// Get benefits and options along with salary
}else if (e instanceof Contractor) {
// Get hourly rates
}else {
// temporary employee
}
}
假如能使用引用将对象传递到它们的父类中,那么有时你想知道实际有什么,这就是instanceof运算符的目的。假设类层次被扩展,那么你就能得到:
public class Employee extends Object
public class Manager extends Employee
public class Contractor extends Employee
注—记住,在可接受时, extends Object 实际上是多余的。在这里它仅作为一个提示项。
如果通过Employee类型的引用接受一个对象,它变不变成Manager或Contractor都可以。可以象这样用instanceof 来测试:
public void method(Employee e) {
if (e instanceof Manager) {
// Get benefits and options along with salary
}else if (e instanceof Contractor) {
// Get hourly rates
}else {
// regular employee
}
}
注—在C++中,可以用RTTI(运行时类型信息)来做相似的事,但在Java编程语言中的instanceof功能更强大。
5.6.9 对象的类型转换
对象的类型转换
使用instanceof来测试一个对象的类型。
用类型转换来恢复一个对象的全部功能。
用下述提示来检查类型转换的正确性:
向上的类型转换是隐含地实现的。
向下的类型转换必须针对子类并由编译器检查。
当运行时错误发生时,运行时检查引用类型。
在你接收父类的一个引用时,你可以通过使用Instanceof运算符判定该对象实际上是你所要的子类,并可以用类型转换该引用的办法来恢复对象的全部功能。
public void method(Employee e) {
if (e instanceof Manager) {
Manager m = (Manager)e;
System.out.println( " This is the manager of " + m.department);
}
// rest of operation
}
如果不用强制类型转换,那么引用e.department的尝试就会失败,因为编译器不能将被称做department的成员定位在Employee类中。
如果不用instanceof做测试,就会有类型转换失败的危险。通常情况下,类型转换一个对象引用的尝试是要经过几种检查的:
向上强制类型转换类层次总是允许的,而且事实上不需要强制类型转换运算符。可由简单的赋值实现。
对于向下类型转换,编译器必须满足类型转换至少是可能的这样的条件。比如,任何将Manager引用类型转换成Contractor引用的尝试是肯定不允许的,因为Contractor不是一个Manager。类型转换发生的类必须是当前引用类型的子类。
如果编译器允许类型转换,那么,该引用类型就会在运行时被检查。比如,如果instanceof检查从源程序中被省略,而被类型转换的对象实际上不是它应被类型转换进去的类型,那么,就会发生一个运行时错误(exception)。异常是运行时错误的一种形式,而且是后面模块中的主题。
第七节 覆盖方法
覆盖方法
子类可以修改从父类继承来的行为
子类能创建一个与父类方法有不同功能的方法,但具有相同的
名称
返回类型
参数表
除了能用附加额外特征的办法在旧类基础上产生一个新类,它还可以修改父类的当前行为。
如果在新类中定义一个方法,其名称、返回类型及参数表正好与父类中方法的名称、返回类型及参数相匹配,那么,新方法被称做覆盖旧方法。
注—记住,在同类中具有相同名称不同参数表的方法是被简单覆盖。这导致编译器使用所提供的参数来决定调用哪个方法。
考虑一下在Employee和Manager类中的这些方法的例子:
public class Employee {
String name;
int salary;
public String getDetails() {
return " Name: " + name + " \n " +
"Salary: " + salary;
}
}
public class Manager extends Employee {
String department;
public String getDetails() {
return " Name: " + name + " \n " +
" Manager of " + department;
}
}
Manager类有一个定义的getDetails()方法,因为它是从Employee类中继承的。基本的方法被子类的版本所代替或覆盖了。
覆盖方法
虚拟方法调用
Employee e = new Manager();
e.getDetails();
编译时类型与运行时类型
假设第121页上的例子及下述方案是正确的:
Employee e = new Employee();
Manager m = new Manager();
如果请求e.getDetails()和m.getDetails,就会调用不同的行为。Employee对象将执行与Employee有关的getDetails版本,Manager对象将执行与Manager有关的getDetails()版本。
不明显的是如下所示:
Employee e = new Manager();
e.getDetails();
或某些相似效果,比如一个通用方法参数或一个来自异类集合的项。
事实上,你得到与变量的运行时类型(即,变量所引用的对象的类型)相关的行为,而不是与变量的编译时类型相关的行为。这是面向对象语言的一个重要特征。它也是多态性的一个特征,并通常被称作虚拟方法调用。
在前例中,被执行的e.getDetails()方法来自对象的真实类型,Manager。
注—如果你是C++程序员,就会在Java编程语言和C++之间得出一个重要的区别。在C++中,你要想得到该行为,只能在源程序中将方法标记成virtual。然而,在纯面向对象语言中,这是不正常的。当然,C++这么做是要提高执行速度。
第八节 调用覆盖方法
覆盖方法的规则
必须有一个与它所覆盖的方法相同的返回类型
不能比它所覆盖的方法访问性差
不能比它所覆盖的方法抛出更多的异常。
覆盖方法的规则
记住,子方法的名称以及子方法参数的顺序必须与父类中的方法的名称以及参数的顺序相同以便该方法覆盖父类版本。下述规则适用于覆盖方法:
覆盖方法的返回类型必须与它所覆盖的方法相同。
覆盖方法不能比它所覆盖的方法访问性差
覆盖方法不能比它所覆盖的方法抛出更多的异常。(异常将在下一个模块中讨论。)
这些规则源自多态性的属性和Java编程语言必须保证“类型安全”的需要。考虑一下这个无效方案:
public class Parent {
public void method() {
}
}
public class Child extends Parent {
private void method() {
}
}
public class UseBoth {
public void otherMethod() {
Parent p1 = new Parent();
Parent p2 = new Child();
p1.method();
p2.method();
}
}
Java编程语言语义规定,p2.method()导致方法的Child版本被执行,但因为方法被声明为private,p2(声明为Parent)不能访问它。于是,语言语义冲突。
第九节 调用父类构造函数
调用父类构造函数
对象的初始化是非常结构化的。
当一个对象被初始化时,下述行为按顺序发生:
存储空间被分配并初始化到0值
层次中的每个类都进行显式初始化
层次中的每个类都调用构造函数
在Java编程语言中,对象的初始化是非常结构化的。这样做是要保证安全。在前面的模块中,看到了当一个特定对象被创建的实例时发生了什么。由于继承性,图象被完成,而且下述行为按顺序发生:
存储空间被分配并初始化到0值
进行显式初始化
调用构造函数
层次中的每个类都会发生最后两个步骤,从最上层开始。
Java技术安全模式要求在子类执行任何东西之前,描述父类的一个对象的各个方面都必须初始化。因此,Java编程语言总是在执行子构造函数前调用父类构造函数的版本。
调用父类构造函数
在许多情况下,使用缺省构造函数来对父类对象进行初始化。
public class Employee {
String name;
public Employee(String n) {
name = n;
}
}
public class Manager extends Employee {
String department;
public Manager(String s, String d) {
super(s);
department = d;
}
}
无论是super还是this,都必须放在构造函数的第一行
调用父类构造函数
通常要定义一个带参数的构造函数,并要使用这些参数来控制一个对象的父类部分的构造。可能通过从子类构造函数的第一行调用关键字super的手段调用一个特殊的父类构造函数作为子类初始化的一部分。要控制具体的构造函数的调用,必须给super()提供合适的参数。当不调用带参数的super时,缺省的父类构造函数(即,带0个参数的构造函数)被隐含地调用。在这种情况下,如果没有缺省的父类构造函数,将导致编译错误。
public class Employee {
String name;
public Employee(String n) {
name = n;
}
}
public class Manager extends Employee {
String department;
public Manager(String s, String d) {
super(s); // Call parent constructor
// with String argument
department = d;
}
}
注—调用super()能将任意数量的合适的参数赋到父类中的不同构造函数中,但它必须是构造函数中的第一个语句。
当被使用时,super或this必须被放在构造函数的第一行。显然,两者不能被放在一个单独行中,但这种情况事实上不是一个问题。如果写一个构造函数,它既没有调用super(…)也没有调用this(…),编译器自动插入一个调用到父类构造函数中,而不带参数。其它构造函数也能调用super(…)或this(…),调用一个Static方法和构造函数的数据链。最终发生的是父类构造函数(可能几个)将在链中的任何子类构造函数前执行。
第十节 编组类
5.10.1 包
包
包声明必须在源程序文件的开始被声明
根据源程序文件,只允许有一个包声明
// Class Employee of the Finance department for the
// ABC company
package abc.financedept;
public class Employee {
...
}
包名称是分层的,由圆点隔开
Java编程语言提供package机制作为把相关类组成组的途径。迄今为止,所有的这些例子都属于缺省或未命名包。
可以用package语句表明在源程序文件中类属于一个特殊的包。
// Class Employee of the Finance department for the
// ABC company
package abc.financedept;
public class Employee {
...
}
包声明,如果有的话,必须在源程序文件的开始处。可以以空白和注解开始,而没有其它方式。只允许有一个包声明并且它控制整个源程序文件。
包名称是分层的,由圆点分隔。通常情况下,包名称的元素被整个地小写。然而,类名称通常以一个大写字母开始,而且每个附加单词的首字母可以被大写以区分类名称中的单词。
5.10.2 import语句
Import语句
告诉编译器到哪儿寻找类来使用,必须先于所有类声明
import abc.financeDept.*;
public class Manager extends Employee {
String department;
Employee [] subordinates;
}
当需要使用包时,使用import语句来告诉编译器到哪儿去寻找类。事实上,包名称在包中(比如,abc.financedept)形成类的名称的一部分。你可以引用Employee类作为abc.financedept.Employee,或可以使用import语句及仅用类名称Employee。
import abc.financeDept.*;
public class Manager extends Employee {
String department;
Employee [] subordinates;
}
注—import语句必须先于所有类声明。
当使用一个包声明时,不必引入同样的包或该包的任何元素。记住,import语句被用来将其它包中的类带到当前名空间。当前包,不管是显式的还是隐含的,总是当前名空间的一部分。
5.10.3 目录布局及CLASSPATH环境变量
目录布局及CLASSPATH环境变量
包被贮存在包含包名称的目录树中。
package abc.financedept
public class Employee {
}
javac –d . Employee.java
Employee.class的目录路径是什么?
包被贮存在包含包名称分支的目录树中。例如,来自前面页中的Employee.class文件必须存在于下述目录中:
path\abc\financeDept
查寻类文件的包的目录树的根目录是在 CLASSPATH中。
编译器的-d选项规定了包层次的根,类文件被放在它里面(前面所示的path)。
Java技术编译器创建包目录,并在使用-d选项时将编译的类文件移到它当中。
c:\jdk1.2\source\> javac -d . Employee.java
将创建目录结构abc\financedept 在当前目录中(“.”)。
CLASSPATH变量以前没有使用过,因为如果它没被设定,那么,工具的缺省行为将自动地包括类分布的标准位置和当前工作目录。如果想访问位于其它地方的包,那么必须设定CLASSPATH变量来显式地覆盖缺省行为。
c:\jdk1.2\source\>javac -d c:\mypackages Employee.java
为了让编译器在编译前页中的Manager.java 文件时给abc.financedept. Employee类定位, CLASSPATH环境变量必须包括下述包路径:
CLASSPATH=c:\mypackages;.
练习:使用对象及类
练习目的—会写、编译及运行三个程序,通过模仿使用银行帐目使用继承、构造函数及数据隐藏等面向对象概念。
一、准备
为了成功地完成该实验,必须理解类和对象的概念。
二、任务
一级实验:银行帐目
创建一个类,Account.java,它定义银行帐目。决定应该做什么样的帐目,需要贮存什么样的数据,以及将用什么样的方法。
使用一个包,bank,来包含类
二级实验:帐目类型
修改一级实验,因而会针对CheckingAccount类的细节对Account划分子类。
允许检查帐目来提供溢出保护。
三级实验:在线帐目服务
创建一个简单的应用程序,Teller.java,它使用一级或二级实验来提供一个在线帐目开户服务。
三、练习总结
讨论—花几分钟时间来讨论一下实验练习中的经验、问题或发现。
经验 解释 总结 应用
四、检查进步情况
在继续下一个模块前,检查一下,确信你能:
定义封装、多态性和继承
使用访问修饰符private和public
开发一个程序段来创建并初始化一个对象
在一个特定的对象上调用方法
描述构造函数和方法重载
描述this引用起什么作用
讨论为什么Java应用程序代码是可重用的
在Java软件程序中,确认:
package语句
import语句
类成员函数和变量
构造函数
重载方法
覆盖方法
父类构造函数
五、思考
既然理解了对象和类,如何在当前或今后工作中使用这个信息?