【Java】多态和动态绑定中的坑

大一学C++的时候就感觉多态这部分略烦,小坑不断。一年过后学习Java再次遇到多态的问题,写一篇笔记来记下发现的各种小坑,以供以后复习。


来看下面一段代码:

enum Note {
    MIDDLE_C,
    C_SHARP,
    B_FLAT
}

class Instrument {
    public int field = 0;

    void play(Note note) {
        System.out.println("Instrument.play() " + note);
    }

    String what() {
        return "Instrument";
    }

    void adjust() {
        System.out.println("Adjust Instrument");
    }
}

class Wind extends Instrument {
    public int field = 1;

    @Override
    void play(Note note) {
        System.out.println("Wind.play() " + note);
    }

    @Override
    String what() {
        return "Wind";
    }

    @Override
    void adjust() {
        System.out.println("Adjust Wind");
    }
}

class Brass extends Wind {
    @Override
    void play(Note note) {
        System.out.println("Brass.play() " + note);
    }

    @Override
    void adjust() {
        System.out.println("Adjust Brass");
    }
}

public class Music3 {

    public static void tune(Instrument instrument) {
        instrument.play(Note.C_SHARP);
    }

    public static void tuneAll(Instrument[] instruments) {
        for (Instrument instrument : instruments) {
            tune(instrument);
        }
    }

    public static void main(String... args) {
        Instrument[] orchestra = {
                new Wind(),
                 new Brass()
        };
        tuneAll(orchestra);
    }

}

/*
Output:
Wind.play() C_SHARP
0
Brass.play() C_SHARP
0
*/

当创建orchestra数组时,如果new的是导出类,那么他会自动向上转型成Instrument。因为Wind和Brass毕竟是继承自Instrument的,所以Instrument的接口必定存在于它导出的类中。

在Main函数中,tune方法接收一个Instrument引用,但同时也接受任何导出自Instrument的类。那么在这种情况下编译器怎样才能知道这个Instrument引用指向的是Wind对象,而不是Brass呢?实际上,编译器也不知道。。那么这个问题怎么解决呢?答案是采用动态绑定技术。

动态绑定是指在运行时根据对象的类型,将一个方法调用同一个方法主体关联起来。也就是说编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。因此,tune方法就可以正确调用导出类重写的相应方法。

Note: Java中除了static方法和final方法(private方法也属于final方法)之外,其他所有方法都是动态绑定的。当然,这样做有很多好处,也有一定的麻烦。


我们来看看麻烦在哪里:


class Glyph {
    void draw() {
        System.out.println("Glyph.draw()");
    }

    private void duplicate() {
        System.out.println("Glyph.duplicate()");
    }

    Glyph() {
        System.out.println("Glyph() before draw");
        draw();// 动态绑定,调用子类覆盖的draw
        duplicate();// 非动态绑定,调用Glyph的duplicate
        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);
    }

    @Override
    void draw() {
        System.out.println("RoundGlyph.draw().radius = " + radius);
    }

    public void duplicate() {
        // 这个duplicate和父类的duplicate没有任何关系
        // 不是Override
        // 因为父类的duplicate是private final的
        System.out.println("RoundGlyph.duplicate()");
    }
}

public class PolyConstructors {
    public static void main(String...args) {
        new RoundGlyph(10);
    }
}

/*
Output:
Glyph() before draw
RoundGlyph.draw().radius = 0
Glyph.duplicate()
Glyph() after draw
RoundGlyph.RoundGlyph().radius = 10
*/

首先要说明一下Java初始化对象的实际过程:

  1. 在做任何事情之前,先将分配给对象的存储空间初始化成二进制的零
  2. 自下向上调用基类构造器。这里在Glyph的构造器中会调用覆盖了的draw()方法,而不是Glyph的draw()。由于步骤1的缘故,我们会发现radius的值是0
  3. 完所有基类的初始化后,按照声明的顺序调用当前成员的初始化方法。这里会将radius初始化为1
  4. 调用导出类的构造器主体

第一次看到这样的结果时我的内心其实是拒绝的。。觉得Glyph构造器里调用的draw()方法理所当然的就应当是Glyph.draw()。然而这是一个坑,很容易被忽视掉,而且不好debug。

Note: 在构造器内唯一能够安全调用的方法是基类中的final方法,当然包括private方法。这些方法不会被导出类覆盖,因此也就不会出现掉进上面这个坑的情况了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值