类的族谱

  最近刚把《java编程思想》第十章内部类看完,有关类的介绍也就告一段落,而我脑子里始终萦绕的是那些继承、接口、内部类以及它们各自琐碎的语法,让我不知如何是好。我决定抛开这些细枝末节,重构一下我对对象组成的新理解,把以上那些名词编制成网,方便记忆和理解。其中若有不对的地方,欢迎指正,谢谢大家。同时这篇文章我决定换一种轻松明朗的书写风格,让大家看的愉快。

 

1.类的理解。

  从我开始学Java时,知道类被作为一种数据类型,可以自己随意造类时,我觉得惊讶不已。因为我觉得程序中只能包含26个字母以及10个数字,加上一些运算符号,这些才是计算机所能识别的语言,要不然电脑怎么了解你要传递的信息呢。而Java通过编写一个类,往这些类里面塞变量、方法,这个类被编译执行,最终表现形式也还是转化为以上提到的那些符号,来跟虚拟机交流。所以类应该算是一个中间层的角色。它封装着我们对事物的理解,在这个层面上,我们用Java提供的语法写类,就像我们用熟悉的词语、语法写一篇文章。虚拟机会把我们的“文章”用它熟悉的语言翻译一遍,在这个过程中,我们之前的一行行代码都被拆解,最终如一个完整的n*n拼图被打乱一样。我们很难从零乱的拼图中窥见完整图形的原貌,但实际上这是虚拟机按自己的思维又重新拼的图案。虚拟机得到了信息之后它就知道自己该干什么,接下来的事我们也就无需过问了。毕竟我们最最重要的事怎么写出一个个类来准确表达信息

  通常我们用许多属性来描述一件事物。古人说:一阴一阳之谓道。如果把阴理解为“静”,阳理解为“动”,那么万事万物都具有动静两种属性。例如我们人都具备的眼睛、鼻子、手、脚,这些都属于静态属性,它成了人的特征;此外人还具有动态属性,能吃喝打闹,蹦蹦跳跳,它成了人的行为。静态属性对应的就是类的成员变量,动态属性对用的则是方法。于是按这种思想把一种事物抽象成类,就具有了实实在在的意义。

 

2.继承

  有很多伟大的科学家取得重要成就时,总会谦虚地说:我是站在巨人的肩膀上的。说明很多成功都是利用现成的资源,没必要自己去重新走老路造轮子。我对继承的理解同此,如果一个初始类类用一个圆来表示,继承的使命就是扩展初始类,也就是让这个圆变大,可以包含更多属性。它通过在初始圆的外层再画一个圆,把旧圆包含进去,达到目的。此外还有一种常用方法(组合)来扩展类:建一个新类,将新类的成员变量为设置初始类的引用。也就是新类存着一把可以访问初始类的钥匙。类似这样的钥匙越多,这个类可以访问的属性也就越多,也达到了扩展类的目的。而这两种扩展类的方式的使用场景是怎样的呢?一言以蔽之:根据两个类的所属关系。类A -交通工具,类B表示汽车,类C表示发电机。汽车是这一种特殊的交通工具,即B is A,所以B extends A;汽车里有发电机,即B has C,所以C c可作为B的成员变量。

 

3.多态

  因为继承,所以多态。(这里的继承也包括implements)多态给人印象最深的还是它的向上转型加动态绑定。写了这么多字,还是来一段“名言”吧:

 

class Pig{
    public void getInfo() {
        
    }
}
//麦兜
class MaiDou extends Pig{
    public void getInfo() {
        System.out.println("两情若是久长时,又岂在猪猪肉肉");
    }
}
//佩奇
class PeiQi extends Pig{
    public void getInfo() {
        System.out.println("小猪佩奇身上纹,掌声送给社会人");
    }
}

public class Test {
    public static void show(Pig pig) {
        pig.getInfo();
    }
    
