1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > JavaSE基础知识(十八)--Java多态之向上转型(多态初步)

JavaSE基础知识(十八)--Java多态之向上转型(多态初步)

时间:2018-10-27 22:01:31

相关推荐

JavaSE基础知识(十八)--Java多态之向上转型(多态初步)

Java SE 是什么,包括哪些内容(十八)?

本文内容参考自Java8标准

再次感谢Java编程思想对本文的启发!

开篇一句很重要的话:在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征

Java面向对象程序设计的三大要素:封装,继承,多态(顺序不能乱)

⑴、封装:对应数据抽象,如何将现实中的事物抽象成一种数据类型(用代码表述)

"封装"通过合并特征(类变量)和行为(方法)来创建新的数据类型。其中"实现隐藏"则通过将细节"私有化"把接口和实现分离开来(这种分离的好处重点不在于应用,而是方便代码的维护)。

⑵、继承:对应类的继承extends、接口的实现implements(接口本质上也是一种类)

继承允许将对象视为它自己本身的类型或其父类型来加以处理。这种能力极为重要,因为它允许将多种类型(从同一个父类导出)视为同一类型来处理,而同一份代码也可以毫无差别地运行在这些不同类型之上了(框架代码)。

⑶、多态:对应多态

多态的作用则是消除类型之间的耦合关系(比如,框架代码只与餐具类型耦合,而不会与具体的餐叉、勺子、小刀耦合),它必须建立在继承关系之上。

"多态方法调用允许一种类型表现出与其它相似类型之间的区别,只要它们都是从同一父类导出而来的,这种区别是根据方法行为的不同而表现出来的,虽然这些方法都可以通过同一个父类来调用。"这几句话是Java编程思想中文第四版原书的翻译,个人觉得翻译的实在是有点蹩脚(可能翻译的人本身不是很懂吧),实际上这几句话要表达的意思是:多态允许同一个父类/父接口的方法在不同的子类中有不同的实现,根据你使用的子类不同,多态将表现出不同的行为(因为每个子类对这个方法的实现都不同)

只有将封装继承完全弄明白了,才能领悟到多态带来的好处。

多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。

多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序—即无论项目在最初创建时还是在需要添加新功能时都可以"生长"的程序

多态也称为"动态绑定"、“后期绑定”、"运行时绑定"

在前一篇博文中,我已经提到:对象既可以作为它自己本身的类型使用,也可以作为它的父类型(或根类型)使用,这种把某个对象的引用视为其父类型的引用的做法被称作向上转型(因为在继承树的画法中,父类是放置在上方的)

下面通过一个有关乐器的代码例子说明(因为乐器都要演奏乐符,所以单独创建一个乐符类):

代码示例:

// 向上转型//枚举类Note,代表乐符public enum Note{//包括了三种乐符MIDDLE_C,C_SHARP,B_FLAT;}//类Instrument,代表乐器类,但是不代表具体的乐器类型//它是所有乐器类的父类class Instrument{//方法play(),带一个Note类型的形式参数npublic void play(Note n){//打印字符串"Instrument.play()"System.out.println("Instrument.play()");}}//类Wind,代表具体的乐器(具体是什么乐器我百度了没查到),从父类Instrument继承class Wind extends Instrument{//重新实现了父类Instrument中的方法play()public void play(Note n){//打印字符串"Instrument.play()"System.out.println("Wind.play() "+n);}}//类Music,代表的是乐器的演奏public class Music{//方法tune(Instrument i),带一个Instrument类型的形式参数i//类似框架了,固定接受Instrument类型的参数(当然,我们已经知道,除了Instrument//类型本身,还接受它所有的子类对象)public static void tune(Instrument i){//调用Instrument类的方法play(Note n),传入了一个实际参数,//也就是一个乐符Note.MIDDLE_C。i.play(Note.MIDDLE_C);}//程序执行入口main方法public static void main(String[] args) {//创建Instrument类的子类Wind的对象.Wind flute = new Wind();//将对象引用传入方法tune(Instrument i)中tune(flute);}}

结果示例:

在上面的代码示例中,类Music的方法tune(Instrument i)接受一个类Instrument的引用(实际上,它还接受任何类Instrument子类对象的引用)。

在main()方法中,当将一个类Wind(类Instrument的子类)的对象引用传递到tune(Instrument i)方法时,虽然未经任何的转换,但是程序未报任何错误,也没有任何的警告,运行后还能得出满意的结果----这样做是允许的,因为Wind从Instrument继承而来,所以Instrument的接口(也就是方法play(Note n))必定存在于类Wind中。也就是说类Wind的对象也能调用方法play(Note n)(从Wind向上转型到Instrument可能会"缩小"接口,但不会比Instrument的全部接口更少)。

现在,我们发现,将子类对象传入了"框架"中,似乎"框架"代码故意忘记了它的具体类型(忘记它是具体的Wind类型,而是将它当成Instrument类型处理)。

上面代码示例中的Music.java看起来似乎有些奇怪,为什么它故意忘记对象(Wind对象)的类型呢?

实际上,在进行向上转型的时候,就会发生这种类似"忘记对象类型"的情况。

试想一下,如果让**tune(Instrument i)**方法直接接受一个Wind引用作为自己的参数(也就是将它的形式参数换成Wind i),似乎更加直观。但是这样引发的一个重要问题是:

如果那样做了,就需要为系统内Instrument的每种类型都编写一个新的tune()方法,如果现在加入Stringed(弦乐)和Brass(管乐)这两种Instrument(乐器):

那么,代码需要做出如下的改变:

代码示例:

// 增加几种类Instrument的子类//类Stringed继承类Instrument,代表具体的弦乐class Stringed extends Instrument{//重新实现了父类方法play(Note n)public void play(Note n){//打印字符串"Stringed.play()"System.out.println("Stringed.play() "+n);}}//类Brass继承类Instrument,代表具体的管乐class Brass extends Instrument{//重新实现了父类方法play(Note n)public void play(Note n){//打印字符串"Brass.play()"System.out.println("Brass.play() "+n);}}//类Music2,需要与类Music做对比public class Music2{//方法tune(Wind i),带一个Wind类型的形式参数i,此方法与具体的子类型Wind//高度耦合.调用此方法时,只能传入类Wind的对象。public static void tune(Wind i){//调用Wind类的方法play(Note n),传入了一个实际参数,//也就是一个乐符Note.MIDDLE_C。i.play(Note.MIDDLE_C);}//方法tune(Stringed i),带一个Stringed类型的形式参数i,此方法与具体的子//类型Stringed高度耦合.调用此方法时,只能传入类Stringed的对象。public static void tune(Stringed i){//调用Stringed类的方法play(Note n),传入了一个实际参数,//也就是一个乐符Note.MIDDLE_C。i.play(Note.MIDDLE_C);}//方法tune(Brass i),带一个Brass类型的形式参数i,此方法与具体的子类型Brass//高度耦合.调用此方法时,只能传入类Brass的对象。public static void tune(Brass i){//调用Brass类的方法play(Note n),传入了一个实际参数,//也就是一个乐符Note.MIDDLE_C。i.play(Note.MIDDLE_C);}//程序执行入口main方法public static void main(String[] args) {//创建类Wind的对象.Wind flute = new Wind();//将对象引用传入方法tune(Wind i)中tune(flute);//创建类Stringed的对象.Stringed violin = new Stringed();//将对象引用传入方法tune(Stringed i)中tune(violin);//创建类的对象.Brass frenchHorn = new Brass();//将对象引用传入方法tune(Brass i)中tune(frenchHorn);}//代码的运行结果实际上已经很清晰了,与特定的子类高度耦合,//各自调用各自对应类型的方法,得出的结果自然是各自类的结果。}

在上面的代码中,为每一种Instrument的子类都重载了一次方法tune()。

