【Java】多态和动态绑定中的坑
2016-05-07 19:29
585 查看
大一学C++的时候就感觉多态这部分略烦,小坑不断。一年过后学习Java再次遇到多态的问题,写一篇笔记来记下发现的各种小坑,以供以后复习。
来看下面一段代码:
当创建orchestra数组时,如果new的是导出类,那么他会自动向上转型成Instrument。因为Wind和Brass毕竟是继承自Instrument的,所以Instrument的接口必定存在于它导出的类中。
在Main函数中,tune方法接收一个Instrument引用,但同时也接受任何导出自Instrument的类。那么在这种情况下编译器怎样才能知道这个Instrument引用指向的是Wind对象,而不是Brass呢?实际上,编译器也不知道。。那么这个问题怎么解决呢?答案是采用动态绑定技术。
动态绑定是指在运行时根据对象的类型,将一个方法调用同一个方法主体关联起来。也就是说编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。因此,tune方法就可以正确调用导出类重写的相应方法。
Note: Java中除了static方法和final方法(private方法也属于final方法)之外,其他所有方法都是动态绑定的。当然,这样做有很多好处,也有一定的麻烦。
我们来看看麻烦在哪里:
首先要说明一下Java初始化对象的实际过程:
在做任何事情之前,先将分配给对象的存储空间初始化成二进制的零
自下向上调用基类构造器。这里在Glyph的构造器中会调用覆盖了的draw()方法,而不是Glyph的draw()。由于步骤1的缘故,我们会发现radius的值是0
完所有基类的初始化后,按照声明的顺序调用当前成员的初始化方法。这里会将radius初始化为1
调用导出类的构造器主体
第一次看到这样的结果时我的内心其实是拒绝的。。觉得Glyph构造器里调用的draw()方法理所当然的就应当是Glyph.draw()。然而这是一个坑,很容易被忽视掉,而且不好debug。
Note: 在构造器内唯一能够安全调用的方法是基类中的final方法,当然包括private方法。这些方法不会被导出类覆盖,因此也就不会出现掉进上面这个坑的情况了。
来看下面一段代码:
enum Note { MIDDLE_C, C_SHARP, B_FLAT } class Instrument { public int field = 0; void play(Note note) { System.out.println("Instrument.play() " + note); } String what() { return "Instrument"; } void adjust() { System.out.println("Adjust Instrument"); } } class Wind extends Instrument { public int field = 1; @Override void play(Note note) { System.out.println("Wind.play() " + note); } @Override String what() { return "Wind"; } @Override void adjust() { System.out.println("Adjust Wind"); } } class Brass extends Wind { @Override void play(Note note) { System.out.println("Brass.play() " + note); } @Override void adjust() { System.out.println("Adjust Brass"); } } public class Music3 { public static void tune(Instrument instrument) { instrument.play(Note.C_SHARP); } public static void tuneAll(Instrument[] instruments) { for (Instrument instrument : instruments) { tune(instrument); } } public static void main(String... args) { Instrument[] orchestra = { new Wind(), new Brass() }; tuneAll(orchestra); } } /* Output: Wind.play() C_SHARP 0 Brass.play() C_SHARP 0 */
当创建orchestra数组时,如果new的是导出类,那么他会自动向上转型成Instrument。因为Wind和Brass毕竟是继承自Instrument的,所以Instrument的接口必定存在于它导出的类中。
在Main函数中,tune方法接收一个Instrument引用,但同时也接受任何导出自Instrument的类。那么在这种情况下编译器怎样才能知道这个Instrument引用指向的是Wind对象,而不是Brass呢?实际上,编译器也不知道。。那么这个问题怎么解决呢?答案是采用动态绑定技术。
动态绑定是指在运行时根据对象的类型,将一个方法调用同一个方法主体关联起来。也就是说编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。因此,tune方法就可以正确调用导出类重写的相应方法。
Note: Java中除了static方法和final方法(private方法也属于final方法)之外,其他所有方法都是动态绑定的。当然,这样做有很多好处,也有一定的麻烦。
我们来看看麻烦在哪里:
class Glyph { void draw() { System.out.println("Glyph.draw()"); } private void duplicate() { System.out.println("Glyph.duplicate()"); } Glyph() { System.out.println("Glyph() before draw"); draw();// 动态绑定,调用子类覆盖的draw duplicate();// 非动态绑定,调用Glyph的duplicate System.out.println("Glyph() after draw"); } } class RoundGlyph extends Glyph { private int radius = 1; RoundGlyph(int r) { radius = r; System.out.println("RoundGlyph.RoundGlyph().radius = " + radius); } @Override void draw() { System.out.println("RoundGlyph.draw().radius = " + radius); } public void duplicate() { // 这个duplicate和父类的duplicate没有任何关系 // 不是Override // 因为父类的duplicate是private final的 System.out.println("RoundGlyph.duplicate()"); } } public class PolyConstructors { public static void main(String...args) { new RoundGlyph(10); } } /* Output: Glyph() before draw RoundGlyph.draw().radius = 0 Glyph.duplicate() Glyph() after draw RoundGlyph.RoundGlyph().radius = 10 */
首先要说明一下Java初始化对象的实际过程:
在做任何事情之前,先将分配给对象的存储空间初始化成二进制的零
自下向上调用基类构造器。这里在Glyph的构造器中会调用覆盖了的draw()方法,而不是Glyph的draw()。由于步骤1的缘故,我们会发现radius的值是0
完所有基类的初始化后,按照声明的顺序调用当前成员的初始化方法。这里会将radius初始化为1
调用导出类的构造器主体
第一次看到这样的结果时我的内心其实是拒绝的。。觉得Glyph构造器里调用的draw()方法理所当然的就应当是Glyph.draw()。然而这是一个坑,很容易被忽视掉,而且不好debug。
Note: 在构造器内唯一能够安全调用的方法是基类中的final方法,当然包括private方法。这些方法不会被导出类覆盖,因此也就不会出现掉进上面这个坑的情况了。
相关文章推荐
- java对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序
- 二叉查找树