    public static void main(String[] args) {
        Pig md = new MaiDou();
        Pig pq = new PeiQi();
        show(md);
        show(pq);
    }
}
//output:
//    两情若是久长时,又岂在猪猪肉肉
//    小猪佩奇身上纹,掌声送给社会人

  这段代码中只要理解Pig md = new MaiDou(),以及show()的调用过程就行。首先,创建麦兜对象,将这个对象引用向上转型赋给Pig,说明麦兜是猪,就像海水是水一样合乎情理。后面show()方法接受pig类型的参数,然后getInfo()调用了我们期盼的结果,为什么会这样。这源于Java语言的动态性。Java中有个名词:绑定。意思是:方法的调用与方法所在的类进行关联。绑定分为:静态绑定,动态绑定。静态绑定是指:在编译Java文件时就已经知道方法对应的是哪个类。常见的的私有方法、final 方法、静态方法、构造方法都是静态绑定。这四种方法的共同点就是,要么不能被继承(如私有方法、final方法、构造方法),要么被继承也不能被重写(如静态方法),于是这些方法就只和声明它的那个类文件进行了绑定。动态绑定则有趣的多。来我们分析一下pig.getInfo():

  1.在编译的时候:编译器对这个方法只做一个很简单的判断:pig(及其它的父类、父父类。。)有没有getInfo()方法。没有就报错,有就通过编译。

  2.在运行期,找到md实际引用的那个对象,也就是麦兜。老规矩,查看麦兜(及其它的父类、父父类。。)中的所有getInfo()方法,哪个方法参数与当前这个方法参数最相符,就调用哪个。

  多态就是基于动态绑定实现的。了解了这个过程,多态运行的结果也就理所应当。

 

4.接口

  如果你想扩展你的类,不能总是依赖继承,继承让你的对象像滚雪球一样越滚越大,其中还掺杂着你可能不需要的属性方法。例如:你想飞,那就继承鸟吧,顺便给你一层厚厚的羽毛,以及捕虫子的方法-囧。所以我们需要一种轻巧的扩充类的方法,这就是接口的意义。接口的实现其实与继承的意思相差无几,所以实现接口也可以说继承接口。一个类可以继承一个父类,但可以实现多个接口,简单的可以直接理解为多重继承。另外接口体现在多态上的范围要比继承在多态上的范围还要宽广。举个例子:当你的方法参数为一个鸟类时,你传递的参数只能是鸟类和鸟类的导出类,但如果你的方法参数为一个飞行接口,任何继承(实现)这个接口的类都可以传,那我可以传鸟类和它的导出类,还可以传飞机类和它的导出类。

  关于接口,还有一点值得一提。上面说到接口作为一种更轻巧的方式扩充类,我们常见的情况是接口最常见的是只定义抽象方法,而很少定义变量,即使定义了变量,也必须把它声明为一个初始化的常量。这个原因网上很多说的比较明白,这里我简单回顾一下:接口是为了定制统一的规约,它要达到这个目的,你只能改我允许你改的部分,其余的部分你只能用我的。接口已经把方法扩展的权利拱手相让了,如果它里面的变量都还可以被实现类修改,那它个规约的所有地方都能被修改,那算什么规约!所以有了static让接口的变量被接口所公有,而不是每个实现类独占一份;有了final关闭了实现类修改的权利。还有一个原因是我自己琢磨的:多态是在方法调用上体现出来的,而成员变量并不具备多态的特征。

 

class Father{
    int age = 45;
}
class Son extends Father{
    int age = 21;
}

class Daughter extends Father{
    int age = 24;
}

public class Test {
    public void showAge(Father f) {
        System.out.println(f.age);
    }
    
    public static void main(String[] args) {
        Son son = new Son();
        Daughter daughter = new Daughter();
        Test t = new Test();
        t.showAge(son);
        t.showAge(daughter);
    }
}
// output
// 45   45

 

  f.age()并没有如我们期望那样,找到儿子、女儿对象,分别显示他们的年龄,也就是多态没有体现出来。所以接口中的变量不能在实现过程中不能表现出多态,如果继承不是为了多态,那么继承毫无意义!那还不如就声明为常量。当然这个理由,并没有前面的有力,只当自己的一种看法与大家探讨。

 

 