如果按照上面代码的方式,完全可以行得通,但是有一个主要的缺点:必须为每一个新Instrument子类编写特定类型的方法(有多少个子类就有多少个重载方法),这意味着在开始时就需要更多的编程,这也意味着如果以后想添加类似tune的新方法,或者添加Instrument的子类,仍然需要做大量的配套工作,此外,如果我们忘记重载某个方法。编译器并不会有任何错误提示信息返回,这样关于类型的整个处理过程就变得难以控制了。

所以,还是回到tune(Instrument i)方法的形式,我们只写这样一个简单方法,它仅接收父类作为参数,而不是那些特殊的子类(不与具体的子类耦合),也就是说,我们可以完全不管子类的存在以及子类的多少,编写的代码只与父类打交道。

以上的描述正是多态允许的。

再回到Music.java类中,运行程序后,我们便会发现,虽然tune(Instrument i)仅与父类Instrument打交道,但是,产生输出的结果却是Wind.play()的结果,这无疑是我们所期望的结果,这又是为什么呢?

再观察一下tune(Instrument i)方法:

// 多态方法tune(Instrument i)public static void tune(Instrument i){i.play(Note.MIDDLE_C);}

它接受一个Instrument引用,那么在这种情况下,编译器怎样才能知道这个Instrument引用实际指向的是Wind对象还是Stringed对象还是Brass对象呢?

实际上,编译器无法得知,因为在编译阶段,编译器只能知道它仅是一个Instrument类型,真正得知这个Instrument引用实际指向的是Wind对象还是Stringed对象还是Brass对象的时机是在运行期

为了更深入理解这个问题,接下来将研究"绑定"这个话题。

方法调用绑定

将一个方法调用同一个方法主体关联起来被称作绑定,若在程序执行前进行绑定(由编译器和连接程序实现),叫做前期绑定,可能你们以前从来都没有听说过这个术语,因为它是面向过程的语言中不需要选择就默认的绑定方式(非常常见的绑定方式),例如,C语言只有一种方法调用,就是前期绑定

但是在上述的代码示例中,如果用前期绑定的概念去理解,就会让人产生迷惑,因为,方法只与父类打交道,当编译器只有一个Instrument引用时,它不可能知道去调用哪个子类的方法。

想解决编译期代码只与父类打交道,但是在运行期能根据具体的子类得出结果的问题,只能通过后期绑定

后期绑定的含义就是在运行时根据具体的子类型对象进行绑定,后期绑定也叫做动态绑定运行时绑定

如果一种语言想实现后期绑定,就必须具有某种机制,以便能在运行期准确判断对象的类型,从而调用恰当的方法(多态指的就是在正确的时机调用了恰当的方法)。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要略微想一下就知道,不管怎样都必须在对象中安置某种"类型信息"。

Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定,这意味着通常情况下,我们不必判定是否应该进行后期绑定,因为在Java中,它(后期绑定)是自动发生的

为什么要将某个方法声明为static呢?正如前一篇博文提到的一样,它可以防止其他人覆盖该方法。但更重要的一点是:这样做,可以有效地"关闭"动态绑定,或者说,告诉编译器,不需要对这个方法进行动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。然而,在现在的技术环境里看来,这样做对提升程序的整体性能并没有什么显著的改观,所以,最好是根据设计来决定是否使用final(详情请参见有关我描述关键字final的博文),而不是出于试图提高性能的目的来使用final。

一旦知道Java中所有方法都是通过动态绑定实现多态这个事实之后。我们就可以编写只与父类打交道的程序代码了(因为在编译期不用知道类型,所以有父类型就好,运行期有后期绑定,可以识别正确的类型,实现多态),并且这些代码对所有的子类都可以正确运行。或者换一种说法,发消息给某个对象(实际上就是用父类搭建好发消息的框架),让该对象去判定应该做什么事情(实际上就是让运行期的子类对象来做事)。

