JavaSE基础知识(五)--面向对象代码实现收尾(多态概述)

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

本文内容参考自Java8标准

一、面向对象(多态概述):

在处理类型的结构层次时,经常想把一个对象不当作它所属的特定类型来对待,而是将其当作其基类对象来对待(这里需要举一个例子,在继承的博文中提到了,每种类型一定会有一个基类,比如鸟类,有了基类之后,特定的鸟类可以直接通过继承基类来实现,比如黄鹂鸟,那么问题来了,如果在接下来的一个业务逻辑代码中,涉及到了鸟类,那么是使用鸟类基类的代码,还是使用黄鹂鸟的代码?如果使用了黄鹂鸟类的代码,那么说明这段代码仅对黄鹂鸟类有效,下次遇到其他鸟类,比如喜鹊,那么肯定是另外一段业务逻辑代码了,也就是说,针对每一种特定类型的鸟类,都需要有一段特定的业务逻辑代码与他们一一对应?这种想法可以实现,但是非常难管理。因为如果有1000种特定类型的鸟类,你将会有1000段业务逻辑代码,事实上,这1000段业务逻辑代码会有很大一部分是相同的,这个想法行不通,还有别的办法吗?有的,还是回到继承博文中的内容,子类和基类产生差异的第二条覆盖,在纯粹替代中,子类和基类是拥有同样的接口的,子类并没有新增的方法,只是实现不同。那么,能不能这样:所有的业务逻辑代码都与基类代码交互,实际上也就是与基类的接口交互,但是在运行时使用子类的实现!可以的,在Java中这种操作有一个称呼:多态多态实现的基础就是子类和基类拥有完全一样的接口,只是实现不同,而之前,我们已经实现了接口与实现分离,所以代码交互时统统与基类交互,在运行时,自动使用子类的实现,说白了,就是调用基类的一个方法,这个方法在子类中也有,代码运行时,真正使用的方法实现是子类的。这样一来,无论有多少种特定的子类,业务逻辑代码都不会依赖于特定类型代码)。这使得人们可以编写出不依赖于特定类型的代码,在"几何形"的例子中,方法操作的都是泛化的形状,而不关心他们是"圆形"、“正方形”、“三角形"还是其他什么尚未定义的形状,因为所有的几何形都可以被绘制、擦除、移动,所以这些方法都是直接对一个几何形对象发送消息,它们不用担心对象如何处理消息。
这样的代码是不会受新添加的类型的影响的,而且添加新类型是扩展一个面向对象程序以便处理新情况的最常用方式。例如,可以从"几何形"中导出一个新的子类型"五角形”,而并不需要修改处理泛化几何形状的方法,通过导出新的子类型而轻松拓展设计的能力是对改动进行封装的基本方式之一(创建了新的类即意味着封装),这种能力可以极大地改善我们的设计,同时也降低软件维护的代价。
但是,在试图将导出类型的对象当作其泛化基类型对象来看待时(比如把圆形看成是几何形,把自行车看成是交通工具,把鸬鹚看作是鸟等等),仍然存在一个问题:如果某个方法要让泛化形状绘制自己,让泛化交通工具行驶,或者让泛化的鸟类移动,那么编译器在编译的时候是不知道执行哪一个特定类型的代码的(Java语言涉及编译和执行两个阶段,后面会提代码,通过代码你就能明白,为什么在编译的时候不知道执行哪一段代码,但是在执行的时候知道。),这就是关键所在,当发送这样的消息时,程序员并不想知道哪一段代码将被执行(也就是不想知道最后到底是用的哪个特定子类的实现),绘图方法可以被等同的应用于圆形正方形三角形,最后是依据具体使用的对象类型来执行恰当的代码(如果最后具体使用的对象是圆形,那就执行圆形的实现,如果是正方形,那就执行正方形的实现,如果是三角形,那就执行三角形的实现)。
你在添加了新的子类型的时候,你并没有同步更改调用它的方法,但是最后运行的结果却是对应的新增子类型方法实现应该有的结果。而编译器在编译期是无法精确了解最后应该执行哪一个特定子类型的代码的,那么这是怎么做到的呢?
如图:
多态类型层次图!
BirdController仅仅处理泛化的Bird对象,而不直接处理Goose以及Penguin这两个特定的子类型对象,当BirdController处理泛化的Bird对象的时候,并不知道确切的子类型(到底是Goose还是Penguin),从BirdController的角度来看,这么做非常的方便,因为不需要编写专门的代码来判断要处理的Bird对象的确切子类型以及行为(这句话是重点),当move方法被调用的时候,会依据你最后使用的对象类型产生正确的行为(Goose(鹅)走,飞或游泳,Penguin(企鹅)走或游泳)。我们接下来具体解释一下这样实现的原理。
编译器不可能产生传统意义上的函数调用。–这句话的意思是:Java多态实现的原理和以前的函数调用完全不相同。
一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定…这么做意味着编译器将产生对一个具体函数名字的调用。–这句话的意思是Java是面向对象的语言,之前的非面向对象的语言,比如C,C++等,Java中的方法调用在它们的概念里是函数调用,那么函数调用实际上在编译期就已经确定了运行时的类型,这种形式有一个名称,叫做"前期绑定(这里的前期实际上指的就是编译期)",换句话说,C,C++在编译期就产生了对一个具体函数名字的调用了。
而运行时将这个调用解析到将要被执行的代码的绝对地址。–这句话的意思是,在编译期就确定了执行哪一段代码,运行期只要解析出执行的代码的地址就可以了。
然而在OOP中,程序直到运行时才能够确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他机制。 Java之所以能实现多态,唯一的改变就是:将确定执行代码的时机放在的运行期,也就是说,编译期不再确定具体执行哪一段代码了。
为了解决这个问题,面向对象程序语言使用了后期绑定的概念,当向对象发送消息时,被调用的代码直到运行时才能确定。–这句话的意思是,Java与C、C++的不同是,它设置在运行期才最终确认要执行哪一段代码(后期绑定(这里的后期就是运行期)),确定的同时,将代码地址解析出来并立即执行。所以这里就会存在一个空间–在编译期和运行期只要类型相同就可以了,正是这个空间的存在,实现了多态,因为我们后面会知道,一个类只要实现了一个接口,那这个类和这个接口就是同一个类型了。
编译器确保被调用方法的存在,并对调用参数和返回值进行类型检查(无法提供此类保证的语言是弱类型的),但是并不知道将被执行的确切代码。–这句话的意思就体现了后面我们将要学习的接口的一个强制规定:实现一个接口就要实现这个接口的所有方法。为什么?因为在编译期,仅要求是这个接口的类型就行,所以,编译器就会记录这个接口类型的所有方法,如果实现这个接口的子类型缺失了其中某个方法,或者方法名相同,但是参数或者参数类型不同,编译器都会报错,这就是为什么编译器确保被调用方法的存在,并对调用参数和返回值进行类型检查(无法提供此类保证的语言是弱类型的) 要这么做的原因。只要编译期类型没有问题,就继续转入执行期。
为了执行后期绑定,Java使用了一小段特殊的代码来代替绝对地址调用,这段代码使用在对象中存储的信息来计算方法体的地址,这样,根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。–首先,你的思维不能仅局限于类型,因为多态的效果最后是需要通过类型创建的一个个对象来体现,父类型就是这个接口,但是这个接口的子类型可以有很多,每个子类型创建的对象会有更多,唯一的关联在于,每个子类型的对象的行为(方法)都是大于或者等于这个接口的行为,所以通过接口去调用方法时,实际上执行的将会是每个对象中的那个同名方法,所以可以表现出很多种不同的行为(因为每个子类型创建出的对象的同名方法的实现都是不同的)。
举例:比如父接口类型是Car,它有一个方法是start(),假设它有三个子类型,一个是AudiCar(奥迪),一个是BenziCar(奔驰),一个是BMWCar(宝马),显然,这三个子类型里面都会存在一个叫start()的方法,那么这三个子类型创建出的每一个对象都会有start()方法,但是,这三个子类型的start()方法的实现都不一样,所以,在AudiCar(奥迪),BenziCar(奔驰),BMWCar(宝马)这三个子类型所创建的每一个对象中,都会有一小段代码代替绝对地址调用,找到属于自己的那个start()方法实现去执行,得到正确的结果。
为什么会这样,从代码上可以看出来:

// 多态的代码举例
Car c = new AudiCar();
//这个start()实现是奥迪的实现。
c.start();
Car c = new BenziCar();
//这个start()实现是奔驰的实现。
c.start();
Car c = new BMWCar();
//这个start()实现是宝马的实现。
c.start();

通过代码你会发现,你如果新增了新的汽车类型,你只需要替换new后面的类型,c.start()是固定的,因为这个c的类型在执行的时候才确定下来(根据new关键字后面的实际类型确定)。
所以说多态很好的扩展了程序。
下面再通过一个例子来全面说明多态:
几何形:
几何形整个类族(其中所有的类都基于相同一致的接口,简单来说,就是所有的类方法都相同!),如下图示:
经典的几何形继承情况说明图!
其中Shape是基类,它的子类有Circle、Square、Triangle三类,他们的接口全部都是draw()、erase()、move()、getColor()、setColor()
为了说明多态,接下来编写一段代码,它忽略了类型的具体细节,仅仅和基类Shape交互,这段代码和具体的类型信息是分离的,这样做使代码编写更为简单,也更易于理解。而且,如果通过继承机制(有时也可以通过实现接口,如果是类或者是抽象类就是继承,如果是接口就是实现,有需要的话,可以深入了解接口,类,抽象类三者之间的区别,后期博文也会详细解读)添加一个新类型,例如Hexagon(六边形),之前编写的代码可以直接拿来使用,不需要改动。正因为如此,这段代码(程序)被称为是可扩展的。
可拓展的代码如下:

