首页 > 代码库 > Java编程思想之8多态
Java编程思想之8多态
这一章看下来,感觉比较混乱。乱感觉主要乱在8.4 8.5。 开始读的时候,感觉8.1 8.2 8.3都挺乱的。读了两遍后发现前三节还是比较有条理的。
8.1主要讲了什么是多态,多态的好处。
8.2主要讲了什么情况会发生多态??
8.3主要讲了构造器内部里面的方法调用会发生多态。
8.4就一页,而且感觉一般用不到。用到了再看也行。
8.5也很简单,相当于一个总结,一个补充(向下转型)
我主要根据书上的内容,总结两个内容: 1.什么是多态,多态的好处; 2.什么情况下会发生多态?为什么这些情况下会发生多态,而别的情况不会发生多态?
什么是多态??
多态是面向对象的三个基本特征之一。也是比较不容易理解的一个。
多态,按照我的理解,应该是:根据调用对象的不同,而执行不同的方法。 这个应该是比较表面的,比较浅的定义。
多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术(《Java编程思想》155页)。 这个说法或许说明了最本质的东西,但是没有几年道行,很难理解。
其实这两种说法是一致的,正是因为多态可以 在运行是根据不同的对象,调用不同的方法。所以才能将改变的事物与未变的事物分开。不容易理解,就看书上149页的例子。函数tune(Instrment i)里面,调用了i.play()方法。实际的执行过程中,就会根据i的具体类型,而执行这个具体类型里的方法。
《java编程思想》148页头两段,写的真是好。摘抄一下:
多态是通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能创建可扩展的程序。
这四小句话,每一小句,都很耐人寻味的。下面的一段开始解释了。
- 先解释第二小句,什么是另一个角度呢??我们已经有一个角度了吗??答案是:有,封装。“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”是通过细节“私有化”把接口和实现分离开来。这是封装的两个特性,我们只把public方法暴露出来,而这个方法的逻辑实现,我们可能调用了很多private方法。所以,通过私有化将实现隐藏,而仅仅把public方法暴露。
- 解释第一小句,感觉它的这个角度(通过分离做什么和怎么做,实现接口与实现的分离),是跟抽象类/基类/接口的角度很象的。 或者说,他们其实是一个东西,基类(包括抽象类接口)与多态结合,才能实现这个分离做什么和怎么做。 基类(接口)规定了做什么,子类(接口的实现)负责怎么做。 但是,两者之间的结合是靠的多态。多态的作用是:消除类型之间的耦合关系。耦合关系越低,肯定就是可以将改变的东西和不变的东西分离的越好。还是第149页的上下两个例子,如果不用基类作为函数的参数,而是用的具体类。那么,这个Msic2类就跟具体类偶尔很大了,具体类的添加和删除都会影响到Msic2类。这个基类,相当于提供了一个中间层,消除了这个耦合。而具体运行时,多态可以根据调用者的不同,而执行不同的方法。所以,我认为:是基类(接口)和多态共同实现了这个特性(分离做什么和怎么做)。而多态这个特性的实现,必须依靠继承的一个特性,继承有两个特性:一. 代码复用;二.允许将对象视为它自己本身的类型或其基类型来加以处理。第二个特性太重要了,正是因为第二个特性,才得以让多态的特性得到了发挥。第二个特性,也是使用继承的重要标志。第二个特性允许将多种对象视为它自己本身的类型或其基类型来处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。
- 第三句和第四句,也算是多态的好处吧。看着149页的代码,就能感觉到,第一个比第二个组织结构好,可读性好。 也容易扩展。
注:多态一般是消除了类使用时的耦合,我们只要使用抽象类或者接口就行。系统根本不需要知道我们到底使用了哪个具体类。 而这个类实例的创建,很容易发生强耦合,new 一个具体类。 这个问题解决是依靠了设计模式里面的工厂模式,将类的创建和类的使用彻底分开。《Java编程思想》也多次举例说明工厂模式。
什么方法的调用会发生多态??
书上8.2 8.3都是讲的哪些情况的调用会发生多态。但是,感觉讲的比较乱,如果从java虚拟机的角度出发,就很好搞定,也不用记太多的东西了。
8.2.1说道,有些方法是在编译期间绑定的,叫做前期绑定。有些方法是在运行时绑定的,叫做后期绑定的。后期绑定也叫做动态绑定或运行时绑定。显然,前期绑定不会发生多态,应为调用哪个方法已经确定了。 后期绑定才会发生多态。牢记:static方法,final方法,private方法是前期绑定,不会发生多态。 其余的方法是后期绑定,会发生多态。
我们知道,Java经过编译后,会编译成字节码。那么方法调用会编译成什么样的字节码呢??一共有四种方法调用字节码指令。分别是:
- invokestatic:调用静态方法;
- invokespecial:调用实例构造器<init>方法,私有方法和父类方法;
- invokevirtual:调用所有的虚方法;
- invokeinterface:调用接口方法。
只要能被invokstatic invokespecial调用的方法,都可以在解析阶段确定唯一调用的版本。与之相反,其它方法就成为虚方法(final方法出外,虽然final方法是使用invokevirtual调用的,但是final方法无法被覆盖,没有其它版本,调用的结果肯定是唯一的。所以,java语言规范明确说明了final方法是一种非虚方法)。看下面的例子:
public class DynamicDispatch { static abstract class Human{ protected abstract void sayHello(); } static class Man extends Human{ @Override protected void sayHello(){ System.out.println("man say hello"); } } static class Women extends Human{ @Override protected void sayHello(){ System.out.println("women say hello"); } } public static void main(String[] args){ Human man = new Man(); Human women = new Women(); man.sayHello(); women.sayHello(); man = new Women(); man.sayHello(); } }
下面是进入到这个java文件目录后,编译并输出字节码文件(命令在图片里面)
主要看main方法,第0行new了一个对象实例。创建对象的过程中,会调用<init>方法,这个方法是Java编译器自动生成的,用来初始化对象实例的。第3行,是把0行的指针复制一下,因为4行的语句会耗费一个指针。7行是保存这个对象到局部变量。8到15行类似。
第16行,是将第一个局部变量载入内存,那么第一个局部变量是谁呢??我们知道,一个实例方法的第一个局部变量保存的是this,然后是各个函数参数的依次保存。然后是方法里面声明的变量。而静态方法是没有this变量的。所有这个main方法保存的第一个是函数的参数String[],保存在位置0。而位置1就是我们函数里面
Human man = new Man();这个变量了。关键看第17行,这个指令会根据上一个变量的实际类型,来选择所应该调用的方法。只有到了运行期才会确定出这个方法的具体位置。#6指向的是常量池,里面是一个字符串“sayHello:()V",根据这个16行的实际类型,会先定位到这个实际类型本身,查找这个实际类型的所有方法,如果找到了这个sayHello()方法,就调用。否则的话,就搜索父类。这样一直搜索到Object()类。就是这样,实现了多态。 当然,有可能会优化这个搜索过程。具体可以看《深入理解Java虚拟机》,周志明老师写的。
类似的,可以这样反编译一下private方法,final方法的情况,很容易,就可以知道的了。
其实到这里,我感觉已经把这一章的主要东西说完了。为了让思路更加顺畅,下面我想总结一下,为什么private final方法不发生多态???
为什么private final方法不发生多态???
其实,很多时候都是这样的:能在尽早搞定的事儿,就不要拖到后面。 比如 尽早的警告代码里的错误等。 所以,能在解析期就确定的方法,就不要到运行期再确定。因为在运行时再去确认哪个方法,可能会影响执行速度。看下面的代码:
public class DynamicDispatch { static abstract class Human{ protected abstract void sayHello(); public final void love(){ System.out.println("I love you"); } } static class Man extends Human{ @Override protected void sayHello(){ System.out.println("man say hello"); } } public static void main(String[] args){ Human h = new Man(); h.love(); } }
love()方法是final的。 main函数里面,不论这个Human h被实例化成哪个子类,h.love()永远调用的是Human类中的love方法,首先不可能调用到Human父类里面去,因为Human类已经有这个方法了。 也不可能调用到Human的子类里面,因为love()方法是final的,不能被子类覆盖的。所以,在解析期间完全可以确定这个love()应该调用的具体位置。所以,这个方法就不会多态了。
注:编译期间只是把方法的调用,转化到了调用常量池的某个字符串。在解析期间,才会把一起方法的常量池字符从转化为具体的方法位置,private方法,final方法都是在这个阶段失去了多态的可能性。而虚方法是不会转化的,所以会发生多态。