Java编程思想(八)—— 多态

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Alexshi5/article/details/84451222

        在面向对象程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。它的作用主要是消除类型之间的耦合关系。

一、再论向上转型

        对象既可以作为它自己本身的类型使用,也可以作为它的基类使用,而这种把对某个对象的引用视为对其基类型引用的做法被称作为向上转型。因为在继承类的画法中,基类是放置在上方的。

如下示例:

/**
 * author Alex
 * date 2018/11/24
 * description 乐器类
 */
public class Instrument {
    public void play(){
        System.out.println("Instrument play");
    }
}

//管乐器
class Wind extends Instrument{
    public void play(){
        System.out.println("Wind play");
    }
}

class Music{
    private static void beginPlay(Instrument i){
        i.play();
    }

    public static void main(String[] args) {
        Wind wind = new Wind();
        beginPlay(wind);
        //打印结果:Wind play
    }
}

1、方法调用绑定

        将一个方法调用同一个方法主体关联起来被称作绑定,若在程序执行前进行绑定,叫做前期绑定(C++叫静态联编)。如果在程序运行时根据对象的类型进行绑定,叫做后期绑定,也叫做动态绑定或运行时绑定(C++叫做动态联编)。

        Java中除了static方法和final方法(private方法属于final方法),其他所有方法都属于后期绑定。通过动态绑定实现正确的调用,在上例的基础上添加内容,如下所示:

//钢琴
class Piano extends Instrument{
    public void play(){
        System.out.println("Piano play");
    }
}

//二胡
class Erhu extends Instrument{
    public void play(){
        System.out.println("Erhu play");
    }
}

//乐器生成器
class InstrumentGenerator{
    private Random random = new Random();
    public Instrument next(){
        switch (random.nextInt(3)){
            case 0:return new Wind();
            case 1:return new Piano();
            case 2:return new Erhu();
        }
        return null;
    }
}

class Music{
    private static void beginPlay(Instrument i){
        i.play();
    }

    public static void main(String[] args) {
        Wind wind = new Wind();
        beginPlay(wind);
        //打印结果:Wind play
        Instrument[] instruments = new Instrument[9];
        for(int i=0;i<instruments.length;i++){
            instruments[i] = generator.next();
        }
        for(Instrument instrument:instruments){
            instrument.play();
        }
        //打印结果:
        //Erhu play
        //Wind play
        //Wind play
        //Erhu play
        //Piano play
        //Wind play
        //Wind play
        //Wind play
        //Wind play
    }

    private static InstrumentGenerator generator = new InstrumentGenerator();
}

        在上面的例子中,InstrumentGenerator类相当于个工厂,在我们每次调用next()方法时,它将随机产生一个Instrument类型的引用,而向上转型是发生在return的时候,在调用next()方法时,我们是不可能知道具体类型是什么的,因此我们只能获得一个通用的Instrument类型的对象,对play()方法的调用都是通过动态绑定进行的。

        由于有多态机制,我们可以根据自己的需求对系统添加任意多的新类型,而不需要更改任何方法。在一个设计良好的OOP程序中,大多数方法都会与基类接口通信,这样的程序是可扩展的,因为可以通过通用的基类继承出新的数据类型,从而添加一些新的功能,那些操纵基类接口的方法,不需要任何改动就可以应用于新类。

2、静态方法的行为不具有多态性

        因为静态方法是与类相关联的,而不是与单个的对象相关联的。对比如下:

/**
 * author Alex
 * date 2018/11/24
 * description 一个含有静态方法的超类
 */
public class StaticSuper {
    public static void fun1(){
        System.out.println("StaticSuper fun1");
    }
    public void fun2(){
        System.out.println("StaticSuper fun2");
    }
}

//静态超类的子类
class StaticSup extends StaticSuper{
    public static void fun1(){
        System.out.println("StaticSup fun1");
    }
    public void fun2(){
        System.out.println("StaticSup fun2");
    }
}

//测试类
class StaticTest{
    public static void main(String[] args) {
        StaticSuper s = new StaticSup();
        s.fun1();
        s.fun2();
        //打印结果如下:
        //StaticSuper fun1
        //StaticSup fun2
    }
}

二、构造器与多态

1、构造对象的计数器

示例如下:

/**
 * author Alex
 * date 2018/12/2
 * description 一个演示计数器的类,用于统计当前对象创建的数量
 */
public class Counter {
    //静态的成员变量每个类只有一份数据,用于共享对象引用数
    private static long count = 0;
    //final变量不可修改,它是非静态成员变量,为每个对象所有,而在此对象的生命周期内不可修改此变量的值
    private final long counter = ++count;

