您的位置:首页 > 编程语言 > Java开发

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

2019-02-14 23:24 621 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/m0_37264516/article/details/87307384

概述

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 类方法
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: