Java炸弹:重载、重写、隐藏、遮蔽、遮掩

        看《重构(注释版)》中“封装集合(Encapsulate Collection)”一节时,因为该重构手法对于不一样的 Java 版本会有相对应不一样的处理方式,因而注释者在旁边给出提示:Java 2 中的新 Collections API 主要是由《Java 解惑》、《Effective Java》这两本书的做者开发改进的。我想,这可真是一个大消息,Java 类库的开发者所写的书必定得看,开发者确定深刻探寻过 Java 内部机制,说不定能从书中得到未知的知识点呢。java

        好在我记得本身电脑里下载过《Java 解惑(中文版)》的清晰电子版,因而改变路线,看起这本书来了。真是不看不知道,一看吓一跳!里面有 95 个 Java 谜题,一点点地看了以后,我是既惭愧又兴奋,由于里面的题真的是“莫名其妙”,直白点就是:十有八九是想都想不出来的,⊙﹏⊙b…想要真正掌握 Java ,我以为《Java 解惑》是不得不看的。大多谜题可谓是前所未见的炸弹,一不当心就 over 了。编程

        快点切入正题,如文章标题所示,我从《Java 解惑》中彷佛认识到了几个名词,之因此用彷佛修饰,由于重载、重写、隐藏这几个接触过了,而遮蔽、遮掩或许是见过但也就忘光了。就整理一下文中的一些与此相关的 Java 谜题用本身的理解描述一下吧。数组

        重载(overload):同一个类中名字相同但参数列表不一样的多个方法之间的关系。ide

        关于重载,是咱们比较熟悉的了,最多见的就是运用在类的多个构造函数中,看一下 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 对于重载方法的解析是这样的:先找出方法名匹配的全部可能的方法;而后根据传进来的形参再次筛选出可能的重载方法;最后才是在这些方法中匹配到一个最精确的一个方法。因而,上面的那个谜题就变成肯定哪个才是最精确这一点子上了。学习

          而关于如何判断最精确,有这样的机制:若是某个重载方法可以接收全部传递给另外一个重载方法的实参类型,那么对于参数列表来看,显而后者至少是前者的子集,固然也就更精确了。this

           回到谜题上来,Confusing(Object)能够接受任何传递给 Confusing(double[ ])的参数(任何数组引用最终可以都是 Object 对象),所以 main() 中的 null 应该是被 JVM 匹配到 Confusing(double[ ]) 中,也就有了与我所认为的结果相反的输出了。spa

           小结:这个谜题代表了咱们在写重载方法时,最好是明确地区分出各个方法中的参数列表,不要让彼此之间有互相包含、模糊不清的关系。虽然重载是为了在相同名字的方法中传入实参,由 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!”~~~~(>_<)~~~~

总结:

一、在咱们编程领域,好书真的是一大把,就看本身有没时间、有没策略地去吸取了;

二、有时候看好书时留意做者对其余书籍的“友情连接”,或者出版社推荐的相关书籍,这样就可以免去本身慢慢搜寻好书的过程了,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 ...”,为何会这样子?缘由有下:

        一、父类 Swan 中的 static 静态方法 fly() 是不能被重写的,上一段我对重写二字用了双引号;

        二、尽管子类 UglyDuck 中的 fly() 方法与父类中的有一致的参数列表,可是对于 static 方法来讲,这叫隐藏(hide),而不是重写(override);

        三、对于 static 方法,根本不存在像多态那样的动态分派机制,JVM 不会根据对象引用的实际类型来调用对应的重写方法。这一点在个例子中是最重要的。

        对于 static 方法,咱们称之为类方法,不是实例方法,对 static 方法的调用直接用所属类名加个点就行,如 UglyDuck.fly() 。而实例方法就不得不使用对象引用来得到其可访问方法的调用权。在上面的例子 main() 中的 uglyDuck.fly() 语句,JVM 根本据不会去判断 uglyDuck 引用的到底是什么类型,既然调用的是 fly() 方法,那么 JVM 只会根据 uglyDuck 的声明类型(即 Swan 类)去得到该 static 方法的调用。根本就谈不上多态…

        这就说明,最好避免用对象引用的方式来访问一个 static 方法。此外,别觉得在继承关系上的父类、子类只要方法名、参数列表一致就是重写(override)而构成多态,其实还得看看父类中的方法有没有被什么修饰符声明(在这个例子中是 static 修饰的)。再如 final 修饰符的方法则代表不可被子类重写,即方法名、参数列表不能和父类彻底一致。在我看来,这一类修饰符就代表了方法、变量、字段等特有的性质,或者是身份。

        对于隐藏(hide),其实是为了使得父类中的该方法、字段、内部类等不容许再被下一级继承树的子子类所继承。提及隐藏,我想起《代码大全 2》当中恰好看过的内容,做者认为把握住信息隐藏的原则来思考软件构建要优于面向对象原则。有点抽象难懂,书中还讲到封装、模块化和抽象等几个概念,建议看看,我也要回过头去多啃啃这些抽象概念。

        要修改上面代码,只须要去掉两个 static 则可,那就构成多态了。《Java 解惑》中其余谜题还讲到多种该注意的地方,能够看看。

小结:

        一、注意掌握重写(override)与隐藏(hide)的异同点:相同点就是二者都是相对于继承树中父类、子类来讲,而不一样点就是其目的以及所形成的效果。别把重写和隐藏混淆在一块儿了;

        二、对于 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");  
    }  
}

小结:

        一、我以为,在文章标题当中的五个名词当中,尤其前面三个最为重要,陷阱炸弹也多多,并且文中所讲仅仅是那么一丁点儿相关的,大量更细节的还得慢慢发现;

        二、就算后面两个名词,也就是这两种状况不常见,但了解一下记在脑里仍是不错的,毕竟为本身增长了专业词汇量;

        三、最后,就是建议各位也看看《Java 解惑》而后也告诉我一些炸弹型陷阱之类的,呵呵...学习快乐!加油!

相关文章
相关标签/搜索