5.内部类

  内部类作为最后的大佬总算要登场了!内部类的定义很简单,在类的里面再定义一个类,里面的那个类就是内部类。根据内部类所处的位置以及修饰符,于是有了位于成员变量同一级别的成员内部类,位于方法中(也可以说是代码块中)的局部内部类,用static修饰的嵌套类,以及大名鼎鼎的匿名内部类。在介绍在四种内部类之前,我们先看看为什么要有内部类,总得找个让人信服的理由。一句话概括就是:内部类的出现就是为了完善java的多重继承(再次重申一次,全文中的继承都可以理解为继承+实现)机制。怎么个完善法呢?先前用接口时,我们是这样扩充类的:A extends B implements C,D,E... ,这样A 的肚子了就装下了B,C,D,E。但实际运用过程中,会发现有些情况需要的继承结构更细致精确。举个例子:一个女孩觉得自己的鼻子不好看,想把鼻子整个容,这时候,没必要整个人全身上下都整,她仅仅只需要整的是她这个对象的某个成员变量(鼻子)。也就是对她的鼻子实现整容接口,而不是让她整个人实现整容接口。还有一些情况,如:太空飞船实现了航天接口,为了飞向宇宙探险,而飞船中有一个逃生舱,它也实现了航天接口,对同一接口,它的行为是为了返回地球。类与它的成员都要实现同一个接口,这种情况也只能使用内部类实现。从这两个例子可以看出使用继承多个接口的方式还是过于粗犷,内部类将继承关系拆解到类的里面,从而达到更准确更有针对性的继承。而且内部类还提供了相应的技术支持,那就是在定义内部类的时候,我们可以在内部类里面访问外部类的所有成员,不管你是方法还是成员变量,不管你是私有还是公开。外部类对它而言就是打开自家冰箱。在这里,顺便提一点我经常会犯的错误。书中提到:当生成一个内部类对象时,它能访问其外围对象的所有成员。我当时的理解是:

 

public class Test {
    private int a =10;
    class Father{
        public int getVar() {
            return a;
        }
    }
    public Father getFather() {
        return new Father();
    }
    public static void main(String[] args) {
        Test t = new Test();
        Test.Father f = t.getFather();
        f.a//f根本不能访问外围类的变量a
    }
}

 

  我想当然的理解为内部类对象可以直接点点点,父类的成员。后来才知道,Father与Test是两个独立的class文件,class定义的时候,压根就没有a,怎么能访问呢?书中的那句话我的理解是,它可以畅通无阻的访问外围类的所有成员是在定义class的时候,也就是我们在写Test这个类时,所有的成员都在这同一个类里,根据内能访问外的规律,所以内部类可以外围类的所有成员。而在实际运行过程中,外围对象跟内部类对象关系也很紧密,因为内部类可以访问修改外部类。只要你要编写内部类的时候,写了相应的方法就行,如上面代码中的getVar(),我用f.getVar()还是可以拿到a 的。这里注意一下:就算你外部类的所有的方法变量都是私有的,只要你的内部类里提供了访问外部类成员的方法,那么只要得到了内部类对象,也就得到了打开外部类的钥匙。这就是内部类被称作闭包的原因吧。

 

  另外,四种不同的内部类,实际用的最多的应属匿名内部类和成员内部类了,

 

interface Fly{
    void fly();
}
public class Test {
    class Bird implements Fly{
        @Override
        public void fly() {
            System.out.println("成员内部类实现了接口");
            
        }
    }
    public Fly getBird() {
        return new Bird();
    }
    
    public void show(Fly f) {
        f.fly();
    }
    
    public static void main(String[] args) {
        Test t = new Test();
        Fly bird = t.getBird();
        t.show(bird);
        t.show(new Fly() {
            @Override
            public void fly() {
                System.out.println("匿名内部类实现了接口");    
            }
        });
        
    }
}
//output
//成员内部类实现了接口
//匿名内部类实现了接口

 

   匿名内部类是浓缩版的成员内部类,它的显著特征是没有构造器。所以如果你想要继续创造内部类实现对象,那就用成员内部类好了;如果只需要一个继承接口(抽象类)的对象,那么匿名内部类更简洁。另外还有局部内部类和静态内部类,用的少我就不多讲了,而且当你理解了java方法的动态绑定特征后,理解内部类的实际调用情况简直so easy!

 

  

  

  

posted on 2018-08-29 13:01  荒郊野岭一根葱  阅读(132)  评论(0编辑  收藏  举报