第五章
1.类、超类和子类
java的继承格式:
class Manager extends Employee
{
added methods and fields
}
C++注释:java继承用extends代替了C++中的冒号(:)。在java中,所有的集成都是公有继承,没有C++中的私有继承和保护继承。
关于继承的类的不同叫法:
超类(superclass) / 基类(base class) / 父类(parent class)分别对应:
子类(subclass) / 派生类(derived calss) / 孩子类(child class)
java中在子类中调用超类方法的方法--使用super关键字,如:
class Manager extends Employee
{
public Manager(String n, double s, int year, int month, int day)
{
super(n, s, year, month, day);
bonus = 0;
}
public double getSalary() //覆盖超类中的getSalary()方法
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
private double bonus;
}
C++注释:java中的super调用超类相当于C++中的超类名加上::操作符,即Employee::getSalary()。
由于Manager类的方法(包括构造器)不能访问Employee类的私有域,所以必须使用Employee类的构造器对这部分私有域进行初始化。使用super调用构造器的语句必须是子类构造器的第一条语句。如果子类的构造器没有显示调用超类的构造器,则将自动调用超类默认(没有参数)的构造器。如果此时超类没有不带参数的构造器,则编译器将报错。
注释:前面讲到,关键字this有2个用途:一是引用隐式参数,而是调用该类其他的构造器。同样,super关键字也有2个用途:一是调用超类的方法,而是调用超类的构造器。在调用构造器的时候,两个关键字使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。
C++注释:在C++的构造函数中,使用初始化列表语法调用超类的构造函数,而不调用super。如在C++中应该这样:
Manager::Manager(String n, double s, int year, int month, int day) // C++
: Employee(n, s, year, month, day)
{
bonus = 0;
}
下面给出例子,功能为创建一个新经理和2个雇员,设置薪水和奖金,打印出每个人的薪水:
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
for (Employee e : staff)
System.out.println(e.getName() + " " + e.getSalary());
结果如下:
Carl Cracker 85000.0
Harry Hacker 50000.0
Tommy Tester 40000.0
在这里,e.getSalary()能够确定应该调用哪一个getSalary方法,当e引用Employee对象时,调用Employee的getSalary,当e引用Manager对象时,调用Manager的getSalary方法。虚拟机知道e实际引用的对象类型并正确调用相应的方法。一个对象可以引用多种实际类型的现象叫多态。在运行时能自动选择调用哪个方法的现象叫动态绑定。
C++注释:在java中,不需要将方法声明为virtual,动态绑定是默认的处理方法。如果不希望方法具有虚拟特性,可标记为final。
(1)继承层次
C++注释:java不支持多继承。关于java中多继承功能的实现方式,参看下一张关于接口的讨论。
(2)多态
"is-a"规则是一个用来判断是否应该设计为继承关系的简单规则,它表明子类的每个对象也是超类的对象。"is-a"规则的另一种表示法是置换规则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。如可以将一个子类的对象赋给超类变量:
Employee e;
e = new Employee(. . .); // Employee object expected
e = new Manager(. . .); // OK, Manager can be used as well
在java中,对象变量是多态的。一个Employee变量既可以引用一个Employee对象,也可以引用一个Employee类的任何一个子类的对象。
然而,不能将一个超类的引用赋给子类变量。如下面的赋值时非法的:
Manager m = staff[i]; // ERROR
原因很清楚:不是所有雇员都是经理。如果赋值成功,则在后面调用Manager独有的方法m.setBonus(...)时就可能发生运行时错误。
警告:在java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。如下面是合法的:
Manager[] managers = new Manager[10];
Employee[] staff = managers; // OK
然而,这样做有可能会引起一些问题。切记manager和staff引用的是同一个数组。现在看一下这条语句:
staff[0] = new Employee("Harry Hacker", ...);
编译器竟然接纳了这个操作。但在这里,staff[0]和managers[0]引用的是同一个对象,当调用managers[0].setBonus(...)的时候,会导致调用一个不存在的实例域,进而搅乱相邻存储空间的内容。
为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责仅将类型兼容的变量存储到数组中。如使用new managers[10]创建的是一个经理数组,如果试图存储一个Employee类型的引用就会引发ArrayStoreException异常。
(3)动态绑定
下面是调用对象方法过程x.f(“Hello”),且隐式参数x声明为C类的详细描述:
1)编译器查看对象的声明类型和方法名。编译器会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。至此,编译器已获得所有可能被调用的候选方法。
2)接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选这个方法,这个过程被称为重载解析;如果没有找到或通过类型转换后有多个方法匹配,就会报告一个错误。
注释:前面提到过,方法的名字和参数列表成为方法的签名,这里就是找出与要调用的方法相同签名的方法。
3)如果是private、static、final方法或者构造器,则编译器可以准确知道该调用哪个方法,这是静态绑定。于此对应的是,调用的方法依赖于隐式参数的实际类型,并在调用时实现动态绑定。
4)当程序运行,且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最适合的那个类的方法。假设x的实际类型是D,它是C的子类。如果D类定义了方法f(String),就直接调用它,否则将在D的超类中寻找f(String),以此类推。
每次调用方法都要进行搜索,开销很大,因此虚拟机预先为每个类创建了一个方法表,其中列出所有方法的签名和实际调用的方法。实际使用的时候,虚拟机仅查找这个表。需要注意,如果调用super.f(param),编译器将对隐式参数超类的方法表进行搜索。
警告:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是如果超类方法是public,子类方法一定要声明为public。如果遗漏,编译器将会把它解释为试图降低访问权限。
(4)阻止继承:final类和方法
在定义类的时候使用final修饰符表示不能扩展;类中的方法声明为final类表示子类不能覆盖该方法(final类中的所有方法自动成为final方法,但不包括域。类中的域也可被声明为final,这样,构造对象之后就不允许改变它的值了)。
(5)强制类型转换
写法和C++一样,如:
Manager boss = (Manager) staff[0];
将一个值存入变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的,但将一个超类的引用赋给一个子类变量,必须经过类型转换才能呢个通过运行时的检查。
如果类型转换时“谎报”有关对象包含的内容,如:
Manager boss = (Manager) staff[1]; // ERROR
运行该程序时,java运行时系统将报告错误,并产生ClaseeCastException异常。如果没有捕获该异常,程序会终止。因此,应该养成这样的习惯:在进行类型转换前,先查看一下是否能转换成功。可以用instanceof运算符实现,如:
if (staff[1] instanceof Manager)
{
boss = (Manager) staff[1];
. . .
}
如果这个类型转换不可能成功,编译器不会进行该转换。如,下面的类型转换会产生编译错误,因为Date不是Employee的子类:
Date c = (Date) staff[1];
综上所述:
只能在继承层次内进行类型转换;
在将超类转换成子类之前,应该使用instanceof进行检查。
(6)抽象类
抽象类可作为派生其他类的基类,而不作为实例类。如Employee类和Student类都是人,可将类Person作为基类,如下图:
使用abstract关键字的方法被称为抽象方法,抽象方法不需要实现,类似于C++中的纯虚函数。包含一个或多个抽象方法的类被称为抽象类,且必须被声明为abstract,如:
abstract class Person
{ . . .
public abstract String getDescription();
}
除了抽象方法外,抽象类还可以包含具体数据和具体方法。建议尽量将通用的域和方法(不管是否抽象)放在超类(不管是否抽象)中。
抽象方法的具体实现在子类中。扩展抽象方法可以有两种选择,一是在子类中定义部分抽象方法或不定义抽象方法,这样必须将子类标记为抽象类;另一种是定义全部的抽象方法,这样,子类就不是抽象的了。
类中即使不含抽象方法,也可以将类声明为抽象类。
抽象类不能被实例化,如下面的表达式是错误的:
new Person("Vince Vu")
但是,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象,如:
Person p = new Student("Vince Vu", "Economics");
C++注释:在C++中,纯虚函数是抽象方法。只要有一个纯虚函数,这个类就是抽象类,但没有用于表示抽象类的关键字。
(7)受保护访问
java中用于控制可见性的4个访问修饰符:
1)仅对本类可见--private
2)对所有类可见--public
3)对本包和所有子类可见--protected(C++中的protected是对子类和友元函数可见)
4)对本包可见--默认,即没有任何修饰符的情况
2.Object:所有类的超类
如果没有明确指出超类,Object就被认为是这个类的超类。本章介绍一些基本内容。在Object中有几个只在处理线程时才会被调用的方法,有关线程内容参看卷II。
可以用Object类型的变量引用任何类型的对象:
Object obj = new Employee("Harry Hacker", 35000);
当然,要对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换:
Employee e = (Employee) obj;
在java中,只有基本类型不是对象,如数值、字符和布尔类型的值 都不是对象。所有数组类型,不管是对象数组还是基本类型的数组都扩展于Object类。
Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK
C++注释:在C++中没有根类,不过,每个指针都可以转换成void*。
(1)Equals方法
用于检测一个对象是否等于另一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。但在大多数类中,这种判断没有什么意义。不过,经常需要检测两个对象状态的相等性,如果对象状态相等,就认为这两个对象是相等的。
例如,如果两个雇员的姓名、薪水、雇佣日期都一样,就认为它们相等(实际上,比较ID更有意义)。如:
class Employee
{
. . .
public boolean equals(Object otherObject)
{
// a quick test to see if the objects are identical
if (this == otherObject) return true;
// must return false if the explicit parameter is null
if (otherObject == null) return false;
// if the classes don't match, they can't be equal
if (getClass() != otherObject.getClass())
return false;
// now we know otherObject is a non-null Employee
Employee other = (Employee) otherObject;
// test whether the fields have identical values
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
}
}
getClass方法将返回一个对象所属的类。
在子类中定义equals方法时,首先调用超类的equals方法,如果检测失败,对象就不可能相等:
class Manager extends Employee
{
. . .
public boolean equals(Object otherObject)
{
if (!super.equals(otherObject)) return false;
// super.equals checked that this and otherObject belong to the same class
Manager other = (Manager) otherObject;
return bonus == other.bonus;
}
}
(2)相等测试与继承