看《重构(注释版)》中“封装集合(Encapsulate Collection)”一节时,由于该重构手法对于不同的 Java 版本会有相对应不同的处理方式,于是注释者在旁边给出提示:Java 2 中的新 Collections API 主要是由《Java 解惑》、《Effective Java》这两本书的作者开发改进的。我想,这可真是一个大消息,Java 类库的开发者所写的书一定得看,开发者肯定深入探寻过 Java 内部机制,说不定能从书中获得未知的知识点呢。
好在我记得自己电脑里下载过《Java 解惑(中文版)》的清晰电子版,于是改变路线,看起这本书来了。真是不看不知道,一看吓一跳!里面有 95 个 Java 谜题,一点点地看了之后,我是既惭愧又兴奋,因为里面的题真的是“莫名其妙”,直白点就是:十有八九是想都想不出来的,⊙﹏⊙b…想要真正掌握 Java ,我觉得《Java 解惑》是不得不看的。大多谜题可谓是前所未见的×××,一不小心就 over 了。
快点切入正题,如文章标题所示,我从《Java 解惑》中似乎认识到了几个名词,之所以用似乎修饰,因为重载、重写、隐藏这几个接触过了,而遮蔽、遮掩或许是见过但也就忘光了。就整理一下文中的一些与此相关的 Java 谜题用自己的理解描述一下吧。
重载(overload):同一个类中名字相同但参数列表不同的多个方法之间的关系。
关于重载,是我们比较熟悉的了,最常见的就是运用在类的多个构造函数中,看一下 Java 帮助文档,就可以明白这一情况了。而在《Java 解惑》中,作者给出了下面一个谜题:
- public class Confusing {
- private Confusing(Object o) {
- System.out.println("Object");
- }
- private Confusing(double[] dArray) {
- System.out.println("double array");
- }
- public static void main(String[] args) {
- new Confusing(null);
- }
- }
问此时 main() 中将会输出什么?初初一看,并没有多分析就觉得应该是输出“Object”,虽然Java中的数组实际上也是引用类型,但毕竟Object 是所有类的最终父类,而且目前 JDK 就连参数中的基本数据类型变量也可以被自动想上转型成包装类而成为 Object 的子类。于是我保守一点地就认为参数 null 应该是匹配到 Object 那个重载方法去了。
可是这答案是错的,JVM 对于重载方法的解析是这样的:先找出方法名匹配的所有可能的方法;然后根据传进来的形参再次筛选出可能的重载方法;最后才是在这些方法中匹配到一个最精确的一个方法。于是,上面的那个谜题就变成确定哪一个才是最精确这一点子上了。
而关于如何判断最精确,有这样的机制:如果某个重载方法能够接收所有传递给另一个重载方法的实参类型,那么对于参数列表来看,显然后者至少是前者的子集,当然也就更精确了。
回到谜题上来,Confusing(Object)可以接受任何传递给 Confusing(double[ ])的参数(任何数组引用最终能够都是 Object 对象),因此 main() 中的 null 应该是被 JVM 匹配到 Confusing(double[ ]) 中,也就有了与我所认为的结果相反的输出了。
小结:这个谜题表明了我们在写重载方法时,最好是明确地区分出各个方法中的参数列表,不要让彼此之间有互相包含、模糊不清的关系。虽然重载是为了在相同名字的方法中传入实参,由 JVM 动态解析选择合适的方法,但有时也很容易陷入这种方便背后所带来的地雷区当中。其中一种可行的办法就是,提供不同的方法名。但是构造函数的名字一定得相同的啊?
实际上,在《重构与模式》第六章中,作者用他自身的项目经验对“创建”这一话题展开了讲解,就算是构造函数,也有很好的重构手法将其清晰地区分开来,不使用重载而是用不同名称的方法,将原本需要重载的构造函数委托给具有最大完整参数列表的私有构造函数中。又是一本经典,值得看哦…
重写(override):父类中的实例方法被其子类重新实现。既然是实例方法,那就是非 static 修饰的了,否则就是 static 静态方法了,那叫做类方法。在我看来,正是重写这一机制的存在,才为多态机制提供了基础。或许 implements (实现)一个 interface (接口)中所声明的方法也能成为重写,因为 interface 的一部分存在原因也是为了多态。
对于重写,在《Java 解惑》中有下面这个谜题让我明白:绝对不能在构造函数中调用可能会被子类重写的方法。
- class Point {
- protected final int x, y;
- private final String name;
- Point(int x, int y) {
- this.x = x;
- this.y = y;
- name = makeName();
- }
- protected String makeName() {
- return "[" + x + "," + y + "]";
- }
- public final String toString() {
- return name;
- }
- }
- public class ColorPoint extends Point {
- private final String color;
- ColorPoint(int x, int y, String color) {
- super(x, y);
- this.color = color;
- }
- protected String makeName() {
- return super.makeName() + ":" + color;
- }
- public static void main(String[] args) {
- System.out.println(new ColorPoint(4, 2, "purple"));
- }
- }
此时程序运行结果并不是我们所想的 [4,2]:purple ,而是 [4,2]:null 。为什么会这样?看看下面用流程标号注释过的代码,就能理解了。
- class Point {
- protected final int x, y;
- private final String name;
- Point(int x, int y) {
- this.x = x;
- this.y = y;
- name = makeName();// 3. 由于被子类重写过的makeName()
- }
- protected String makeName() {
- return "[" + x + "," + y + "]";
- }
- public final String toString() {
- return name;
- }
- }
- public class ColorPoint extends Point {
- private final String color;
- ColorPoint(int x, int y, String color) {
- super(x, y); // 2. 调用Point父类构造函数
- this.color = color; // 5. 初始化 color ,可是已经太晚了...
- }
- protected String makeName() {
- // 4. 问题来了:它在子类构造函数之前调用了
- // 而此时的 color 是 null 的啊!!!
- return super.makeName() + ":" + color;
- }
- public static void main(String[] args) {
- // 1. 调用ColorPoint子类构造函数
- System.out.println(new ColorPoint(4, 2, "purple"));
- }
- }
思路很清晰了,ColorPoint 子类中的构造函数中的 this.color = color; 还未被执行到就将 null 作为 String color 的值了。正是因为这种来来回回的调用使得程序变得不正常了,在我看来,有那么一点类似于“回调”的意思。
要去除这种代码结构的不合理,最好还是把 Point 父类构造函数中调用 makeName() 方法一句去掉,然后在 toString 中判断并调用 makeName() 来为 name 初始化,如下:
小结:重写对于多态固然重要,但是设计出不正确的代码结构的话,原本想要的多态就会被扭曲甚至造成反效果。于是,绝对不要在构造函数中调用可能会被子类重写的方法。
好像文字太多的文章看了容易使人晕乎乎的,啰啰嗦嗦、模模糊糊地才写了两个词儿,还是分开来写吧。其实,看了一部分《Java 解惑》才明白还有好多好多 Java 里面该注意的要点。要想在适当的时候辨清各种语法上、机制上的知识点,难啊!
记得高中语文课上读过一片抒情的散文,标题为“哦——香雪!”而我看了《Java 解惑》,想说“噢——Java!”~~~~(>_<)~~~~
总结:
1、在我们编程领域,好书真的是一大把,就看自己有没时间、有没策略地去吸收了;
2、有时候看好书时留意作者对其他书籍的“友情链接”,或者出版社推荐的相关书籍,这样就能够免去自己慢慢搜寻好书的过程了,O(∩_∩)O哈!