多态我们在初期学习的时候就有所了解这个概念,但是要说深入的了解恐怕没有几个人。
什么是多态?
简而言之就是同一个行为具有多个不同的表现形式或形态的能力。
比如说有一杯水,不知道是温的,凉的还是冰的烫的,但是通过手一摸就知道了。摸水杯这个动作,对不同的水得到的结果是不同的。这就是多态。
那么,在JAVA中是怎么实现多态的呢?
public class Water {
public void showTem(){
System.out.println("我的温度是: 0度");
}
}
public class IceWater extends Water {
public void showTem(){
System.out.println("我的温度是: 0度");
}
}
public class WarmWater extends Water {
public void showTem(){
System.out.println("我的温度是: 40度");
}
}
public class HotWater extends Water {
public void showTem(){
System.out.println("我的温度是: 100度");
}
}
public class TestWater{
public static void main(String[] args) {
Water w = new WarmWater();
w.showTem();
w = new IceWater();
w.showTem();
w = new HotWater();
w.showTem();
}
}
//结果:
//我的温度是: 40度
//我的温度是: 0度
//我的温度是: 100度
这里的方法showTem()就相当于你去摸水杯。我们定义的water类型的引用变量w就相当于水杯,你在水杯里放了什么温度的水,那么我摸出来的感觉就是什么。就像代码中的那样,放置不同温度的水,得到的温度也就不同,但水杯是同一个。
想必你也看出来了,这段代码中最关键的就是这一句
Water w = new WarmWater();
看过我前几篇文章的应该知道,我说在讲多态的时候,会讲一个很重要的知识点 --- 向上转型。这句代码体现的就是向上转型。后面我会详细讲解这一知识点。
多态的分类
已经简单的认识了多态了,那么我们来看一下多态的分类。
多态一般分为两种:重写式多态和重载式多态。重写和重载这两个知识点前面的文章已经详细将结果了,这里就不多说了。
重载式多态,也叫编译时多态。也就是说这种多态再编译时已经确定好了。重载大家都知道,方法名相同而参数列表不同的一组方法就是重载。在调用这种重载的方法时,通过传入不同的参数最后得到不同的结果。
多态的一般定义是:程序中定义的引用变量所指向的具体类型,以及通过该引用变量发出的方法调用在编程时并不能确定,而是在程序运行期间才确定,这种情况叫做多态。也即是重写式多态的定义。
重写式多态,也叫运行时多态,这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说只有程序运行起来,我们才能知道调用的是哪一个子类的方法。这种多态通过函数的重写以及向上转型来实现。
动态绑定的技术涉及到了JVM技术,这个我暂时也还没有去学习,但是终归是要学的。
多态的条件:
继承:在多态中必须存在有继承关系的子类和父类
重写:子类对父类中的某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备既能调用父类方法,
又能调用子类方法的能力。
在这里,继承也可以被替换为实现接口。
继承和重写我们在前面已经讨论过了,接下来可以看一下转型是什么。
转型又分向上转型和向下转型
向上转型:
子类引用的对象转换为父类类型称为向上转型,通俗的说就是将子类对象转换为父类对象,这里的父类可以是接口。
这里还是用代码来举例子:
public class Animal{
public void eat(){
System.out.println("animal eatting...");
}
}
public void Cat extends Animal{
public void eat(){
System.out.println("我吃鱼");
}
}
public class Dog extends Animal{
public void eat(){
System.out.println("我吃骨头");
}
public void run(){
System.out.println("我会跑");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Cat(); //向上转型
animal.eat();
animal = new Dog(); //向上转型
animal.eat();
}
}
//结果:
//我吃鱼
//我吃骨头
这就是向上转型,子类对象Cat,Dog都变成了父类的对象。但是实际上这个对象调用的方法还是子类的方法。
关于方法的调用顺序后面还会重新讲解。
向上转型中需要注意的问题是:
向上转型后,子类自己独有的方法会丢失。也就是说向上转型仅能调用子类从父类那里继承下来重写的方法。比如上面Dog的run方法就无法通过转型后的对象来调用。
向上转型的好处:
减少重复的代码,使代码变得简洁。提高系统扩展性。
还是用代码来说明问题。现在有很多动物要进行喂食,如果不使用向上转型,那就需要给每一个动物单独写一个吃东西的方法。
public void eat(Cat c){
c.eat();
}
public void eat(Dog d){
d.eat();
}
//.......
eat(new Cat());
eat(new Dog());
一种动物写一个方法,如果我有一万种动物,我就要写一万个方法,写完大概猴年马月都过了好几个了吧。好吧,你很厉害,你耐着性子写完了,以为可以放松一会了,突然又来了一种新的动物,你是不是又要单独为它写一个eat方法?开心了么?
那如果我使用向上转型呢?
我只需要这样写:
public void eat (Animal a){
a.eat();
}
eat(new Cat());
eat(new Dog());
//.....
恩,搞定了。代码是不是简洁了许多?而且这个时候,如果我又有一种新的动物加进来,我只需要实现它自己的类,让他继承Animal就可以了,而不需要为它单独写一个eat方法。是不是提高了扩展性?
向上转型时,是子类转换成父类对象,难道向下转型就是父类对象转换成子类对象吗。其实不是这么简单的。
向下转型:
案例驱动
仔细看看上面的例子:
Animal a = new Cat();//向上转型
Cat c = ((Cat) a);//向下转型
c.eat();
//输出 我吃鱼
Dog d = ((Dog) a);
//报错: java.lang.ClassCastException:Cat cannot be cast to Dog
Animal a1 = new Animal();
Cat c1 = ((Cat) a1);
c1.eat();
//报错:java.lang.ClassCastException:Animal cannot be cast to Cat
为什么第一段代码不报错呢?相比你也知道了,因为a本身就是Cat对象,所以它理所当然的可以向下转型为Cat,也理所当然的不能转为Dog,你见过一条狗突然就变成一只猫这种猎奇现象?
第二段代码我们可以看到它是一个animal对象,它也不能被直接做转型。
向下转型注意事项:
向下转型的前提是父类对象指向的是子类对象,也就是向下转型之前先要向上转型。并且向下转型只能转型为父类对象指向的那个子类对象,猫当然是不能变成狗的。
很多人就会又要奇怪了,我吃饱了撑得吗?为什么要先向上转型再向下转型呢?这时你翻上去看一下向上转型那里的注意点,一个子类向上转型后就没法再访问自己的独立方法了,那如果狗狗利用向上转型的高性能吃完饭后还想跑跑步消消食怎么办呢?
public void eat(Animal a){
if(a instanceof Dog){
Dog d = (Dog) a;
d.eat();
d.run();
}
if(a instanceof Cat){
Cat c = (Cat) a;
c.eat();
System.out.print("somthing");
}
a.eat();
}
eat(new Cat());
eat(new Dog());
//...
为了加深印象,不妨再来看一个关于多态的经典案例:
class A{
public String show(D obj){
return ("A and D");
}
public String show(A obj){
return ("A and A");
}
}
class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
class C extends B{
}
class D extends B{
}
public class Demo {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println("1--" + a1.show(b));//1--B and B 错误了 A and A
System.out.println("2--" + a1.show(c));//2--A and A
System.out.println("3--" + a1.show(d));//3--"A and D"
System.out.println("4--" + a2.show(b));//4--"B and B" 错误了 B and A (B中没办法执行自己独有的方法)
System.out.println("5--" + a2.show(c));//5--"A and A" 错误了 B and A (同上)
System.out.println("6--" + a2.show(d));//6--"A and D"
System.out.println("7--" + b.show(b));//7--"B and B"
System.out.println("8--" + b.show(c));//8--"A and A" 错误了 B and B
System.out.println("9--" + b.show(d));//9--"A and D"
}
}
上面是我第一次的理解得出的结论,结果就是错了一小半,这里就需要get一点新知识了。
当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定了可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。
为了解释上面拗口的内容,这里举一个简单的例子:
class X{
public void show(Y y){
System.out.println("x and y");
}
public void show(){
System.out.println("only x");
}
}
class Y extends X {
public void show(Y y){
System.out.println("y and y");
}
public void show(int i){
}
}
class main{
public static void main(String[] args) {
X x = new Y();
x.show(new Y());//"y and y"
x.show();//"only x"
}
}
Y继承了X,覆盖了x中的show(Y y)方法,但是没有覆盖show()方法。这个时候,引用类型为X的x指向的对象为Y,这个时候,调用的方法由Y决定,会先从Y中寻找。
执行x.show(new Y());,该方法在Y中定义了,所以执行的是Y里面的方法;
但是执行x.show();的时候,有的人会说,Y中没有这个方法啊?它好像是去父类中找该方法了,因为调用了X中的方法。
事实上,Y类中是有show()方法的,这个方法继承自X,只不过没有覆盖该方法,所以没有在Y中明确写出来而已,看起来像是调用了X中的方法,实际上调用的还是Y中的。
上面的是一个简单的知识,它还不足以让我们理解那个复杂的例子。我们再来看这样一个知识:
继承链中对象方法的调用的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
如果你能理解这个调用关系,就ok了。我们回到那个复杂的例子:
a2.show(b) ==> this.show(b),这里this指的是B。
然后.在B类中找show(B obj),找到了,可惜没用,因为show(B obj)方法不在可调用范围内,this.show(O)失败,进入下一级别:super.show(O),super指的是A。
在A 中寻找show(B obj),失败,因为没用定义这个方法。进入第三级别:this.show((super)O),this指的是B。
在B中找show((A)O),找到了:show(A obj),选择调用该方法。
输出:B and A
如果能照着这个思路将其他集中都总结清楚,基本上多态就算是掌握了。
总结:
- 多态简而言之就是同一个行为具有多个不同的表现形式或形态的能力。
- 运行时多态的前提:继承(实现),重写,向上转型。
- 向上转型与向下转型。
- 继承链中对象方法的调用的优先级:
this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
关于多态还有一些其他的知识点,比如几个关键字:static、final等,我们在后面继续学习。