看《重构(注释版)》中“封装集合(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哈!
隐藏(hide):子类的某个字段、静态方法、成员内部类与其父类的具有相同名字(对于静态方法还需要相同的参数列表),此时父类对应的字段、静态方法、成员内部类就被隐藏了。
举个例子,天鹅(Swan)是会飞的,而丑小鸭(UglyDuck)小时候是不会飞的,看看下面的代码,看看能够打印出什么。
class Swan {
public static void fly() {
System.out.println("swan can fly ...");
}
}
class UglyDuck extends Swan {
public static void fly() {
System.out.println("ugly duck can't fly ...");
}
}
public class TestFly {
public static void main(String [] args) {
Swan swan = new Swan();
Swan uglyDuck = new UglyDuck();
swan.fly();
uglyDuck.fly();
}
}
按道理的话,我们认为应该是输出两句不同的结果,因为我们可能认为 UglyDuck 继承了 Swan 并且“重写”了 fly() 方法,而且在 main() 方法中 Swan uglyDuck = new UglyDuck(); 也表明了 uglyduck 实际上是 UglyDuck 类型的,因此构成了多态行为。
其实,运行结果是两句“swan can fly ...”,为什么会这样子?原因有下:
1、父类 Swan 中的 static 静态方法 fly() 是不能被重写的,上一段我对重写二字用了双引号;
2、尽管子类 UglyDuck 中的 fly() 方法与父类中的有一致的参数列表,但是对于 static 方法来说,这叫隐藏(hide),而不是重写(override);
3、对于 static 方法,根本不存在像多态那样的动态分派机制,JVM 不会根据对象引用的实际类型来调用对应的重写方法。这一点在个例子中是最重要的。
对于 static 方法,我们称之为类方法,不是实例方法,对 static 方法的调用直接用所属类名加个点就行,如 UglyDuck.fly() 。而实例方法就不得不使用对象引用来获得其可访问方法的调用权。在上面的例子 main() 中的 uglyDuck.fly() 语句,JVM 根本据不会去判断 uglyDuck 引用的究竟是什么类型,既然调用的是 fly() 方法,那么 JVM 只会根据 uglyDuck 的声明类型(即 Swan 类)去获得该 static 方法的调用。根本就谈不上多态…
这就说明,最好避免用对象引用的方式来访问一个 static 方法。此外,别以为在继承关系上的父类、子类只要方法名、参数列表一致就是重写(override)而构成多态,其实还得看看父类中的方法有没有被什么修饰符声明(在这个例子中是 static 修饰的)。再如 final 修饰符的方法则表明不可被子类重写,即方法名、参数列表不能和父类完全一致。在我看来,这一类修饰符就表明了方法、变量、字段等特有的性质,或者是身份。
对于隐藏(hide),实际上是为了使得父类中的该方法、字段、内部类等不允许再被下一级继承树的子子类所继承。说起隐藏,我想起《代码大全 2》当中刚好看过的内容,作者认为把握住信息隐藏的原则来思考软件构建要优于面向对象原则。有点抽象难懂,书中还讲到封装、模块化和抽象等几个概念,建议看看,我也要回过头去多啃啃这些抽象概念。
要修改上面代码,只需要去掉两个 static 则可,那就构成多态了。《Java 解惑》中其他谜题还讲到多种该注意的地方,可以看看。
小结:
1、注意掌握重写(override)与隐藏(hide)的异同点:相同点就是两者都是相对于继承树中父类、子类来说,而不同点就是其目的以及所造成的效果。别把重写和隐藏混淆在一起了;
2、对于 static 方法,要避免用具体的对象引用来调用,而应该简单的用其所属类名进行调用即可。
遮蔽(shadow):其实就是平时我们可能遇到的窄作用域的变量名、方法名、类名等将其他相同名字的变量、方法、类屏蔽掉的现象。
例如,最常见的就是局部变量将类实例变量屏蔽了。其实,遮蔽这个词我之前好像也没什么印象,不过作用域屏蔽这种情况我们大多应该会避免的了,因为课堂上、教材上对于变量作用域的内容已经讲解过了,尽管没有这么一个术语。此时如果想要获得被遮蔽实体的引用、调用,则只能通过完整的限定名去实现了。不过有一些情况可能是根本就引用不到的,被屏蔽得太严密了。
遮掩(obscure):一个变量可以遮掩具有相同名字的一个类,只要它们都在同一个范围内:如果这个名字被用于变量与类型都被许可的范围,那么它将引用到变量上。相似地,一个变量名或一个类名可以遮掩一个包。遮掩是唯一一种两个名字位于不同的名字空间的名字重用形式,这些名字空间包括:变量、包、方法或类。如果一个类型或一个包被遮掩了,那么你不能通过其简单名引用到它,除非是在这样一个上下文环境中,即语法只允许在其名字空间中出现一种名字。遵守命名习惯就可以极大地消除产生遮掩的可能性。
其实,遮掩这个术语我更是完全没听过了,上面这一段是从《Java 解惑》中引用过来的。我觉得,如果代码是一个人所写,或者团队中大家都遵守了一定的命名规范,而且也各自分配了一定职责,那么遮掩这种情况应该是可以避免的。同样,需要使用完全限定名来引用被遮掩掉的实体,如下:
用前面例子的代码大概就是这种情况:
public class TestFly {
// 如此变量名
static String System = "system";
public static void main(String [] args) {
//String System = "hao";
// 编译不通过
//System.out.println("No");
// 编译通过
java.lang.System.out.println("OK");
}
}
小结:
1、我觉得,在文章标题当中的五个名词当中,尤为前面三个最为重要,陷阱炸弹也多多,而且文中所讲仅仅是那么一丁点儿相关的,大量更细节的还得慢慢发现;
2、就算后面两个名词,也就是这两种情况不常见,但了解一下记在脑里还是不错的,毕竟为自己增加了专业词汇量;
3、最后,就是建议各位也看看《Java 解惑》然后也告诉我一些炸弹型陷阱之类的,呵呵...学习快乐!加油!