【Java笔记】一网打尽Java中的多态

一网打尽Java中的多态


  • 前提声明
  • 前提概念
  • 多态的概念
  • 多态中的向上转型和向下转型
  • 多态在代码层面的体现
  • (总结)到底什么是多态?

前提声明


据我查阅的资料和自己的一些理解,Java的多态性可以不严谨分为两类

  • 编译时的多态性(函数重载属于编译时的多态性)
  • 运行时的多态性(子类重写父类方法属于运行时的多态性)

当然也可以理解为Java的多态性只有一类

  • 运行时的多态性 (只有重写才能体现多态性)

因为这个东西,网上会有很多的争议,说重载是静态分派的过程,与多态性无关,只有重写才能体现多态性。这里我引用《深入理解Java虚拟机》作者的观点。原话是:有一种观点认为,因为重载是静态的,重写是动态的,所以只有重写算是多态性的体现,重载不算多态,笔者认为这种争论是没有意义的,概念仅仅是说明问题的一个种工具而已。

所以我这里对此争议不做出讨论,毕竟公说公有理,婆说婆有理,你觉得怎么方便理解就怎么来。概念都是死的,重要的是自己的能不能更好的吸收。

在下面的例子中,为了简单易懂,我们就不将重载纳入多态的范畴,单从重写的角度讨论多态

前提概念


Java面向对象的三个特性

封装、继承、多态。可以这么说,封装和继承是多态的实现基础。所以我们首先要了解什么是封装,什么是继承,我们才能更好的理解什么是多态。

静态类型和实际类型:
Father son = new Son();            //(Father类是Son类的父类,这是一段实现多态的代码)

在以上的代码中,“Father”是变量的静态类型(Static Type),"Son"是变量的实际类型(Actual Type)

总的来说是这么个情况,Father类型的son变量指向了Son类型的对象。这么说起来好像很绕,我们可以这么理解,我们把这句话分为两段,Father son代表定义了一个Father类型的变量,是定义的时候就已经明确了的,Father类型是son变量的静态类型。我们在来看一下= new Son()这一段,代表实例化了一个Son类型的对象并赋值给左边,所以son变量最后所指向一个Son类型的实例对象,Son类型是son变量的实际类型。
(参考于《深入理解Java虚拟机》的第八章节,如果我总结的不好,可以直接去看书里面的内容,也许更能让你理解)

方法调用的静态绑定和动态绑定

什么是绑定?
把一个方法与其所在的类/对象关联起来的行为就叫做方法的绑定,绑定行为分为两种:

  • 静态绑定(前期绑定)
  • 动态绑定(后期绑定)

静态绑定:
静态绑定是指在程序运行前就已经知道方法是属于那个类的,在编译时生成的字节码文件中就已经将该方法和所属的类关联起来,通常被静态绑定的方法具有“编译器可知,运行期不变的”的特性。所以Java中,finalprivatestatic修饰的方法以及构造函数都是静态绑定的,不需等待程序运行,不需具体的实例对象就可以知道这个方法的具体内容。

动态绑定:
动态绑定是实现多态性的重要技术手段,在java中,所有的方法都是通过动态绑定来实现多态的,也可以说几乎所有方法都是动态绑定的。动态绑定是指在程序运行期间才能确定所调用的方法属于哪个类,是属于子类还是父类。跟静态绑定不同,静态绑定是编译阶段就已经确定好了,而动态绑定是在程序运行期间根据接收者(变量)的实际类型来确定执行的方法到底是属于哪个类,这里就涉及到了动态分派的知识,但是由于篇幅有限,所以这里就简单的带过一下。

继承体系中方法重载,方法重写和隐藏的概念:

方法重载:(方法名相同,参数列表不同的多个方法之间的关系)

  • 方法重载对成员方法有效,与成员变量无关
  • 参数列表不同的意思是参数的个数、数据类型和顺序的不同
  • 静态方法可以实现重载
  • 方法的返回值和访问修饰符不参与重载

