本节详细讲述继承的实现方法以及一些相关技巧。
对于实例变量,当创建一个类的实例时,会生成所有在这个类和它的所有父类中定义的实例变量,然后这些类在其中写入适当的初始化信息。
如果在一个类中声明了一个域和其父类中的域重名,这个类中的域就会将父类中的域隐藏起来。被隐藏的成员变量不能使用简单名字来访问,要使用其他的方法。对于被隐藏的类变量,可以使用限定名称来访问,即在简单名称前面加上类名和点运算符(.)。被隐藏的直接父类中的成员变量,可以使用super关键字来访问;或者还可以将对象强制转换成父类的实例,然后再使用简单名字来访问被隐藏的成员变量。
子类中的实例变量可以隐藏父类中的类变量,同样,子类中的类变量也可以隐藏父类中的实例变量。
【例4-2】在本例中,subClass继承superClass,并且隐藏了superClass的x、y、z域。
class superClass{
static int x = 1;
int y = 2;
static int z = 3;
String str = "superClass";
}
class subClass extends superClass{
static double x = 1.2;
float y = 2.3f;
String z = "subClass";
}
public class FieldExtend{
public static void main(String[] args){
subClass s = new subClass();
System.out.println("s.x = " + s.x);
System.out.println("s.y = " + s.y);
System.out.println("s.z = " + s.z);
System.out.println("s.str = " + s.str);
System.out.println("((superClass)s).y = " + ((superClass)s).y);
}
}
这个程序的运行结果如下:
s.x = 1.2
s.y = 2.3
s.z = subClass
s.str = superClass
((superClass)s).y = 2
结果表明,subClass隐藏了superClass的x、y、z域,继承了superClass的str域,而且把对象s强制类型转化为superClass的实例,在引用y域时,引用的是其父类superClass的y域。
方法继承和成员变量的继承相仿,新的对象可以访问当前类和其所有父类中的方法。但是当方法调用时,它要动态地选择所调用的方法。例如,要调用某个对象的一个方法,Java会先在生成这个对象的类中寻找此方法的定义,如果找到了,就调用这个类中的方法,否则继续在其父类中寻找此方法的定义,直到找到为止。如果找不到,编译器就会报错。
当通过一个类的实例调用一个方法的时候,Java将首先在这个实例的运行时刻的类中查找此方法的声明,如果找到了,就调用这个方法,否则就继续在其父类中查找,直到找到为止。因此,如果在一个类中声明了一个方法,其名称和返回值类型与父类中的某个方法一样,那么当通过这个类的实例调用这个方法时,就会调用此类中的方法,而不是父类中的方法。这样,就产生了覆盖和隐藏的现象。在4.2.1节中已经介绍了成员变量的隐藏,然而对于方法来说,除了隐藏以外,还有覆盖(override)的现象,而且方法的隐藏和成员变量的隐藏有很大的不同。方法的隐藏是对类方法而言的,而方法的覆盖则是对实例方法而言的。
当声明一个类时,它继承了其父类中的所有的可以访问的方法。它可以直接使用这些方法,而不用再将其代码复制一次,这是继承给我们带来的好处。然而,有时会希望子类的对象对父类中定义的方法作出不同的反应,表现出不同的行为。这时,可以在子类中定义相同名称和返回值的方法来覆盖父类中的方法。这样,当通过子类的对象调用这个方法时,就会调用子类中的方法,而不是父类中的方法。例如:
class classA{
void y(){
System.out.println("y() in classA");
}
static void z(){
System.out.println("z() in classA");
}
}
class classB extends classA{
void y(){
System.out.println("y() in classB");
}
}
public class MethodExtend{
public static void main(String[] args){
classB s = new classB();
s.y();
s.z();
classA t = new classB();
t.y();
}
}
这个程序的输出如下:
y() in classB
z() in classA
y() in classB
可见,classB类中的y( )方法覆盖了其父类中的y( )方法,在使用classB类的实例调用这些方法时,将调用classB类中的方法,而不是classA类中的方法。虽然在程序的最后将一个classB类的实例赋给了一个classA类的引用变量,但是当调用其y( )方法时,仍然将调用classB类中的y( )方法,而不是引用变量所属的类classA中的y( )方法。
在子类中访问父类中被覆盖的实例方法,只能使用super关键字,而不能用带类名的限定名称,因为父类中被覆盖的是实例方法。企图将子类的实例强制转换成父类的实例再调用父类中被覆盖的方法也是不可行的,因为即使被强制转换,实例运行时刻所属的类仍然是不变的。
方法的覆盖是针对实例方法而言的,因此,子类的实例方法不能覆盖父类中的类方法,否则会产生编译错误。
另一个相关的概念是方法的隐藏,方法的隐藏是针对于类方法而言的。如果在一个类中定义了一个类方法,那么它将隐藏其父类中定义的所有与之具有相同名称和返回值类型的可访问的类方法。如果一个类方法企图隐藏父类中的实例方法,就会产生编译错误。在子类的类方法中同样可以使用带父类名的限定名称来调用父类中被隐藏的类方法;在子类的实例方法中还可以使用super关键字来访问父类中被隐藏的类方法;同时还可以使用类型转换将子类的实例转换成父类的实例来调用父类中被隐藏的方法,这种转换可以是赋值转换,也可以是强制类型转换。例如:
class classC{
static void x(){
System.out.println("x() in classC");
}
}
class classD extends classC{
static void x(){
System.out.println("x() in classD");
}
}
public class MethodHiding{
public static void main(String[] args){
classD d = new classD();
d.x();
((classC)d).x();
}
}
classD继承了classC,并且隐藏了classC的类方法x( ),这个程序的输出如下。
x() in classD
x() in classC
子类在隐藏了父类的成员变量或覆盖了父类的方法后,常常还要用到父类的成员变量,或在覆盖的方法中使用父类中被覆盖的方法以简化代码的编写,这时就要访问父类的成员变量或调用父类的方法。Java中通过super来实现对父类成员的访问。
Java中,this用来引用当前对象;与this 类似,super用来引用当前对象的父类。
super的使用有以下3种情况。
用来访问父类被隐藏的成员变量,如下所示:
super.variable;
用来调用父类中被覆盖的方法,如下所示:
super.Method ( [paramlist] );
用来调用父类的构造函数,如下所示:
super( [paramlist] );
【例4-3】通过本例来说明super的使用。
class superFoo{
int x = 1;
String name = "";
superFoo(String name){
this.name = name;
System.out.println("superFoo() called.");
}
String getName(){
return name;
}
}
class subFoo extends superFoo{
double x = 1.2;
String name = "";
subFoo(String supername, String subname){
super(supername);
name = subname;
System.out.println("subFoo() called.");
}
int getSuperX(){
return super.x;
}
String getName(){
return super.getName()+ "," + name;
}
}
public class superDemo{
public static void main(String[] args){
subFoo s = new subFoo("Super","Sub");
System.out.println("Super's x: " + s.getSuperX());
System.out.println(s.getName());
}
}
这个程序中,subFoo子类的name域隐藏了父类superFoo的name域,而其构造方法有两个参数,对第1个参数调用super( )传给父类的构造函数,然后把第2个参数赋值给自己的name域。subFoo类的方法getSuperX( )通过super.x访问父类的x值并返回。其getName( )方法则首先通过super.getName( )取得父类name域的值,然后和自己的name域构成一个字符串返回。这个程序的输出如下:
superFoo() called.
subFoo() called.
Super's x: 1
Super,Sub
final关键字是一个修饰符,标记一个类不能有子类,一个方法不能被覆盖,或者一个变量为一个常数。由final类派生子类,覆盖final方法,更改final变量的值,都将产生编译错误。
出于安全性或者是面向对象的设计上的考虑,有时候希望一些类不能被继承。例如,Java中的String类,它对编译器和解释器的正常运行有很重要的作用,不能对它轻易改变,因此把它修饰为final类,使它不能被继承,这就保证了String类型的唯一性。
使用final的另外一个好处是能让编译器作一些优化。Java程序的执行过程就是一个初始化对象和调用其方法的过程。其中对方法的调用花费了很多资源,这些资源都用来转移线程控制,传递参数,返回结果和创建用于存放本地变量及中间结果的帧栈(Frame Stack)。
由于方法的调用需要消耗大量的资源,因此,Java编译器可以将一些方法调用转化为代码嵌入(Inlining),就是将一段代码对一个方法的调用转化为将该方法的代码在编译时嵌入到调用处。这样,由于减少了方法的调用,就可以大大提高代码的性能。当将一个方法声明为final、static或private时,编译器就会自动使用代码嵌入技术将该方法代码在编译时嵌入到调用处。
【例4-4】final声明的例子如下。
class Foo {
final int value =3; //final变量
final void foo (int a,int b) { //final方法
}
}
试图覆盖final方法或者继承final类都将引起编译错误,如下所示。
class Foo {
final int value =3; //final变量
final void foo (int a,int b) { //final方法
}
}
class subFoo extends Foo{
void foo(int a, int b){
System.out.println("Override!");
};
}
final class Bar{}
class subBar extends Bar{
}
编译错误信息如下所示。
extendFinal.java:16: cannot inherit from final Bar
class subBar extends Bar{
^
extendFinal.java:9: foo(int,int) in subFoo cannot override foo(int,int) in Foo;
overridden method is final
void foo(int a, int b){
^
2 errors