面向对象05[深入理解Java多态]

前言

多态是继封装、继承之后,面向对象的第三大特性。从一定角度来看,封装和继承几乎都是在为多态而准备的。这是面向对象三大特性里的最后一个概念,也是最重要的知识点。因为在面向对象编程(OOP)中,多态机制无疑是其最具特色的功能,甚至可以说,不运用多态的编程不能称之为OOP。这也是为什么有人说,使用面向对象语言的编程和面向对象的编程是两码事


面向对象三大机制


Java中的引用和指向

在详细讲解什么是多态前,不妨先来了解一下两个概念:引用指向

如果一个变量的类型是 类类型,而非基本类型,那么该变量又叫做引用

new Student();

代表创建了一个Student对象

但是也仅仅是创建了一个对象,我们没有办法访问它

为了访问这个对象,会使用 引用代表 这个对象

Student stu = new Student();

stu这个变量是Student类型的,又叫做引用

“=” 的意思就指的是stu这个引用 代表 右侧创建的对象

“代表” 在面向对象里,又叫做 “指向”


什么是多态

在聊完Java中引用和指向的概念后,我们回到主题:什么是多态?

在维基百科中多态的行为被描述为:在面向对象编程理论中,不同类型的对象响应同一名称的方法调用的能力。每个调用都是根据特定类型的行为进行的。
同一方法根据发送对象的不同,而采取多种不同的行为方式。简单来说即:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。

不知道是否有不少人在听完定义后,已经是 的状态了。

现实生活中关于多态的例子数不胜数,这里举两个简单的例子你就明白了

1.动物(Animal)这个类,可以包含有猫(Cat)这个类、狗(Dog)这个类、猪(Pig)这个类等等…
当我们让这三类动物发出叫这个动作时,它们会用各自的发声方式去实现这个动作,产生不同的执行结果:喵、汪、哼

2.电脑上的快捷键:当我们按下F1键这个动作,如果是在Flash界面下弹出的就是AS3的帮助文档;如果当前在Word界面下弹出的就是Word帮助;在Windows界面下弹出的就是Windows帮助和支持。


在我们理解了现实中的多态后,那么在Java面向对象编程中多态分为哪几类以及如何实现呢?

多态的分类、实现方法、实现机制(扩展)、多态的用途

多态的分类

本质上多态分为两种:

  • 编译时多态(又称静态多态)
  • 运行时多态(又称动态多态)

重载(overload)是编译时多态的表现形式,体现在同名方法中有着不同的传入参数。但这些都是在编译时就已经确定了的,运行时调用的是确定的方法。并且我们要十分清楚地知道,重载是发生在同一类中的,与什么父类子类、继承毫无关系!

public int add(int a){
}
public int add(int a,int b){
}
public double add(int a,double b){
}

我们通常说的多态指的都是运行时的多态,也就是编译时不确定究竟调用哪个具体的方法,一直延迟到运行时才能确定。
重写(Override)是运行时多态的表现形式,重写是发生在子类中的,也就是说必须要有继承的情况下才有重写发生。我们知道继承一个类,也就有了父类全部的方法,如果你感到那个方法不爽,功能需要改变,那么就把那个方法在子类中重新实现一遍。这样调用这个方法的时候,就是执行子类中方法的过程了。父类中的方法就被覆盖了。(当然,覆盖的时候方法名和参数列表要和父类中完全一样,不然你的方法对父类中的方法就不起任何作用了,因为这是两个方法,毫无关系)

多态的实现方法

  • 子类继承父类(extends)
  • 类实现接口(implements)

无论是哪种方法实现多态,其核心都在于对父类方法的重写或对接口方法的实现,以使得在运行时采用不同的方式,产生不同的执行结果。

要使用多态,在声明对象时就应该遵循一条原则:声明总是父类类型或接口类型,创建的是实际类型。举个栗子:假设我们需要创建一个ArrayList对象,那么声明对象时就应该这样:

List list = new ArrayList();

而不是:

//这是我们在调用非静态方法时,将类实例化并用变量接收的操作
ArrayList list = new ArrayList();

在定义方法参数时通常也应该总是优先使用父类类型或接口类型,例如doSomething方法应该写成:

public void doSomething(List list);

而不是:

public void doSomething(ArrayList list);

这样声明的最大好处在于结构的灵活性高:如果某一天我认为ArrayList的特性无法满足我现在的需要,我希望用LinkedList来代替它,那么我只需要在对象创建的地方把new ArrayList()改为new LinkedList()即可,其它的代码一概不用改动

实现机制及扩展

动态绑定(dynamic binding),又称延迟绑定
是指JVM在执行程序期间会自动判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。(当然这个过程对程序员来说是透明的)

扩展:

在JVM执行Java字节码时,类型信息被存放在方法区中,通常为了优化对象调用方法的速度,方法区的类型信息中增加一个指针,该指针指向一张记录该类方法入口的表(称为方法表),表中的每一项都是指向相应方法的指针。
方法表的构造如下:
由于Java的单继承机制,一个类只能继承一个父类,而所有的类又都继承自Object类。方法表中最先存放的是Object类的方法,接下来是该类的父类的方法,最后是该类本身的方法。这里关键的地方在于,如果子类改写了父类的方法,那么子类和父类的那些同名方法共享一个方法表项,都被认作是父类的方法。
注意这里只有非私有的实例方法才会出现,并且静态方法也不会出现在这里,原因很容易理解:静态方法跟对象无关,可以将方法地址直接引用,而不像实例方法需要间接引用。
更深入地讲,静态方法是由虚拟机指令invokestatic调用的,私有方法和构造函数则是由invokespecial指令调用,只有被invokevirtual和invokeinterface指令调用的方法才会在方法表中出现。
由于以上方法的排列特性(Object——父类——子类),使得方法表的偏移量总是固定的。例如,对于任何类来说,其方法表中equals方法的偏移量总是一个定值,所有继承某父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。
前面说过,方法表中的表项都是指向该类对应方法的指针,这里就开始了多态的实现:
假设Class A是Class B的子类,并且A改写了B的方法method(),那么在B的方法表中,method方法的指针指向的就是B的method方法入口。
而对于A来说,它的方法表中的method方法则会指向其自身的method方法而非其父类的(这在类加载器载入该类时已经保证,同时JVM会保证总是能从对象引用指向正确的类型信息)。
结合方法指针偏移量是固定的以及指针总是指向实际类的方法域,我们不难发现多态的机制就在这里:
在调用方法时,实际上必须首先完成实例方法的符号引用解析,结果是该符号引用被解析为方法表的偏移量。虚拟机通过对象引用得到方法区中类型信息的入口,查询类的方法表,当将子类对象声明为父类类型时,形式上调用的是父类方法,此时虚拟机会从实际类的方法表(虽然声明的是父类,但是实际上这里的类型信息中存放的是子类的信息)中查找该方法名对应的指针(这里用“查找”实际上是不合适的,前面提到过,方法的偏移量是固定的,所以只需根据偏移量就能获得指针),进而就能指向实际类的方法了。
事实上上面的过程仅仅是利用继承实现多态的内部机制,多态的另外一种实现方式:实现接口相比而言就更加复杂,原因在于,Java的单继承保证了类的线性关系,而接口可以同时实现多个,这样光凭偏移量就很难准确获得方法的指针。所以在JVM中,多态的实例方法调用实际上有两种指令:
invokevirtual指令用于调用声明为类的方法;
invokeinterface指令用于调用声明为接口的方法。
当使用invokeinterface指令调用方法时,就不能采用固定偏移量的办法,只能老老实实挨个找了(当然实际实现并不一定如此,JVM规范并没有规定究竟如何实现这种查找,不同的JVM实现可以有不同的优化算法来提高搜索效率)。我们不难看出,在性能上,调用接口引用的方法通常总是比调用类的引用的方法要慢。这也告诉我们,在类和接口之间优先选择接口作为设计并不总是正确的,当然设计问题不在本文探讨的范围之内,但显然具体问题具体分析仍然不失为更好的选择。

总结:多态机制包括静态多态(编译时多态)和动态多态(运行时多态),静态多态比如说重载,动态多态是在编译时不能确定调用哪个方法,得在运行时确定。动态多态的实现方法包括子类继承父类和类实现接口。当多个子类上转型时,对象调用的是相应子类的方法,这种实现是与JVM有关的。

多态的用途

多态最大的用途在于对设计和架构的复用。《设计模式》中提倡的针对接口编程而不是针对实现编程就是充分利用多态的典型例子。定义功能和组件时定义接口,实现可以留到之后的流程中。同时一个接口可以有多个实现,甚至于完全可以在一个设计中同时使用一个接口的多种实现(例如针对ArrayList和LinkedList不同的特性决定究竟采用哪种实现)。