方法重写:(子类重写父类的方法,要求方法名和参数类型完全一样,参数不能是子类)

  • 方法重写仅对实例方法有效,对静态方法无效,与成员变量无关
  • 返回值比父类小或相同(既父类的子类),访问修饰符级别要比父类大或相同,如父类方法为protected,子类则不能是private(比如子类向上转型为父类,那么转型后的方法到底是protected?还是private呢,这就有冲突了
  • 子类在语法上是可以重写父类方法,实际效果并不是重写,因为静态方法是类定义的一部分,仅仅是父子类都有一个相同名称的静态方法而已,可以说是隐藏的形式。
  • 子类不能用实例方法重写父类的静态方法,编译报错
  • 子类不能用静态方法重写父类的实例方法,编译报错

隐藏:

  • 隐藏是与实例方法的重写相对立而言的。其实是成员变量和静态方法在继承关系中的父子类具有相同成员变量或相同静态方法时的一种描述
  • 隐藏不是重写。重写是用子类方法覆盖掉(替换)父类的方法,隐藏仅仅是将父类方法形式上的隐藏起来,父类方法还是存在的
  • 向上转型时,如果是隐藏关系,所调用就是父类的方法或成员变量,如果是重写,则调用的是子类的方法,既重写的方法

多态的概念(方法重写的角度)


多态的定义:

  • 理解一: 是指允许不同类的对象对同一函数调用做出响应。即调用一个相同的方法时可以根据(实例)对象的不同而采用多种不同的行为方式。
  • 理解二: 多态是指同一行为(方法)具有不同的表现形式,假设我有一个打印机,根据不同的实例,有着不同的表现形式,既这台打印机指向这个实例对象,我可以打印黑白,执行另外一个实例对象,我可以打印彩色。但这都是同一个行为(打印行为),但我根据实例对象的不同,有不同的表现形式(打印彩色或打印黑白)

通俗的讲:

不从代码角度出发,在宏观概念上,多态就是比如当我按下F1键这个动作,如果当前在Flash界面下弹出的就是AS3的帮助文档;如果当前在Word下弹出的就是Word帮助;在Windows下弹出的就是Windows帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。

实现多态的三个条件:

  • 存在继承关系
    Son类继承于Father类
  • 存在方法重写
    子类Son重写了父类Father的某个方法
  • 静态类型的变量所指向的对象不是该类型的对象
    如父类引用指向子类对象,Father son = new Son()

多态中的向上转型和向下转型


向上转型:

什么是向上转型:

Instrument piano = new Piano();        //Instrument类是Pinao类的父类

以上代码就是一段向上转型的过程。为什么一个Instrument类的引用变量可以指向一个别的类的实例对象。因为Piano类是Instrument类的子类,子类是父类的一种类型,子类对象其实也是一种父类对象,我们首先就要有这种思想。

为什么呢?我们了解到Instrument是乐器类,Piano是钢琴乐器类。我们这里假设Instrument类是所有乐器的基类(父类),除了钢琴PianoInstrument类的子类外,可能还有很多其他的乐器子类,比如小提琴类,喇叭类等等。我们知道继承可以确保基类中所有的方法在子类中都是有效的,因为子类从基类中继承了所以公开的方法和变量,父类该有的东西,我子类都有,所以我们可以准确的说,Piano类对象是一种类型的Instrument,既钢琴是一种(类型的)乐器。所以Piano对象是一种Instrument对象。把一个乐器类的引用变量指向一个钢琴实例对象时,子类对象转型为它的父类对象的过程,就是向上转型。但是转型并不是意味着它完全等同于一个父类实例化出来的父类对象,只是子类在功能上对自己进行窄化,让自己在功能上与父类等同,对外宣传为父类对象,但它的最根本本质还是一个子类对象。我们看到向下转型的时候就能够明白。

为什么称为向上转型呢?
我们知道继承体系其实就是一个树,最上面的根节点就是标准类Object,向下就是继承于上边节点的子类,依次向下。所以子类转型成父类,这就是一个向上的过程。

安全性?
从一个较具体的类型向通用类型的转换,其实是很安全的。也就是说子类是父类的一个超集,子类拥有父类所有的方法,还拥有父类没有东西。所以子类对象转换为父类对象,父类该有的都有,只是会丢失自己特有的方法。所以不存两个类型不能转换的类型检测问题。

向下转型:

什么是向下转型:
当我们了解了什么是向上转型,再去了解向下转型就容易很多了。向下转型就是一个父类对象转型为子类对象的过程。

Instrument piano = new Piano();  //向上转型
Piano pinao0 = (Piano)pinao;	 //向下转型

如代码所示,第二段代码就是一个向下转型的过程,这是一个能正确进行的向下转型的过程。
安全性问题?
向下转型跟向上转型不一样,因为子类拥有父类的所有特征,所以这是一个兼容的过程。子类在抛弃自己特有方法的情况下,它就相当于一个父类。所以向上转型的过程不会出现什么问题。但是向下转型就不一样的。从一个通用类型转换为一个具体的类型,既父类的东西少,子类所拥有的更多,同时父类可以有多个子类,每个子类的所拥有的东西都不同,可能信息上完全不能匹配。所以当父类转换为一个子类的时候,可能会存在类型不匹配的问题。举个例子,看下面代码

Instrument piano = new Piano();  	//将钢琴对象转换为乐器类型
Instrument violin = new Violin();   //将小提琴对象转换为乐器类型
Piano pinao0 = (Piano)violin;       //将一个从小提琴对象向上转型为乐器类型的乐器对象向下转型为钢琴类型
//这里就出现了ClassCastException异常(类型转换异常)

这是为什么呢?因为虽然violin变量指向的是一个父类对象,但是这个父类对象实际是从一个Violin小提琴对象向上转型而成。只是在功能上缩窄成一个父类类型。但是它最根本的本质还是子类对象,既Violin类型。所以不能将一个Violin对象转换为一个Piano对象。因为两个子类之间可能存在不兼容的问题,这相当两个子类在转换。同理,下面的这种问题也是有错的。

Piano pinao0 = (Piano)new Instrument();    //error,运行时异常

这是为什么呢?刚刚说了,一个子类转换成另一个子类,为什么不行,因为虽然他们具有一定的共性(具有同一个父类,既拥有一定的相同的成员),但每个子类都有自己的特定成员。所以类型检测不通过。在这个例子中。是将一个真正的父类对象向下转型为子类。因为子类的信息量更大,拥有比父类更多的成员。所以父类不能用自己较少的东西,强硬把自己转为一个拥有更多功能的子类。所以RTTI类型检测不通过。

补充
(1)在Java语言中,所有转型都会得到检查,所以即使我们只进行一次普通的加括号形式的类型转换,但在进入运行时期间,虚拟机仍然会对其进行类型检查,以确保它的确是我们所希望的类型。如果不是则抛出ClassCastException。这种检测称为运行时类型识别(RTTI)
(2)以下代码证明了,虽然向上转型了,但改变不了本身的根本性质,只是在功能上进行窄化,等同于一个父类对象。

Object str = new String();				//String对象向上转型为父类Object类型
System.out.println(str.getClass());     

//output:   class java.lang.String

多态在代码层面的体现


测试代码:
Animal.java

//Animal类
public class Animal {

	public String name = "Animal";
	public int age = 2;

	public  void eat(){
		System.out.println("animal eating");
	}

	public  void eat(String food){
		System.out.println("Animal eating " +food);
	}

	public  void run(){
		System.out.println("animal run");
	}

	public static void show(){
		Animal animal = new Animal();
		System.out.println("I'm "+animal.name+" and "+animal.age+" years old now!");
	}

}

Dog.java

//Dog类
public class Dog extends Animal{


	public String name = "Dog";
	public int age = 6;

	public void eat(){
		System.out.println("Dog eating");
	}

	public void cry(){
		System.out.println("Dog wang wang ~~");
	}

	public static void show(){
		Dog dog = new Dog();
		System.out.println("I'm "+dog.name+" and "+dog.age+" years old now!");
	}

}

Cat.java

//Cat类
public class Cat extends Animal{
	public String name = "Cat";
	public int age = 4;

	public void eat(){
		System.out.println("Cat eating");
	}

	public void cry(){
		System.out.println("Cat miao miao ~~");
	}

	public static void show(){
		Cat cat = new Cat();
		System.out.println("I'm "+cat.name+" and "+cat.age+" years old now!");
	}

}

主函数

	public static void main(String[] args) {

		Animal dog = new Dog();    //向上转型
		Animal cat = new Cat();    //向上转型
	
		dog.eat();
		cat.eat();
		
		dog.show();
		cat.show();

		System.out.println(dog.name);
		System.out.println(cat.name);

		Dog dog0 = (Dog)dog;       //向下转型
		dog0.cry();
		Cat cat0 = (Cat)cat;       //向下转型
		cat0.cry();

	}

输出结果:

Dog eating
Cat eating

I'm Animal and 2 years old now!
I'm Animal and 2 years old now!

Animal
Animal

Dog wang wang ~~
Cat miao miao ~~

分析:

  • 为什么Animal类型的变量指向的是一个已经向上转型为Animal类型的对象,输出的却是子类各自的方法呢?这是因为在向上转型中,子类仅仅是缩窄了自己的功能,让自己在功能上等同于一个父类对象,但本质还是一个子类对象。又因为子类重写了父类的方法(重写即是将父类方法替换掉)所以实际调用的自然是子类重写后的方法。(以上部分解释的 比较表层,可能有些更深层次的地方我还没有理解好,所以没有不能很好的解释到位。推荐大家可以了解一下构造器内部的多态行为的问题来加深理解,里面有个关于基类构造器调用被重写方法而导致的一些问题,如果还需要更深入的了解,其中的原因,可以还需要从虚拟机中入手,了解Class字节码指令集等)

这有一个观点是我在《Java语言程序设计》里看到的,原话是说:

  • 动态绑定工作机制如下:假设对象o是类C1,C2,…,Cn-1,Cn的实例,其中C1是C2的子类,C2是C3的子类…Cn-1是Cn的子类。
  • 也就是说Cn是最通用的类,C1是最特殊的类。在Java中,Cn就类似于Object类,如果对象o调用一个方法p,那么JVM会依次在类C1,C2…Cn-1,Cn查找方法p的实现,直到找到为止。一旦找到一个实现,就停止查找,然后调用这个首先找到的实现。
  • dog.show()cat.show()执行的是父类Animal的静态方法show(),而不是子类自己的。那是因为静态方法没有重写的概念,虽然子类和父类都具有相同的静态方法,但不算重写,只是Animal类和Dog类和Cat类分别有一个长的一模一样的show方法而已。这种关系我们可以理解为隐藏。所以子类对象已经做了向上转型,对外宣传为父类类型,又没有重写,所以调用的自然是父类的静态方法。

  • System.out.println(dog.name);System.out.println(cat.name);输出的也是Animal父类的name的值,理由同上。然后子类也有相同的属性name,但这只是一种隐藏关系。向上转型后,就可以当做是一个父类对象来处理。成员变量也不存在重写的概念。所以输出的也是父类的name.

  • 最后dog0.cry();,cat0.cry();的输出也是没有悬念的结果。调用子类各自的cry()方法。因为将一个本质是子类,只是被向上转型成父类类型的对象。做向下转型回到原来的类型。得到的自然是原本类型的对象。调用的也自然是子类自己的cry()方法。但是要注意,如果是Dog dog0 = (Dog)cat;的情况,那就会报类转型异常。因为不能把一个Cat类对象转型成一个Dog类对象。

(总结)到底什么是多态?


这个多态的概念,在Java上的确是有些抽象,我这里根据我自己的理解稍作总结。

  • 不同对象根据同一方法会做出不同的响应,既重写就是多态的其中一个表现形式
  • 从向上转型,向下转型,对象在运行期间可以有不同的表现状态。向上转型时,父类变量指向子类对象,子类对象被转型为父类对象,这个对象可以由调用子类重写后的方法,又具有未被重写的父类方法和其他成员,这是一个状态。当我不需要使用父类的某些功能时,我又可以向下转型回子类对象,调用子类才具有的特定方法。这又是一个状态。这也是多态的一个表现形式
  • 动物这个变量可以指向不同的实例对象(狗或猫),对喊叫这个行为可以有不同的表现形式(wangwang,miaomiao)

如果还不明白,这里有个简单的例子,通俗易懂,一个关于花木兰替父从军记的多态故事
知乎 - JAVA的多态用几句话能直观的解释一下吗?
看到@程序狗的回复


参考资料


在这里感谢查询过的网站和博客的作者,非常感谢!!
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值