oop特性——向上转型(番外)

xiaoxiao2021-02-28  157

OOP特性——向上转型(番外)

前言

  在继承和多态中,我们都涉及了一个概念:向上转型(Upcast) 我们在《OOP特性——继承以及复用类的详解》中简单的介绍了向上转型的概念

简单的说在上面的继承中我们谈到继承是is-a的相互关系,在上述的例子中,学生继承与人,所以我们可以说学生是人,或者说学生是属于人的。这样将学生看做人就是向上转型。或者说: 向上转型是从一个较专用类型向较通用的类型转换。

现在我想通过对一个具体的例子的说明来加强我对向上转型的理解。

本文内容: * 前言 * 向上转型的使用 * 为何使用向上转型 * 新的问题以及后期绑定 * 补充说明 * 向下转型及instanceof

向上转型的使用

  首先,我们需要一把乐器(Instrument),乐器的功能是演奏(play)出乐符(Note)。而乐器又分为管乐,弦乐、鼓乐等等。 先创建一个Note枚举类型,包括中音,高音,低音:

package ch8_1; public enum Note { //枚举类型 乐符:中音 高音 低音 MIDDLE_C,C_SHARP,B_FLAT; }

  然后写一个乐器类,在其中添加了演奏功能:

package ch8_1; public class Instrument { //是乐器,就有演奏功能 public void play(Note n){ System.out.print("来自乐器的演奏功能"); } }

  然后我要创建一个管乐,它是属于乐器的一种,因此继承了乐器。也有演奏功能,但是与父类的相比,传进了一个参数,,是Note乐符类型的一种音符n:

package ch8_1; public class Wind extends Instrument{ //管乐继承乐器,对演奏方法进行了重写 //传进来一个参数,是Note乐符类型的n public void play(Note n){ System.out.print("来自管乐的演奏"+n); } }

  在有了这些准备工作之后,我们就可以来享受Music了。若要享受Music,我们得需要一种乐器来演奏,所以添加一个静态方法tune,传进一个乐器对象(父类)。在这个方法中,乐器(父类)调用了其play方法。在我们测试时,我们实例化了一个管乐的对象:长笛 flute,然后在静态方法tune()中传进了一个管乐的对象,而不是参数列表中所规定的乐器(父类)对象。

package ch8_1; public class Music { //一个静态方法,tune曲调,传进一个参数,用一种乐器演奏 public static void tune(Instrument i){ i.play(Note.MIDDLE_C); } public static void main(String args[]){ //实例化一个管乐的对象 Wind flute=new Wind(); //静态方法tune传进了一个管乐的对象, tune(flute); } }

OutPut:

来自管乐的演奏MIDDLE_C

我们来分析一下:   Music.tune()方法接受了一个Instrument的引用对象,那么同时也接受任何Instrument的子类对象。因此在main()方法中,当一个Wind引用传递到tune()方法时,不需要任何类型转换。因为Wind继承自Instrument,所以Instrument的接口必定存在于Wind中。这就是Wind向上转型到Instrument。

以上就是向上转型的的使用。

为何使用向上转型

  但是就像我第一次接触到向上转型这一概念的时候想的那样:为什么所有人要故意忘记对象的类型呢?为什么不让tune()方法直接调用Wind引用作为自己的参数呢?这样不是更直观么?为什么要多此一举的弄出向上转型呢?

  面对当时的四连问,现在的我可以给出这样的答案:

  如果Instrument类只有一个子类,确实没有必要这样多此一举,因为此时没有任何使用向上转型的理由。   但是! 但是!重点来了:一个基类绝非只有一个子类,而如果不采取向上转型,我们的代码就会存在很多缺陷和隐患!比如:每新增加一个新的子类,就必须要把方法重新写一遍,而如果我们忘记重载了某个方法,编译器是不会提醒的!

  比如按照我之前很朴素的想法来看,现在再加入Stringed和Brass这两种乐器,代码如下:

package ch8_2; Class Instrument{ public void play(Note n){ System.out.println("来自乐器的演奏"); } }

  下面我要添加管乐,弦乐,铜管乐三种乐器,均继承于乐器

package ch8_2; public class Wind extends Instrument{ public void play(Note n){ System.out.println("来自管乐的演奏"+n); } } package ch8_2; public class Stringed extends Instrument{ public void play(Note n){ System.out.println("来自弦乐的演奏"+n); } } package ch8_2; public class Brass extends Instrument{ public void play(Note n){ System.out.println("来自铜管乐的演奏"+n); } }

  那么现在我不使用向上转型,要为我添加的每一种乐器编写特定的方法了:

package ch8_2; public class Music{ public static void tune(Wind i){ i.play(Note.MIDDLE_C); } public static void tune(Stringed i){ i.play(Note.MIDDLE_C); } public static void tune(Brass i){ i.play(Note.MIDDLE_C); } public static void main(String args[]){ Wind flute=new Wind(); Stringed violin=new Stringed(); Brass frenchHorn =new Brass(); tune(flute); tune(violin); tune(frenchHorn); } }

  很明显,代码量增加了,并且仅添加三个类我们一般不会出错,但是一旦我们忘记了重载某个方法,编译器还不会提示,那就麻烦了!

  正因为如此,我们需要用一个简单方法,它仅接收基类作为参数,而不是那些特殊的子类。也就是说,如果我们不管子类 的存在,编写的代码只与基类打交道。这就是我们为什么需要向上转型。

新问题的提出以及动态绑定

  那么在我们明确了为什么使用向上转型之后,一个新问题随之而来,首先让我们回到最开始的代码:

public static void tune(Instrument i){ i.play(Note.MIDDLE_C); }

  可能有些人要问:tune()接受了一个Instrument引用,那编译器怎么才知道这个Instrument引用是Wind对象,还是Stringed对象,或者是Brass对象呢?

  首先我们要明确一个概念: Java中除了static方法和final方法之外,其他的所有方法都是后期绑定。

  所谓的绑定,是指一个方法调用同一个方法主题关联起来。在程序执行前进行绑定,叫做前期绑定;在运行时根据对象的类型进行绑定,也叫做后期绑定,也叫做动态绑定或运行时绑定。 也就是说,编译器一直不知道对象的类型,但是方法调用机制来盘对对象的类型,从而找到正确的方法体,并加以调用。

补充说明

  此处补充说明一下《 OOP特性——继承以及复用类的详解》中关于final的一个问题:final可以防止其他人覆盖方法。但更重要的也许是有效地“关闭”动态绑定。

向下转型及instanceof

向上转型和向下转型同为多态性的两种主要类型:

向上转型:子类对象->父类对象 向下转型:父类对象->子类对象

对于向上转型,也就是在继承层次中向上移动,编译器会自动完成,但是会丢失具体的类型信息。

父类 父类对象 = 子类实例;

向下转型,也就是在继承层次中向下移动,父类转化为子类的类型,这时候java向下转型需要强制类型转换。

子类 子类对象 = (子类)父类实例;

  此处划一个重点:要想向下转型,则必须先向上转型,否则会报错的。而向下转型之后,之前向上转型之后失去的方法则全部找回。

关键字instanceof

  这个关键字是Java程序在运行时判断对象的类型是否是属于特定类的一个实例或者是否是该特定类的一个子类,instance of通过返回一个boolean值来判读正确与否。

转载请注明原文地址: https://www.6miu.com/read-18341.html

最新回复(0)