多态存在的条件及具体的代码实现

多态存在的条件

  • 有继承关系
  • 子类重写父类的方法
  • 父类引用指向子类对象

代码实现

1.启动类

public class Application {
    public static void main(String[] args) {
        //我们前面学的最常规的非静态方法通过new创建类的实例化对象
        //子类Cat的引用,能调用的方法都是自己的或者是继承父类的
        Cat c = new Cat();
        //父类Animal的引用,指向子类对象,但是不能调用子类特有的方法
        //父类引用a1指向子类对象new Cat();
        Animal a1 = new Cat();
        //父类引用a2指向子类对象new Dog();
        Animal a2 = new Dog();
        //父类引用a3指向子类对象new Pig();
        Animal a3= new Pig();


        //同一方法作用于不同对象,可以有不同解释,执行产生不同结果
        //对象能执行哪些方法,只取决于对象左边的类型(即父类引用的类型),类里面有的话才能用啊
        //父类引用a1调用sound方法,输出:猫叫了一声:喵
        a1.sound();
        //父类引用a2调用sound方法,输出:狗叫了一声:汪
        a2.sound();
        //父类引用a3调用sound方法,输出:猪哼了一声:哼
        a3.sound();
        //子类Cat调用自己特有的方法catchMice
        c.catchMice();
        //a1.catchMice();这样是不行的,父类的引用只能调用子类重写的方法,不能调用子类特有的方法
        //((Cat) a1).catchMice();    引用类型的强制转换
    }
}

2.父类Animal

public class Animal {
    public void sound(){
        System.out.println("sound");
    }
}

3.子类Cat

public class Cat extends Animal{
    /**
     * 具有继承关系,子类Cat重写父类Animal
     */
    @Override
    public void sound() {
        System.out.println("猫叫了一声:喵");
    }
    
    /**
     * 子类Cat的特有方法catchMice
     */
    public void catchMice(){
        System.out.println("猫可以抓老鼠");
    }
}

4.子类Dog

public class Dog extends Animal{
    /**
     * 具有继承关系,子类Dog重写父类Animal
     */
    @Override
    public void sound() {
        System.out.println("狗叫了一声:汪");
    }
}

5.子类Pig

public class Pig extends Animal{
    /**
     * 具有继承关系,子类Pig重写父类Animal
     */
    @Override
    public void sound() {
        System.out.println("猪哼了一声:哼");
    }
}

多态的主要作用

把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。

地址赋值之后,父类型的引用就可以根据当前赋值给它的子对象的特性以不同的方式运作。也就是说,父亲的行为像儿子,而不是儿子的行为像父亲。

举个例子:从一个基类中派生,响应一个虚命令,产生不同的结果。(C++)虚方法说明:Java中并没有明确说明什么是虚方法;虚方法是C++中的概念,在Java中我们可以将它理解为所有被overriding(重新定义)的方法都是virtual(虚方法)。在JVM字节码执行引擎中,方法调用会使用invoke virtual字节码指令来调用所有的虚方法。

某个父类继承出多个对象,其父类有一个虚方法,当然其子类也有这个方法,但行为不同,将这些子类对象中的任何一个的地址赋值给父类的引用,这样父类引用就可以执行不同的操作了。因此,实际上你是在通过其父类来访问其子类对象的,你要做的就是一个赋值操作。


多态的好处和弊端

  • 多态的好处:实现了动态编译,提高了程序的扩展性
  • 多态的弊端:不能使用子类的特有功能

注意事项

1.多态是建立在重写基础上的,因此同样多态是方法的多态,属性没有多态性。
2.父类和子类有联系,要符合类型转换规则,否则将出现类型转换异常!ClassCastException!
Sting s1 = new Student(); 这种就是瞎转换!需要的是String类型,提供的Student引用类型,是不可以的!

回顾前面学习的方法重写,不能被重写的方法有哪些?

1.static,属于类,跟类相关的,不属于实例
2.final, 被final修饰的我们将无法改变,就不能重写了
3.private,被private修饰的是私有的,私有的方法也不能重写


如果我的博客对你有一点点帮助,望请大侠可以给文章点个赞再走~
声明:因个人能力有限,博文中必有不足之处,望学术同仁不吝赐教!一起学习一起进步!
以上内容均为原创,转载请注明出处!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值