十一、Java多态原理与使用详解

概述

Java 中最常见的一种操作是封装,封装是将特征和行为合并起来形成一种新的数据类型,可以实现将细节隐藏、私有化。使用者可以看到该看到的,看不到不该看到的,可以有效的避免一些误操作。

其次,Java 中的继承,可以实现类的扩展,也就是将一个笼统的类通过继承机制产生更多的细分类,例如,动物类是笼统的,具有所有动物的特性和行为,而老虎类,蚂蚁类均继承自动物类,它们本质是动物类,但是在动物类的基础上扩展了新的行为和特征。

由于继承机制的特殊性,可以将一个子类当做父类来使用,这就是多态,多态可以消除类之间的耦合关系

实现多态:向上转型

一个对象,既可以当做它本身的类型来使用,也可以当做它的基类类型来操作,将一个对象类型转型为基类被称为向上转型,由于导出类与基类的关系是 “is a”,因此,向上转型是安全的。

首先来看一段向上转型的代码:

class fu {
    public void play() {
        System.out.println("fu_play()");
    }
}

class zi1 extends fu {
    public void play() {
        System.out.println("zi1_play()");
    }
}

class zi2 extends fu {
    public void play() {
        System.out.println("zi2_play()");
    }
}

public class text() {
    public static void tune(fu obj) {
        obj.play();
    }
    
    public static void main(String [] args) {
        zi1 a = new zi1();
        zi2 b = new zi2();
        tune(a);
        tune(b);
    }
}
/*Output:
zi1_play()
zi2_play()
*/

上面这个例子很好的说明了多态是如何消除类型直接的耦合关系的,并且有效的减少了代码的复杂度。

tune()方法是用于执行某个对象的play()方法,而为了可以增加该方法的通用性,参数为基类类型,因此当调用tune()方法,传入的参数是子类对象时,对象被动转型为父类类型,而实际调用的方法依旧是对象自身类型的方法。

这里可以理解为是披着父类外衣的子类对象。

上述代码中还体现出一个多态的优点,就是可扩展性,想象一下假如没有多态,那么tune方法需要针对各个类型重载一个方法,当父类新增子类时,也需要增加。

多态内部原理

要想理解多态的实现原理,首先要了解方法调用绑定

C语言中在调用某个函数时,调用的语句和函数体在编译时就已经绑定,因此运行时,调用该函数即可直接定位到该函数的具体位置,这被称为前期绑定

显然,在多态的情况下,前期绑定是无法满足的,实际上,多态中是使用后期绑定来解决这个问题的,那么,后期绑定的原理是什么样的呢?

后期绑定,指的是在编译时期,方法调用语句不会和具体的方法体绑定,而是在运行时根据具体参数对象来确定需要执行的方法,这也被称为动态绑定运行时绑定

向上转型的缺陷:域和静态方法

多态虽然具有这么多好处,同样也会有其缺陷,通过对多态的介绍,你可能会以为除了方法调用,其他一切也都是多态的,实际并不是,先看一个栗子:

class Super {
    public int field = 0;
    public int getField() {
        return field;
    }
}

class Sub extends Super {
    public int field = 1;
    public int getField() {
        return field;
    }
    public int getSuperField() {
        return super.field;
    }
}
public class test {
    public static void main(String [] args) {
        Super sup = new Sub();
        Sub sub = new Sub();
        System.out.println(sup.field + "----" + sup.getField());
        System.out.println(sub.field + "----" + sub.getField() + "----" + sub.getSuperField());
    }
}
/*Output:
0----1
1----1----0
*/

通过上述代码可以发现,成员变量是不具有多态特性的,如果要从一个父类类型的子类对象处得到父类的成员变量,则需要使用super关键字获取。

这里我们可以这样理解,代码中的Super sub = new Sub();sub对象的类型是父类型的,实体是子类型的,实际在访问成员变量时获取的是Super类中的,所以成员方法是以对象区分,成员变量是以类型区分。即成员变量不具有多态特点。
多态内存图解
如上图所示,在完成第一步加载任务后,所具有的方法和对象的对应关系,我们知道,成员变量是一个对象的外在特性,成员方法是一个对象的内在行为,因此,在获取对象的成员变量时,以外在类型为区分,而调用方法时,以对象实际类型来区分。

