第11章继承和多态11.1介绍(Introduction)面向对象编程允许从已有的类派生出新类,这叫继承(Inheritance)。
继承是软件代码重用的一种机制,是一种在面向对象编程中非常重要的,而且强大的特性。
假设已经定义了一个圆形,矩形和三角形的模型类,这些类有许多相同的特征,如何设计才能避免冗余,而且还能使系统易于理解和维护?答案就是使用继承。
11.2超类和子类不同的类可能包含一些相同的,公共的特性和行为,把这些相同的东西组合在一起形成一个新的公共类来被其他类共享。
继承就是定义一个一般类,然后扩展这个一般类形成更多的特殊类。
这些特殊类继承了一般类的某些属性和操作。
这些类的对象我们可以称为几何对象,那么就创建一个称为几何类的一般类,这个一般类包含几何元素中的一些公共的属性和操作,如可以填充元素,修改线条的颜色,或撤销填充等。
因此一般类GeometricObject可以作为所有几何对象的一般类模型。
如图一般类的UML图,以及特殊类之间的关系图。
在Java术语中,一个类C1是从C2扩展来的,那么C1类称为子类,C2类称为父类或超类。
子类继承父类可访问的数据和方法,同时可以扩展出自己的新的数据和方法如上图所示。
代码如下。
思考一下,如下定义的构造方法是否正确?为什么?答案是否定的,原因就是子类不能访问父类的私有的数据,但可以通过调用父类的get或set方法来访问它。
关于继承的几点注意:1.子类不是超类的一个子集,实际上子类包含父类,并对其进行了扩展,内容比父类更丰富。
2.父类中私有的数据不能被它之外的任意类访问。
因此,它们不能在子类中被直接使用。
但是,如果在父类中定义了setter或getter,可以通过使用它们来访问这些数据。
3.并不是所有的is-a的关系都是继承关系,例如,一个方形是一个矩形,但却不能使用方向矩形来扩展方向,因为没有什么可扩展的。
但却可以从几何类中进行扩展定义方形类。
因为新的子类要比父类包含更多的详细信息。
4.继承被用来模型化is-a的关系。
不要盲目的为了重用方法而扩展类。
例如,没必要把一个树类扩展为人类,尽管二者具有很多相同的属性,比如高度和重量。
子类和父类之间必须存在is-a的关系。
5.很多其他的编程如c++允许一个子类可以从几个父类扩展和继承,称多重继承,但Java不允许多重继承,即严格的单继承。
即一个类的声明中只能有一个关键字extends,而且后面只有一个类名。
如果想要拥有若干个类的特性,可以实现接口(在14章讲授)。
11.3supper关键字子类继承了父类可访问的数据和方法,它是否继承构造方法呢?父类的构造方法可以在子类中被调用吗?在前面一章中,介绍过一个关键字this,表示对象自己。
Super关键字则指它的父类,有2种方式被使用。
1.调用父类构造方法。
2.调用父类的其他方法11.3.1 调用父类构造方法调用父类的构造方法的格式如下:其中super()调用父类无参数的构造方法,super(argument)调用父类和参数argument匹配的构造方法。
但需注意的是,不管是使用哪条语句,调用父类的构造方法语句必须放在子类定义的构造方法之前,并且只能显示的调用父类的构造方法。
如下语句是正确的。
父类的构造方法必须使用super关键字调用。
构造方法被用来创建类的实例,不像属性和方法,父类的构造方法是不能继承的,只能使用super通过在子类中调用父类的构造方法。
11.3.2 构造链构造方法可以调用重载的构造方法,或其父类的构造方法。
如果二者都没显式调用,编译器会自动在子类构造方法前添加super(),如下图所示。
在任何情况下,利用构造方法创建一个类的实例时,会沿着它的继承链向上所有的父类的构造方法都要被调用。
当构造一个子类对象时,子类构造方法先调用父类的构造方法,然后才能履行自己的其他任务。
如果这个父类又是由另外一个类派生的,那么它父类的构造方法在调用前,先要调用它祖父的构造方法,如此,直到最终没有父亲的类的构造方法被调用,然后返回。
这条构造方法的执行和返回的过程称构造链,如下代码。
如果一个类设计成被继承的类,即一般类,那么最好在类中提供无参数的构造方法来避免编程中的错误。
如下代码所示。
因为在Apple类中没有显式的定义,那么Apples的默认的无参数的构造方法会被添加上。
既然Apple是Fruit 的子类,那么在调用构造方法的时候,会先调用父类的构造方法,但Fruit没定义无参数的构造方法,系统不会为Fruit添加,因此这个程序不会被编译,会产生语法错误。
因此如果有可能,为了避免出错,在定义任何一个类时,最好提供无参数的构造方法。
11.3.3 调用父类方法关键字super也可以用来引用除构造方法之外的其他方法,如下语法:如下代码:11.4重写(Overriding)方法子类会从父类继承方法,有时,从父类继承的方法在子类中非常有必要被修改会改进来适应新类的需要。
这种对继承来方法的改写或改造称方法重写,也称方法覆盖。
例如GeometricObject类中的toString方法,返回一个代表几何对象的字串。
这个方法在Circle类中可以被重写成返回代表圆对象的字串,如下程序所示。
那么在使用的过程中是调用父类定义的方法还是子类重写的方法呢?在Circle类中,两个方法都可以被调用,使用的是具体哪个方法,根据语句的位置和具体语句的样式决定。
如果像一般调用方法一样使用这个方法,则使用的是改写的方法,如果想再Circle类中使用父类定义的toString方法,则要使用super.toString()才能调用。
那么能否以如下方式在Circle的子类中调用GeometricObject中定义的toString方法呢?答案是错误的,这会发生语法错误,关于重写,有以下2点值得注意:1.只有可访问的实例方法才能被重写,即私有方法不能被重写(因为在外类中根本不可见),如果同一个方法在父类中被定义成了私有的方法,而在子类中又被定义了,那么这2个方法没有关系,不是重写。
2.如实例方法一样,一个静态方法也可以被继承,但却不能被重写。
如果一个在父类方法中被定义为静态的,又被定义在子方法中,那么在父类中定义的那个静态方法会被隐藏,可以通过父类名来引用父类的静态被隐藏的方法。
11.5重写和重载重载意味着定义若干个名字相同,但参数和内容不同的方法。
重写意味着在子类中对继承的方法进行改写。
为了重写方法,方法的名字一定要和父类中定义的方法名相同,相同的参数列表和相同的返回类型。
举例来说明重载和重写的不同,如下图所示。
11.6Object类和toString()方法Java中的每个类都是Object类的子类,如果没有继承声明,那么在定义一个类的时候,这个类默认就是Object 类的直接子类,如下所示。
之前使用过的诸如String,Loan和GeometricObject都是Object的直接子类。
可以任何类中使用Object类中提供的方法,下面将介绍Object类的toString方法,头部描述如下。
对象调用此方法返回一个关于对象的描述,默认返回一个字符串的组合,包含对象实例的类名字@对象的十六进制内存地址,如下Loan类对象调用toString方法。
显示如Loan@1537e5之类的信息,这个信息并没多大用处,通常在新的子类中要重写这个方法,如下在GeometricObject中重写。
注意:可以直接使用System.out.print(object)调用,与System.out.print(object.toString())等价。
即System.out.print(Loan.toString())与System.out.print(Loan)等价。
11.7多态(Polymorphism)面向对象的三大特性就是封装,继承和多态,前2个都已经了解,这节介绍多态。
定义2个词汇:subtype和supertype。
一个类实际上就是定义一个类型,如果一个类被定义为某个类的子类,则称它为subtype,它的继承类称为supertpye。
继承关系使得子类继承父类的某些特性,同时可以添加自己独特的特征。
子类是父类的特例,每一个子类的实例也都是父类的实例,但反之不然。
例如,每个圆都是几何图像类的对象,但不是每个几何图像都是圆。
因此,总是可以把子类的一个实例作为参数传递给父类参数存在的任何位置,如下例。
一个子类的对象可以被用在父类对象出现的任何地方。
这就是普通意义的多态(Many forms),简单的说,多态就是父类型的变量可以被子类型对象赋值,即父类型变量指向子类型对象。
11.8动态绑定(Dynamic binding)一个方法可以被定义在父类中,并且在子类中被重写。
例如Object类中定义的toString方法,在GeometricObject被重写了,请看如下代码。
那么哪个toString方法会被对象o调用?重写的还是Object类中定义的?要回答这个问题,先要介绍2个概念:声明类型(declared type)和实际类型(actrual type)。
一个变量必须有声明类型,o对象声明类型为Object类型。
引用数据类型的变量可以指向一个null值或一个声明类型的实例,而这个实例可以是声明类型创建的,也可以是声明类型的子类创建的。
实际类型变量是对象实际指向的实例类型。
o的实际类型是GeometricObject类型。
对象o的toString方法是根据实际类型调用的,这就是所谓的动态绑定。
动态绑定的工作方式:假设一个对象o 是C1,C2,…,Cn类的实例,这里C1是C2的子类,C2是C3的子类,以此类推,如图所示。
如果对象o调用方法p,那么JVM就会按C1…Cn的顺序搜索p方法,一旦找到即执行,搜索停止。
匹配方法和绑定方法的实施是2个分开的步骤。
声明类型是决定在编译时那个方法会被匹配。
编译器在编译时根据参数类型,个数和顺序找到匹配的方法。
JVM在运行时动态的绑定方法的实施,决定变量的实际类型。
11.9对象类型强制转换和instanceof操作符已经清楚掌握基本数据类型的强制转换。
对于引用数据类型的类类型也可以进行强制类型转换,但有条件,只能发生在继承与被继承的类之间(直接或间接均可)。
如语句:m(new Student());实际上把new Student()这个匿名Student对象传递赋值给Object类型声明的变量,语句等价如下:直接把子类对象赋给父类声明的对象是允许的,即,此时子类对象会自动转换为父类对象为其赋值。
但假设我们想把Object对象o赋值给Student类型对象,如Student b=o; 就会发生编译错误。
为什么?原因是Student 实例对象同时也是Object实例对象,反过来,Object对象实例却不一定都是Student对象实例。