下面通过具体的例子来说明

在面向对象程序设计中,有一个经典的例子就是"几何形状(Shape)",因为它很直观,所以经常用到。

在"几何形状"这个例子中,有一个父类Shape,以及多个子类:如Circle(圆)、Square(方形)、Triangle(三角形)等。这个例子之所以好用,是因为我们可以说"圆是一种几何形状",这种说法也很容易被理解。下面通过继承图来展示他们之间的关系:

向上转型可以像下面这条语句这么简单:

// 向上转型语句//将子类型的对象赋值给父类型的引用Shape s = new Circle();

在以上的代码中,创建了一个Circle对象,并立即把它赋值给Shape,这样做看似错误(将一种类型赋值给另一种类型),但实际上是没有问题的,因为通过集成,Circle就是一种Shape。因此,编译器认可这条语句,也不会产生错误信息。

假设你调用一个父类方法(这个方法已经在子类中被覆盖):

// 调用一个方法//调用的这个方法在子类中已经被覆盖s.draw();

你可能认为,调用的是Shape的draw(),因为毕竟是一个Shape的引用,但是,实际上调用的是Circle的draw(),这正是因为后期绑定(多态)。

你可以将所有的代码都写出来,再通过运行结果去证实。

再来看下另外的一个例子:

代码示例:

// 多态的另一个例子//父类Shapeclass Shape{//父类规定了行为,没有具体的实现//draw是绘画public void draw(){}//erase是擦除public void erase(){}//也就是说Shape包含了两种基本的行为,一种是绘画//另一种是擦除}//类Circle继承类Shapeclass Circle extends Shape{//重新实现了父类中的方法draw()public void draw(){//打印字符串"Circle.draw()"System.out.println("Circle.draw()");}//重新实现了父类中的方法erase()public void erase(){//打印字符串"Circle.erase()"System.out.println("Circle.erase()");}}//类Square继承类Shapeclass Square extends Shape{//重新实现了父类中的方法draw()public void draw(){//打印字符串"Square.draw()"System.out.println("Square.draw()");}//重新实现了父类中的方法erase()public void erase(){//打印字符串"Square.erase()"System.out.println("Square.erase()");}}//类Triangle继承类Shapeclass Triangle extends Shape{//重新实现了父类中的方法draw()public void draw(){//打印字符串"Triangle.draw()"System.out.println("Triangle.draw()");}//重新实现了父类中的方法erase()public void erase(){//打印字符串"Triangle.erase()"System.out.println("Triangle.erase()");}}//类RandomShapeGenerator,代表了Shape子类的随机生成器//能随机生成Shape类的子类对象class RandomShapeGenerator{//创建Java中的随机类Random的对象,随机数因子为43private Random rand = new Random(43);//运行一次能随机返回一个类Shape的子类的对象。public Shape next(){//switch语句,选择因子为rand.nextInt(3)计算式的结果switch(rand.nextInt(3)){//switch语句的默认执行default://如果rand.nextInt(3)计算式的值为0,返回Circle对象。case 0:return new Circle();//如果rand.nextInt(3)计算式的值为1,返回Square对象。case 1:return new Square();//如果rand.nextInt(3)计算式的值为2,返回Triangle对象。case 2:return new Triangle();}}}//类Shapespublic class Shapes{//组合类RandomShapeGenerator,创建类RandomShapeGenerator//的对象private static RandomShapeGenerator gen = new RandomShapeGenerator();//程序执行入口main方法public static void main(String[] args) {//创建了一个Shape类型的数组,一共是9个空间,每个空间都存储了一个//Shape类型的引用。Shape[] s = new Shape[9];//for循环,将数组s的9个空间的引用全部指向具体的对象。for(int i = 0;i<s.length;i++){//调用类RandomShapeGenerator的next()方法。//每调用一次就生成一个类Shape的子类对象。//赋值给数组的引用。s[i] = gen.next();}//循环数组的对象for(Shape shp : s){//调用每个对象的draw()方法。//这里能体现出多态的引用。shp.draw();}}}

结果示例:

Shape父类为它的所有子类建立了一个公用的接口(这个接口指的是所有子类都应该有的方法,本例中指的是draw()和erase())—也就是说,所有的Shape类的子类都可以绘画(draw())和擦除(erase()),子类再通过覆盖这些方法,给它们新的实现,从而为每种特殊的几何形状提供符合它们性质的行为。

类RandomShapeGenerator是一种"工厂",在我们每次调用next()方法时,它可以为随机选择的Shape引用关联一个实际的对象。

你需要注意的是,向上转型是在return语句里发生的。每个return语句取得一个指向某个Circle,Square、或者Triangle的引用,并将其以Shape类型从next()方法中发送出去。所以无论我们在什么时候调用nect()方法时,是绝对不可能知道具体类型到底是什么的(因为我们只能获得一个通用的Shape类型的引用,next()方法的返回类型是Shape类型)。

main()方法包含了一个Shape类型引用组成的数组,通过调用RandomShapeGenerator.next()来填入数据。此时,我们只知道自己拥有一些Shape,除此之外,你不会知道更具体的情况(实际上,编译器也不知道,它只知道Shape),然而,当我们遍历这个数组,并为每个数组元素调用draw()方法时,与类型有关的特定行为会神奇般地正确发生,我们可以从运行该程序时所产生的输出结果中发现这一点(结果中全是与特定子类有关的行为,并没有类Shape的draw()方法执行的痕迹)。

随机选择几何形状是为了让大家理解:在编译时,编译器不需要获得任何特殊信息就能进行正确的调用(因为随机选择,事先不知道任何特性类型的信息)。对draw()方法的所有调用都是通过动态绑定进行的。

可拓展性

现在,我们再返回到"乐器(Instrument)"示例,由于有多态机制,我们可以根据自己的需求对系统添加任意多的新类型,而不需要更改tune(Instrument i)方法。

在一个设计良好的OOP(面向对象设计)程序中,大多数或者所有方法都会遵循tune(Instrument i)的模型:只与父类接口通信

这样的程序是可拓展的,因为可以从通用的父类继承出新的数据类型,从而添加一些新功能。而tune(Instrument i)方法体(也就是框架代码)不需要任何改动就可以直接应用于新类(直接享受新类带来的新功能)。

那么,对于乐器(Instrument)例子,如果我们在父类(instrument)中添加更多的方法,并加入一些新类,将会出现什么情况呢?

如图:

事实上,不需要改动tune(Instrument i)方法,所有的新类都能正确运行。

即使tune(Instrument i)方法是单独存放在某个文件中,并且在Instrument接口中添加了其他的新方法,tune(Instrument i)也不需要再编译就能正确运行。

下面是具体的代码示例:

// 代码示例//父类Instrumentclass Instrument{//方法play(Note n),带一个Note类型的形式参数.void play(Note n){//打印字符串"Instrument.play()"+nSystem.out.println("Instrument.play()"+n); }//方法what()String what(){//返回字符串"Instrument"return "Instrument"; }//方法adjust()void adjust(){//打印字符串"Adjusting Instrument"System.out.println("Adjusting Instrument"); }}//类Wind继承类Instrumentclass Wind extends Instrument{//方法play(Note n),带一个Note类型的形式参数.void play(Note n){//打印字符串"Wind.play()"+nSystem.out.println("Wind.play()"+n); }//方法what()String what(){//返回字符串"Wind"return "Wind"; }//方法adjust()void adjust(){//打印字符串"Adjusting Wind"System.out.println("Adjusting Wind"); }}//类Percussion继承类Instrumentclass Percussion extends Instrument{//方法play(Note n),带一个Note类型的形式参数.void play(Note n){//打印字符串"Wind.play()"+nSystem.out.println("Percussion.play()"+n); }//方法what()String what(){//返回字符串"Percussion"return "Percussion";}//方法adjust()void adjust(){//打印字符串"Adjusting Percussion"System.out.println("Adjusting Percussion"); }}//类Stringed继承类Instrumentclass Stringed extends Instrument{//方法play(Note n),带一个Note类型的形式参数.void play(Note n){//打印字符串"Stringed.play()"+nSystem.out.println("Stringed.play()"+n); }//方法what()String what(){//返回字符串"Stringed"return "Stringed"; }//方法adjust()void adjust(){//打印字符串"Adjusting Stringed"System.out.println("Adjusting Stringed"); }}//类Brass继承类Wind,这里你需要注意了//类Brass继承的是类Instrument的子类,没有直接继承//类Instrument.算间接继承,但是它仍然是类Instrument的子类class Brass extends Wind{//方法play(Note n),带一个Note类型的形式参数.void play(Note n){//打印字符串"Brass.play()"+nSystem.out.println("Brass.play()"+n); }//方法adjust()void adjust(){//打印字符串"Adjusting Brass"System.out.println("Adjusting Brass"); }}//类Woodwind继承类Wind,这里你需要注意了//类Woodwind继承的是类Instrument的子类,没有直接继承//类Instrument.算间接继承,但是它仍然是类Instrument的子类class Woodwind extends Wind{//方法play(Note n),带一个Note类型的形式参数.void play(Note n){//打印字符串"Woodwind.play()"+nSystem.out.println("Woodwind.play()"+n); }//方法what()String what(){//返回字符串"Woodwind"return "Woodwind"; }}//类Music3public class Music3{//方法tune(Instrument i),带一个Instrument类型的形式参数ipublic static void tune(Instrument i){//...//调用方法play(Note n)i.play(Note.MIDDLE_C);}//方法tune(Instrument i),带一个Instrument[]数组类型的形式参数epublic static void tuneAll(Instrument[] e){//...//遍历数组中的对象for(Instrument i:e){//将每个对象都作为实际参数传入方法tune(Instrument i)中tune(i);}}//程序执行入口main方法public static void main(String[] args) {//创建Instrument类型的数组orchestraInstrument[] orchestra = {//Wind对象,是类Instrument的直接子类。//在这里自动完成了向上转型new Wind(),//Percussion对象,是类Instrument的直接子类。//在这里自动完成了向上转型new Percussion(),//Stringed对象,是类Instrument的直接子类。//在这里自动完成了向上转型new Stringed(),//Brass对象,是类Instrument的子类Wind的子类。//等于是类Instrument的间接子类//在这里自动完成了向上转型new Brass(),//Woodwind对象,是类Instrument的子类Wind的子类。//等于是类Instrument的间接子类//在这里自动完成了向上转型new Woodwind()//虽然以上对象中既有直接子类,又有间接子类,//但是都是Instrument类型,都可以放入Instrument类型的数组中};//调用方法tuneAll(Instrument[] e),将数组orchestra//作为实际参数传入。tuneAll(orchestra);}}

结果示例:

新添加的方法what()返回一个带有类描述的String引用,另一个新添加的方法adjust()则提供每种乐器的调音方法。

在main()中,当我们将对象的引用(类Instrument直接子类和间接子类的对象引用)置入orchestra数组中,它们都会自动向上转型成Instrument。

从示例代码的执行结果中可以看到,tune(Instrument i)方法完全可以忽略它周围代码所发生的变化,依旧正常运行,这正是我们期望的多态所具有的特性。

我们所做的代码修改,不会对程序中其他不应该受到影响的部分产生破坏。换句话说,多态是一项让程序员"将改变的事物与未变的事物分离开来"的重要技术。

PS:时间有限,有关Java SE的内容会持续更新!今天就先写这么多,如果有疑问或者有兴趣,可以加QQ:2649160693,并注明CSDN,我会就博文中有疑义的问题做出解答。同时希望博文中不正确的地方各位加以指正。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。