Thinking in Java第八章学习笔记----多态

Deadly 提交于 2020-01-14 12:18:26

封装:

  封装就是把对象的行为和属性装在一个类里。

抽象:

  抽象是将存在于事物之间共同拥有的元素提取的过程,抽象成类或者接口。抽象分为:

  1)数据抽象:用于表示事物的属性,比如人的姓名、年龄等。抽象成属性或者成员变量。

  2)过程抽象:用于表示事物的行为,比如人会吃饭等。抽象成方法。

多态:

  在面向对象程序设计语言中,多态是继封装和抽象的第三大特性。首先,须清楚多态是一种不能单独来看的特性,它只能作为类中"全景"关系中的一部分,与其他特性协同合作。多态的主要功能是将做什么和怎么做分离开来,从另一角度将接口和数据分离开来。多态不仅能改善代码的组织结构和可读性,还能创建可扩展的程序。

  概括来说,多态的作用是消除耦合。没有多态,等号左边和右边需要相互对应,即耦合。有了多态,左边是父类(或者接口),右边是子类(或实现类),我只管调用接口里面的方法就是了,至于你实现类怎么去实现,那是你的事,你要修改一下实现,你只管去把实现类换掉,我这边一行代码都不用变,这就解耦了。

再论向上转型:

  把导出类对象的引用视为其基类的引用,这种做法称为向上转型。因为在继承树的画法中,基类是处于上方的。

问题导出:

  如果我们只写一个简单的方法,它仅接收基类引用作为参数,而不是写多个以导出类引用为参数的类似方法,并且这个方法对所有的导出类参数都可以正确运行,显然这种做法更好。

问题解决:

  当只存在基类引用作为参数的方法时,如何让编译器准确判断调用对象的类型,从而在方法内准确调用恰当的方法(因为方法可能重载了,若无法正确判断是哪个对象,就无法正确调用)。解决的办法是后期绑定,也称动态绑定或运时绑定,当前状态我们只需了解动态绑定依赖某种机制实现的,而且Java中除了final(注意priva为隐式final)和static方法,其他所有的方法都是后期绑定。

  当我们了解到动态绑定来实现多态,即消除耦合时,我们就可以编写只与基类打交道的代码了,并且适用于其导出类。

问题延伸:

  使用final的另一重要原因可能是为了关闭动态绑定,这样,或许编译器就可以为final方法调用更加有效的方法,但是大多数情况下对系统性能并没有多大改善。所以在对final的使用中,应以实际设计来决定,不应为了性能盲目选择final。

几点注意:

  对于基类中的私有方法,是不能被重载的,即使导出类中有同名的方法,也是视为新方法。

  域是由编译器直接解析,无多态;static和final方法不可重载,无多态。

  构造器是特殊的方法,不具有多态性,因为隐式static。

构造器的调用顺序:

  首先,在导出类的构造器中,总是会先调用基类的构造器,若没有明确指定,则自动调用默认构造器,若基类中重写了构造器的话,编译将报错。

  具体调用顺序为:

  1)在其他任何事情发生之前,将分配给对象的存储空间初始化成二进制的零。

  2)调用基类构造器,一直到最顶层,再往下。

  3)按声明顺序调用成员的初始化方法。

  4)调用导出类构造器主体。

  对于1)的理解,参见代码:

class Glyph { 
  void draw() { System.out.println("Glyph.draw()"); }
    Glyph() {
        System.out.println("Glyph() before draw()");
        draw();
        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);
    }
    void draw() {
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
    
    void superDraw(){
        super.draw();
    }

}
public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}
/*output:
    Glyph() before draw()
    RoundGlyph.draw(), radius = 0//在还没有创建RoundGlyph之前,radius还没有被初始化成1,而是一开始编译器知道有这个成员,先给它赋值为0。
    Glyph() after draw()
    RoundGlyph.RoundGlyph(), radius = 5
*/

关于清理:

  对象的清理应该和初始化顺序相反,因为可能存在子对象依赖于其他对象的情况。

  基类的清理,应该先对其导出类进行清理,然后才是自身。

  总之,逆序。

 协变返回类型:

  导出类覆盖基类方法时,返回的类型可以是基类方法返回类型的子类。

class Grain {
    public String toString(){
        return "Grain";
    }
}
class Wheat extends Grain{
    public String toString(){
        return "Wheat";
    }
}

class Mill{
    Grain process(){
        return new Grain();
    }
}
class WheatMill extends Mill{
    @Override
        Wheat process(){   //重载方法的返回类型是Wheat,而基类中的返回类型是Grain。注意只有Wheat是Grain的导出类时,才允许这种写法,称为协变返回类型。
        return new Wheat();
    }
}

 向下转型:

  我们知道,向上转型是安全的,因为基类不会具有大于导出类的接口。但是向下转型,却不一定,所以必须通过运行时类型识别(RTTI)来确保。

  向下转型是通过强制转换实现的,即将基类对象引用强行指向为导出类对象。如果所转类型是正确的,则转型成功;否则,编译器将会返回一个ClassCastException异常。

package com.music;

class Useful {
    public void f(){}
    public void g(){}
}

class MoreUseful extends Useful {
    public void f(){}
    public void g(){}
    public void u(){
        System.out.println("1");
    }
}

public class RTTI {
    public static void main(String[] args) {
        Useful[] x = {
                new Useful(),
                new MoreUseful() //向上转型
        };
        x[0].f();
        x[1].g();
        ((MoreUseful)x[1]).u();//向下转型,因为x[1]指向MoreUseful对象,所以可以向下转型。换句话说,先是向上,再向下才允许。
        ((MoreUseful)x[0]).u();//虽然可以编译成功,但是会报ClassCastException异常。
    }
}

总结:

  虽然似乎所有的东西都可以被继承,但是如果在任何创建新类时,都首先考虑使用继承技术,反而会加重我们的设计负担,这一点同样适用于设计模式的使用上。更好的方式仍然是优选组合,尤其是不能十分确定使用哪一种方式时。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!