//多态可拓展代码示例,注意以下,方法的参数是Shape,这里表示,不一定要是shape类本身,它的子类也是可以的
void doSomething(Shape shape){
    shape.erase();
    shape.draw();
}

以上代码中的方法可以与任何Shape对话,因此它是独立于任何它要绘制和擦除的对象的具体类型的,可以具体来看看如何正确使用它:

//多态可拓展代码的正确使用。
//创建子类的对象
Circle circle = new Circle();
//可以直接将新创建出来的对象circle当作Shape类型传入doSomething(Shape shape)方法中。
doSomething(circle);
//执行结果将是与circle有关。
//其他的子类类似
Line line = new Line();
doSomething(line);
Triangle triangle = new triangle();
doSomthing(triangle);

再通过实际运行的例子截图说明:
基类:
多态中的基类Shape!
子类(Cirlce):
PS:继承的时候,从基类中继承过来的方法如果需要表现出子类的特点,就需要重写,否则还是基类的特点(重写后期博文中详解继承的时候会详细描述)。
子类Circle,重写了基类中的所有方法!
测试类(测试多态的结果):
多态的效果出来了!
可拓展性研究:
PS:这个时候新增一个子类型,可拓展性就体现在,使用新类型的时候,测试类的代码不用做任何改动!
新增的子类Triangle:
新增子类Triangle!
直接将子类使用在测试代码中,而测试代码没有做任何改动:
新增的子类可以直接使用!
这里还有一个小问题需要提醒:就是上文中我使用了这种形式:Car c = new AudiCar();
实际上,这里也可以,就是声明的类型使用基类型

多态的更强有力的说明例子!
所以,在你觉得目前的类型的实现已经不能满足你的需要的时候,就新增类型,然后用新的实现,只是需要继承基类而已,总体框架你不用去改变,这样非常有利于软件的升级和维护!
下面是编程思想的原文解释:
对doSomething()的调用会自动地正确处理,而不管对象的确切类型。–也就是说,传入哪个类型的对象,执行的结果就是与它有关的结果,但是参数声明仅仅是父类型。
这是一个相当令人惊奇的诀窍,看看下面这行代码:
doSomething(Circle);
当Circle传入到预期接收Shape的方法中,究竟会发生什么,由于Circle可以被doSomething()当做Shape,也就是说,dosomething()中可以发送给Shape的消息,Circle都可以接收,那么,这么做是完全安全且合乎逻辑的(在这里,还是要再提一遍:继承是默认继承了父类的所有代码,而实现接口必须要实现接口中的所有方法,这两个规定和"dosomething()中可以发送给Shape的消息,Circle都可以接收"是完全对应的!因为如果不是继承了所有的代码或者是实现了接口中的所有方法,发送给Shape的消息Circle不一定能接收!)。
将导出类(子类)看作是它的基类的过程称为向上转型,转型这个名词的灵感来自于模型铸造的塑模动作,而向上这个词来源于继承图的典型布局方式–通常基类在顶部,而导出类(子类)在其下部散开,因此,转型为一个基类就是在继承图中向上移动。即"向上转型"(继承图在这里我就不画了,自行脑补吧)。
一个面向对象程序肯定会在某处包含向上转型,因为这正是将自己从必须知道确切类型中解放出来的关键。
再看看看doSomething()的代码:shape.erase(),shape.draw(),注意,这里代码并没有体现出如果是Circle,请这样做,如果是Square,请那样做,如果编写了那种检查Shape所有可能实际类型的代码,那么这段代码肯定是杂乱不堪的,而且在每次新增了类型之后都需要去修改这段代码。
而这里仅仅要表达的意思是,你是一个Shape,我知道你开头erase()和draw()你自己,那么去做吧,但是需要注意正确性。
doSomething()的代码给人印象深刻之处在于,不知何故,它总是做了该做的,调用Circle的draw()方法所执行的结果和调用Square或Line的draw()方法所执行的结果是不同的,通过doSomething(),当draw()消息发送给一个匿名的Shape时,也会基于该Shape的实际类型产生正确的行为,这相当神奇,因为就像在前面提到的,当Java编译器在编译doSomething()代码时,并不能知道doSomething()要处理的确切类型,所以通常会期望它的编译结果是调用基类Shape的erase()和draw()版本,而不是具体的Circle、Square、或Line相应的版本,正式因为多态才使得事情总是能够被正确处理!编译器和运行系统会处理相关的细节,你需要知道的是,事情马上会发生,更重要的是怎样通过它来设计,及时设计向上转型,该对象也知道要执行什么样的正确行为!
PS:时间有限,有关Java SE的内容会持续更新!今天就先写这么多,如果有疑问或者有兴趣,可以加QQ:2649160693,并注明CSDN,我会就博文中有疑义的问题做出解答。同时希望博文中不正确的地方各位加以指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值