    public Counter(){
        System.out.println("正在实例化Counter" + counter);
    }

    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        //打印结果如下:
        //正在实例化Counter1
        //正在实例化Counter2
    }
}

2、构造器内部的多态行为

        一个动态绑定的方法调用会向外深入到继承层次结构内部,它可以调用导出类的方法。然而这个调用的效果可能相当难以预料,因为被覆盖的方法在对象被完全构造之前就会被调用,这会导致一些难以发现的错误。

示例如下:

扫描二维码关注公众号,回复: 4364749 查看本文章
public class Animal {
    void eat(){
        System.out.println("Animal eat");
    }
    Animal(){
        System.out.println("Animal eat() 之前");
        eat();
        System.out.println("Animal eat() 之后");
    }
}

class Dog extends Animal{
    private int num = 1;
    Dog(int a){
        this.num = a;
        System.out.println("Dog初始化 num = " + num);
    }
    void eat(){
        System.out.println("Dog eat() num = " + num);
    }
}

class AnimalTest{
    public static void main(String[] args) {
        Dog dog = new Dog(5);
        //打印结果如下:
        //Animal eat() 之前
        //Dog eat() num = 0
        //Animal eat() 之后
        //Dog初始化 num = 5
    }
}

        从调用结果来看,似乎的确是我们想调用的方法,但num的结果不是初始化的1而是0,这是由于导出部分在当前构造器调用时仍没有被初始化。继承结构的实际初始化过程如下:

① 在任何动作之前,将分配给对象的存储空间初始化成二进制的0;

② 然后调用基类构造器,调用被覆盖后的eat()方法(它发生在Dog()构造器被调用之前),此时num为0;

③ 按照声明的顺序调用成员的初始化方法;

④ 调用导出类的构造器主体;

⑤ 按照声明的顺序调用导出类的成员方法。

        在实际编程中,为了避免如上情况的发生,应尽量避免在构造器调用其他方法,如果需要调用,应该将方法声明为private或final方法(private为隐式的final方法),这些方法不能被覆盖,也就不会发生多态行为。

三、用继承进行设计

        在设计时更好的方式首先是组合,其次才是继承。组合具有比继承更好的灵活性,它可以动态的选择类型(也就选择了行为);而继承在编译时就需要知道确切的类型。一条设计的准则是:继承表达行为之间的差异,用字段表达状态上的变化。

1、纯继承与扩展

        采用纯粹的方式来创建继承层次结构是比较好的方式,也就是说,只有在基类中已经建立的方法才可以在导出类中被覆盖,这被称作是纯粹的“is-a”的关系,因为一个类的接口已经确定了它应该是什么,继承可以确保所有的导出类具有基类的接口,而且绝对不会少。这也可以被认为是一种纯替代,因为导出类可以完全替代基类,而在使用它们时完全不需要知道子类的额外信息,基类可以接收发送给导出类的任何信息,因为二者有完全相同的接口,这些都是通过多态来完成的。

        但是,继承基类的一个重要目的是为了扩展基类,即在导出类中添加基类所不具有的特殊接口,这时导出类接口中的扩展部分就不能被基类访问。因此,此时进行向上转型就不能调用那些方法。

2、向下转型与运行时类型识别

        由于向上转型会丢失具体的类型信息,通过向下转型,也就是在继承层次中向下移动,应该能够获取类型信息。然而,我们知道向上转型是安全的,因为基类不会具有大于导出类的接口。但向下转型却不是安全的,因为导出很可能具有比基类更多的扩展接口。所以在Java中,所以的转型都会得到检查,以便确保它是我们所希望的类型,如果不是,就会返回一个类转型异常(ClassCastException)。这种在运行期间以类型进行检查的行为被称作“运行时类型识别”。

示例如下:

public class Info {
    void fun1(){
        System.out.println("Info fun1()");
    }
}

class MoreInfo extends Info{
    void fun1(){
        System.out.println("MoreInfo fun1()");
    }
    void fun2(){
        System.out.println("MoreInfo fun2()");
    }
}

class InfoTest{
    public static void main(String[] args) {
        Info info = new Info();
        Info moreInfo = new MoreInfo();
        info.fun1();
        moreInfo.fun1();

        //((MoreInfo)info).fun2(); 转型失败,报:ClassCastException
        ((MoreInfo)moreInfo).fun2(); //向下转型成功
    }
}

猜你喜欢

转载自blog.csdn.net/Alexshi5/article/details/84451222
今日推荐