跳到主要内容Java 面向对象多态:向上转型与向下转型 | 极客日志Javajava
Java 面向对象多态:向上转型与向下转型
Java 面向对象多态指同一行为具有多种表现形式。主要分为编译时重载和运行时重写。运行时多态依赖继承、方法重写及向上转型。向上转型将子类对象赋给父类引用,提高扩展性但丢失子类特有方法;向下转型需配合 instanceof 检查防止 ClassCastException。文章通过动物喂食等案例演示转型过程,并深入解析继承链中方法调用的优先级规则,帮助理解复杂多态场景下的方法绑定机制。
简单的理解多态
多态,简而言之就是同一个行为具有多个不同表现形式或形态的能力。比如说,有一杯水,我不知道它是温的、冰的还是烫的,但是我一摸我就知道了。我摸水杯这个动作,对于不同温度的水,就会得到不同的结果。这就是多态。
那么,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 ();
w.showTem();
w = ();
w.showTem();
w = ();
w.showTem();
}
}
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
=
new
WarmWater
new
IceWater
new
HotWater
我的温度是:40 度
我的温度是:0 度
我的温度是:100 度
这里的方法 showTem() 就相当于你去摸水杯。我们定义的 Water 类型的引用变量 w 就相当于水杯,你在水杯里放了什么温度的水,那么我摸出来的感觉就是什么。就像代码中的那样,放置不同温度的水,得到的温度也就不同,但水杯是同一个。
Water w = new WarmWater();
这句代码体现的就是向上转型。后面我会详细讲解这一知识点。
多态的分类
已经简单的认识了多态了,那么我们来看一下多态的分类。
多态一般分为两种:重写式多态和重载式多态。重写和重载这两个知识点前面的文章已经详细讲解过了,这里就不多说了。
重载式多态,也叫编译时多态。也就是说这种多态在编译时已经确定好了。重载大家都知道,方法名相同而参数列表不同的一组方法就是重载。在调用这种重载的方法时,通过传入不同的参数最后得到不同的结果。
但是这里是有歧义的,有的人觉得不应该把重载也算作多态。因为很多人对多态的理解是:程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,这种情况叫做多态。这个定义中描述的就是我们的第二种多态——重写式多态。并且,重载式多态并不是面向对象编程特有的,而多态却是面向对象三大特性之一。
我觉得大家也没有必要在定义上去深究这些,我的理解是:同一个行为具有多个不同表现形式或形态的能力就是多态,所以我认为重载也是一种多态,如果你不同意这种观点,我也接受。
重写式多态,也叫运行时多态。这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。
这种多态通过函数的重写以及向上转型来实现,我们上面代码中的例子就是一个完整的重写式多态。我们接下来讲的所有多态都是重写式多态,因为它才是面向对象编程中真正的多态。
动态绑定技术涉及到 JVM,暂时不讲(涉及底层原理,感兴趣可自行研究),感兴趣的可以自己去研究一下。
多态的条件
- 继承。在多态中必须存在有继承关系的子类和父类。
- 重写。子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型。在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够既能调用父类的方法和子类的方法。
继承和重写之前都说过了,接下来我们来看一下转型是什么。
向上转型与向下转型
向上转型
子类引用的对象转换为父类类型称为向上转型。通俗地说就是将子类对象转为父类对象。此处父类对象可以是接口。
public class Animal {
public void eat() {
System.out.println("animal eating...");
}
}
public class 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();
}
}
这就是向上转型,Animal animal = new Cat(); 将子类对象 Cat 转化为父类对象 Animal。这个时候 animal 这个引用调用的方法是子类方法。
- 向上转型时,子类单独定义的方法会丢失。比如上面
Dog 类中定义的 run 方法,当 animal 引用指向 Dog 类实例时是访问不到 run 方法的,animal.run() 会报错。
- 子类引用不能指向父类对象。
Cat c = (Cat)new Animal() 这样是不行的。
举个例子:比如我现在有很多种类的动物,要喂它们吃东西。如果不用向上转型,那我需要这样写:
public void eat(Cat c) {
c.eat();
}
public void eat(Dog d) {
d.eat();
}
eat(new Cat());
eat(new Cat());
eat(new Dog());
一种动物写一个方法,如果我有一万种动物,我就要写一万个方法,写完大概猴年马月都过了好几个了吧。好吧,你很厉害,你耐着性子写完了,以为可以放松一会,突然又来了一种新的动物,你是不是又要单独为它写一个 eat 方法?开心了么?
public void eat(Animal a) {
a.eat();
}
eat(new Cat());
eat(new Cat());
eat(new Dog());
恩,搞定了。代码是不是简洁了许多?而且这个时候,如果我又有一种新的动物加进来,我只需要实现它自己的类,让它继承 Animal 就可以了,而不需要为它单独写一个 eat 方法。是不是提高了扩展性?
向下转型
与向上转型相对应的就是向下转型了。向下转型是把父类对象转为子类对象。(请注意!这里是有坑的。)
Animal a = new Cat();
Cat c = ((Cat) a);
c.eat();
Dog d = ((Dog) a);
d.eat();
Animal a1 = new Animal();
Cat c1 = ((Cat) a1);
c1.eat();
为什么第一段代码不报错呢?相比你也知道了,因为 a 本身就是 Cat 对象,所以它理所当然的可以向下转型为 Cat,也理所当然的不能转为 Dog,你见过一条狗突然就变成一只猫这种不合理现象?
而 a1 为 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.println("我也想跑,但是不会");
}
a.eat();
}
eat(new Cat());
eat(new Cat());
eat(new Dog());
现在,你懂了吗?这就是向下转型的简单应用,可能举的例子不恰当,但是也可以说明一些问题。
敲黑板,划重点!看到那个 instanceof 了吗?
经典案例分析多态
基本的多态和转型我们都会了,最后加点餐。看一个经典案例:
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));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
System.out.println("7--" + b.show(b));
System.out.println("8--" + b.show(c));
System.out.println("9--" + b.show(d));
}
}
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D
前三个,强行分析,还能看得懂。但是第四个,大概你就傻了吧。为什么不是 b and b 呢?
当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。
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());
x.show();
}
}
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 中的。
这个时候再看上面那句难理解的话就不难理解了吧。X 是引用变量类型,它决定哪些方法可以调用;show() 和 show(Y y) 可以调用,而 show(int i) 不可以调用。Y 是被引用对象的类型,它决定了调用谁的方法:调用 Y 的方法。
上面的是一个简单的知识,它还不足以让我们理解那个复杂的例子。我们再来看这样一个知识:
继承链中对象方法的调用的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
如果你能理解这个调用关系,那么多态你就掌握了。我们回到那个复杂的例子:
abcd 的关系是这样的:C/D —> B —> A
首先,a2 是类型为 A 的引用类型,它指向类型为 B 的对象。A 确定可调用的方法:show(D obj) 和 show(A obj)。
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 为类型为 B 的引用对象,指向类型为 B 的对象。没有涉及向上转型,只会调用本类中的方法。
在 B 中寻找 show(D obj) 方法。现在你不会说没找到了吧?找到了,直接调用该方法。
总结
- 多态,简而言之就是同一个行为具有多个不同表现形式或形态的能力。
- 多态的分类:运行时多态和编译时多态。
- 运行时多态的前提:继承(实现),重写,向上转型。
- 向上转型与向下转型。
- 继承链中对象方法的调用的优先级:
this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。