——Java培训、Android培训、iOS培训、.Net培训 期待与您共同交流!——
继承和重写(extends&override)
1. 继承
1.1. 泛化的过程
前面的案例中定义了T类和J类, 通过分析可以发现, 在这两个类中存在着大量的重复代码,像cells属性、print方法、drop方法、moveLeft方法、moveRight方法,在这两个类中都存在,并且实现上基本也是相同的,本着代码重用的原则,可以使用继承的方式来实现。
首先,构建T类和J类的父类Tetromino类,将公共的(T类和J类公有的)信息存放在父类中, T类和J类继承Tetromino父类。此时,子类即可以共享父类的数据。这个过程就是泛化的过程。
1.2. extends关键字
使用继承可以实现代码的重用,在java语言中,需要通过extends关键字实现类的继承。继承完成后,子类(Sub class)可以继承父类(Super class)的成员变量及成员方法,同时子类也可以定义自己的成员变量和成员方法。届时,子类将具有父类的成员及本类的成员。
需要注意的是,Java语言不支持多重继承,即:一个类只能继承一个父类,但一个父类可以有多个子类。看下面的代码:
publicclass Tetromino {
Cell[] cells;
publicTetromino(){
cells =new Cell[4];
}
public void drop(){//同写过的T类 }
public void moveLeft(){//同写过的T类}
public void moveRight(){//同写过的T类}
public void print(){//同写过的T类}
}
publicclass TetrominoT extends Tetromino {
publicTetrominoT(int row, int col){
cells[0]=newCell(row, col);
cells[1]=newCell(row, col +1);
cells[2]=newCell(row, col +2);
cells[3]=newCell(row +1, col +1);
}
}
如上代码说明:声明父类Tetromino,将公共信息放在其中,包括Cell[]声明、drop()方法、moveLeft()方法、moveRight()方法,print()方法。声明无参构造函数,对成员变量Cell数组进行实例化。声明子类TetrominoT继承Tetromino,并声明有参构造函数,传递行row,列col参数,进行T型数组元素的初始化。
下面在main方法中,声明一个T型对象,即可以实现T型对象的构建:
TetrominoT t =newTetrominoT(1,1);
上面的代码,在创建子类对象时,调用了子类的有参构造函数进行数据的初始化,试想下,父类Tetromino的无参构造函数执行了吗?通过分析可以肯定的是,父类的无参构造函数被执行了。在程序中并没有声明父类的构造函数,那它是如何执行的呢?
1.3. 继承中构造方法
父类的无参构造方法之所以被执行,是因为java规定,子类在构造之前必须先构造父类。
事实上,子类的构造方法中是必须要通过super关键字来调用父类的构造方法的,这样才可以保证妥善的初始化继承自父类的成员变量。
但是看上一个案例中的代码并没有super调用父类构造方法,那是因为,如果子类的构造方法中没有调用父类的构造方法,则java编译器会自动的加入对父类无参构造方法的调用。请看如下代码,演示了super关键字的用法:
publicTetrominoT(int row, int col){
super();
cells[0]=newCell(row, col);
cells[1]=newCell(row, col +1);
……
}
上面的代码中,super();为编译器自动加入的,并且super关键字必须位于子类构造方法的第一行,否则会有编译错误。
另外一点需要注意的是,若父类没有提供无参的构造方法,则会出现编译错误。请看如下的示例:
class Foo {//父类
int value;
Foo(int value){
this.value = value;
}
}
class Goo extends Foo {//子类
int num;
Goo(int num){
this.num = num;
}
}
分析上面的代码,在子类构造方法中没有写super调用父类构造方法,这时编译器会默认添加super()来调用父类的无参构造方法,但是父类中又没有定义无参的构造方法,因此会发生编译错误。
针对上面的问题,可以有两种解决方案,方案一为在父类中添加无参的构造方法,方案二为在子类构造方法中显示调用父类的有参构造方法(常常使用),这样可以保证父类的成员变量均被初始化,参见下面的代码:
class Goo extends Foo {
int num;
Goo(int value, int num){
super(value);
this.num = num
}
}
如上的代码,在子类中调用了父类的构造方法,初始化了继承自父类的value成员变量,编译正确。
1.4. 父类的引用指向子类的对象
一个子类的对象可以向上造型为父类的类型。即,定义父类型的引用可以指向子类的对象。看如下代码所示:
class Foo {
int value;
public void f(){…}
Foo(int value){
this.value = value;
}
}
class Goo extends Foo {
int num;
public void g(){…}
Goo(int value, int num){
super(value);
this.num = num
}
}
class Test{publicstatic void main(String[] args){
Foo obj =newGoo(100,3);
}
}
上面的代码,在main方法中,声明了父类型的引用来指向子类型的对象。但是通过父类的引用只能访问父类所定义的成员,而不能访问子类所扩展的部分。看下面的代码:
class Foo {
int value;
public void f(){…}
… … …
}
class Goo extends Foo {
int num;
public void g(){…}
… … …
}
class Test{
publicstatic void main(String[] args){
Foo obj =newGoo(100,3);
obj.value=200;
obj.f();
obj.num =5;
obj.g();
}
}
分析上面的代码,在main方法中,声明父类型的引用指向了子类的对象,而后,访问父类的成员变量value及调用父类的方法f,均可以正常编译。但是,当通过obj引用访问num变量及g的方法时,会出现编译错误。那是因为,当用父类型引用指向了子类对象后,java编译器会根据引用的类型(Foo),而不是对象的类型(Goo)来检查调用的方法是否匹配。
2. 重写
2.1. 方法的重写
下面增加需求,在输出图形之前先打印格子坐标,即调用print()方法。想实现这个需求做法很简单,只需要父类型引用直接调用print()方法即可,因为print()方法是在父类中定义的,所以可以直接调用此方法。
现在需求继续增加,要求,不同的图形类型在打印输出之前先输入相应的语句,例如: TetrominoT对象调用print()方法后,增加输出“I am a T”,TetrominoJ对象调用print()方法后,增加输出“I am a J”。因为现在print()方法是在父类中定义的,只有一个版本,无论是T类对象还是J类对象调用,都将输出相同的数据,所以现在无法针对对象的不同而输出不同的结果。若想实现此需求,需要介绍一个概念,叫做方法的重写。
在java语言中,子类可以重写(覆盖)继承自父类的方法,即方法名和参数列表与父类的方法相同,但是方法的实现不同。
当子类重写了父类的方法后,该重写方法被调用时(无论是通过子类的引用调用还是通过父类的引用调用),运行的都是子类重写后的版本。看如下的示例:
class Foo {
public void f(){
System.out.println("Foo.f()");
}
}
class Goo extends Foo {
public void f(){
System.out.println("Goo.f()");
}
}
class Test{
publicstatic void main(String[] args){
Goo obj1 =newGoo();
obj1.f();
Foo obj2 =newGoo();
obj2.f();
}
}
分析代码得出结论:输出结果均为“Goo.f()”,因为都是Goo的对象,所以无论是子类的引用还是父类的引用,最终运行的都是子类重写后的版本。
2.2. 重写中使用super关键字
在子类重写的方法中,可以通过super关键字调用父类的版本,参见如下的代码:
class Foo {
public void f(){
System.out.println("Foo.f()");
}
}
class Goo extends Foo {
public void f(){
super.f();
System.out.println("Goo.f()");
}
}
class Test{
publicstatic void main(String[] args){
Foo obj2 =newGoo();
obj2.f();
}
}
上面的代码中,super.f()即可以调用父类Foo的f()方法,此程序输出结果为:Foo.f() Goo.f()。这样的语法通常用于子类的重写方法在父类方法的基础之上进行的功能扩展。
2.3. 重写和重载的区别
重载与重写是完全不同的语法现象,区别如下所示:
重载: 是指在一个类中定义多个方法名相同但参数列表不同的方法,在编译时,根据参数的个数和类型来决定绑定哪个方法。
重写: 是指在子类中定义和父类完全相同的方法,在程序运行时,根据对象的类型(而不是引用类型)而调用不同的方法。
分析如下代码的输出结果:
class Super {
public void f(){
System.out.println("super.f()");
}
}
class Sub extends Super {
public void f(){
System.out.println("sub.f()");
}
}
class Goo {
public void g(Super obj){
System.out.println("g(Super)");
obj.f();
}
public void g(Sub obj){
System.out.println("g(Sub) ");
obj.f();
}
}
class Test{
publicstatic void main(String[] args){
Super obj =newSub();
Goo goo =newGoo();
goo.g(obj);
}
}
分析如上代码,输出结果为:g(Super) sub.f()
。
首先,重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法, 因为变量obj为Super类型引用, 所以,Goo的g(Super)被调用,先输出g(Super)。
重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法,因为obj实际指向的是子类Sub的对象,因此,子类重写后的f方法被调用,即sub.f()。
版权声明:本文为博主原创文章,未经博主允许不得转载。