同样的,静态方法也不具有多态性,因为静态方法是与类关联,而不是与当个对象关联。

因此我们在实际开发中应当避免基类与导出类中成员变量、静态方法的命名相同,否则会引起混淆。

构造器内部的多态

在继承体系中,new 一个子类对象时构造器调用顺序如下:

  1. 从最顶层的基类开始往下构造
  2. 按照声明顺序调用成员对象的构造方法
  3. 调用当前子类的构造方法

这样的调用顺序在多态中会出现一个问题,就是如果在构造方法中调用了一个动态绑定的方法会怎么样呢?我们知道,动态绑定的方法只有在运行时才知道该调用哪个类中的方法。而构造器没有执行完成,也就是初始化未完成时调用方法,并且该方法中使用到的成员还未初始化,那么这肯定会出现问题。

如下代码:

class Fu {
    void drow() {
        System.out.println("Fu----drow()");
    }
    
    Fu() {
        System.out.println("Fu----drow()----Before");
        drow();
        System.out.println("Fu----drow()----After");
    }
}

class Zi extends Fu {
    private int i = 10;
    Zi(int i) {
        this.i = i;
        System.out.println("Zi----i=" + i);
    }
    void drow() {
        System.out.println("Zi----drow()---i=" + i);
    }
}

public class test {
    public static void main(String [] args) {
        new Zi(20);
    }
}
/*Output:
Fu----drow()----Before
Zi----drow()---i=0
Fu----drow()----After
Zi----i=20
*/

通过上述代码可以发现,当创建子类对象,按顺序开始从父类构造调用时,父类的构造器中调用了draw()方法,此时子类对象并未完成初始化,从输出结果可以发现,父类中构造器调用的draw()是子类中的draw(),而输出的成员变量i不是10,却是0,思考一下是为什么呢?

之前我们说到继承体系中构造器的执行顺序时,其实还遗漏了一条,在创建子类对象时,执行顶层基类构造器前有一步操作,就是将分配给这个对象的存储空间初始化为二进制的0,由于这一步的原因,上面代码中父类构造器在调用子类的draw()时,成员变量i所处的存储位置为0,如果i是引用类型的话,那么值是null

其实,在编写构造器代码时有一个准则,就是用尽可能简单的方法使对象进入正常状态,如果可以,避免调用其他方法

构造器中唯一可以调用并且不会出现上述问题的是final的方法(包括private方法),因为这些方法不会被覆盖重写。

向下转型与 instanceof

向上转型有一个弊端,就是转型完成后,作为子类对象,却无法使用子类对象特有的方法了。
向上转型

因此,当我们拿到上图中这个obj对象时,如果想要调用Zi类特有的方法,则需要向下转型,即转为子类类型。

但是在向下转型中,是有风险的,比如将一个多边形转型为三角形,但是如果这个多边形实际是一个圆形,那么转型会失败。Java 中所有转型都会得到检查,不论是编译期还是运行期,都会对对象转型进行检查(这被称为运行时类型识别,英文缩写为RTTI)。

class Fu {
    f() {}
    g() {}
}
class Zi extends Fu {
    f() {}
    g() {}
    u() {}
    x() {}
}
public test {
    public static void main(String [] args) {
        Fu f = new Fu();
        Fu z = new Zi();
        ((Zi)f).u();// 编译时报错,因为 u 方法只存在于 Zi 类中
        ((Zi)z).u();
    }
}

那么如果我们不能确切的知道这个基类型对象的实际类型是什么,并且需要按照不同子类型做不同的操作,该如何正确的进行向下转型呢?

可以使用关键字 instanceof 来判断类型。

public void eat(Animal a){
    if(a instanceof Dog){// 判断是否为 Dog 类
        ....  //执行 Dog 类中特有方法
    } 
    if(a instanceof Cat){// 判断是否为 Cat 类
        ....  // 执行 Cat 类中特有方法
    } 
    a.eat();// 执行 Fu 类方法
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值