跳到主要内容Java 多态详解:从向上转型到向下转型与动态绑定 | 极客日志Javajava
Java 多态详解:从向上转型到向下转型与动态绑定
Java 多态允许同一行为在不同对象上有不同表现形式,核心在于继承与方法重写。通过父类引用指向子类对象实现向上转型,利用运行时多态调用重写方法;向下转型需配合 instanceof 确保类型安全。理解静态与动态绑定机制(如 invokevirtual 指令)有助于掌握方法调用原理。此外,构造函数中避免调用可重写方法可防止因初始化顺序导致的潜在 Bug。
念念不忘1 浏览 什么是多态?
举一个简单的例子:
同样是交朋友,李华却需要表现出两种状态。换言之,多态允许同一个行为在不同的对象上有不同的表现形式。
多态的定义:
多态(Polymorphism)是面向对象编程的一个核心特性,它允许同一个行为(方法调用)在不同的对象上有不同的表现形式。简单来说,多态使得程序可以以统一的方式调用不同类型的对象,从而提高了代码的灵活性和可扩展性。
示例:
class Animal {
public void eat() {
System.out.println("吃饭~");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
public class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public static void main(String[] args) {
Animal cat = new Cat();
Animal dog = new Dog();
cat.eat();
dog.eat();
}
}
实现多态需要满足的条件
- 继承关系(最基本的条件)
- 子类重写父类的方法
- 通过父类对象的引用去调用重写的方法
符合上述的三个部分就会发生动态的绑定,而动态的绑定是多态的基础!
向上转型
本质及原理
向上转型(Upcasting)的本质是子类对象可以被赋值给父类引用,也就是说,将一个子类对象看作是它的父类类型。这种机制基于面向对象的继承关系和 IS-A(是一个)原则,即子类对象是父类对象的一种特殊形式。
- 继承关系:子类继承父类,因此子类对象自然包含父类中定义的所有方法和属性。在向上转型时,父类引用只会访问父类中声明的方法和属性,而不会直接访问子类的扩展方法和属性。
- 运行时多态:尽管父类引用只能看到父类的接口,但调用方法时,具体执行的是子类的重写方法。这就是运行时多态的体现。
class Animal {
public String name;
public String color;
protected int age;
public Animal(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
public void eat() {
System.out.println(name + "吃饭~");
}
public void sleep() {
System.out.println(name + "睡觉~");
}
}
public class Cat extends Animal {
public Cat(String name, String color, int age) {
super(name, color, age);
}
public void eat() {
System.out.println(name + "吃鱼");
}
public void mimi() {
System.out.println("喵喵~~");
}
public static void main(String[] args) {
Animal cat = new Cat("小花", "白色", 2);
cat.eat();
}
}
一般来说,只有数据类型一样的变量才能赋值,为什么这两个变量也能用等号呢?因为他们是继承关系。
使用场景及说明
- 直接赋值:将子类对象直接赋值给父类类型的引用。
- 方法传参:在方法调用时,将子类对象作为父类类型的参数传递。
- 方法返回:方法返回一个父类类型的对象,但实际返回的是子类对象。
package cn.nyist.animal;
class Animal {
public String name;
public String color;
protected int age;
public Animal(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
public void eat() {
System.out.println(name + "吃饭~");
}
public void sleep() {
System.out.println(name + "睡觉~");
}
}
public class Cat extends Animal {
public Cat(String name, String color, int age) {
super(name, color, age);
}
public void eat() {
System.out.println(name + "吃鱼");
}
public void mimi() {
System.out.println("喵喵~~");
}
public static void F(Animal a) {
a.eat();
}
public static Animal fAnimal() {
return new Cat("小灰", "灰色", 4);
}
public static void main(String[] args) {
Cat cat1 = new Cat("小白", "白色", 3);
Animal animal1 = cat1;
animal1.eat();
Animal animal2 = new Cat("小花", "花色", 2);
animal2.eat();
Cat cat2 = new Cat("小黑", "黑色", 3);
F(cat2);
Animal animal4 = fAnimal();
animal4.eat();
}
}
- Cat 类型的对象 cat1 被赋值给 Animal 类型的引用 animal1,这是向上转型。animal1 虽然是 Animal 类型,但实际指向的是 Cat 对象,因此调用 animal1.eat() 时,通过多态机制,执行的是 Cat 类中重写的 eat() 方法。
- F 方法的参数是 Animal 类型,因此当调用 F(cat2) 时,Cat 类型的对象 cat2 被向上转型为 Animal 类型。在方法内部,a.eat() 调用的是实际对象 cat2 的 eat() 方法,通过多态机制,执行 Cat 类中的重写方法。
- 方法 fAnimal 的返回类型是 Animal,但方法内部实际上返回了一个 Cat 对象。当返回值被赋值给 Animal 类型的引用 animal4 时,发生向上转型。调用 animal4.eat() 时,通过多态机制,调用了 Cat 类中重写的 eat() 方法。
向上转型的优点:让代码实现更简单灵活。向上转型的缺陷:不能调用到子类特有的方法。
静态绑定和动态绑定
什么是静态绑定?什么是动态绑定?
静态绑定:也称为前期绑定 (早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用哪个方法。典型代表函数重载。
动态绑定:也称为后期绑定 (晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。
Java 字节码中有两种常见的方法调用指令:invokespecial 和 invokevirtual。这两种方法调用指令可以帮助我们区分静态绑定和动态绑定。
package cn.nyist.animal;
class Animal {
public String name;
public String color;
protected int age;
public Animal(String name, String color, int age) {
this.name = name;
this.color = color;
this.age = age;
}
public void eat() {
System.out.println(name + "吃饭~");
}
public void sleep() {
System.out.println(name + "睡觉~");
}
}
public class Cat extends Animal {
public Cat(String name, String color, int age) {
super(name, color, age);
}
public void eat() {
System.out.println(name + "吃鱼");
}
public void mimi() {
System.out.println("喵喵~~");
}
public static void F(Animal a) {
a.eat();
}
public static Animal fAnimal() {
return new Cat("小灰", "灰色", 4);
}
public static void main(String[] args) {
Cat cat1 = new Cat("小白", "白色", 3);
Animal animal1 = cat1;
animal1.eat();
Animal animal2 = new Cat("小花", "花色", 2);
animal2.eat();
Cat cat2 = new Cat("小黑", "黑色", 3);
F(cat2);
Animal animal4 = fAnimal();
animal4.eat();
}
}
先用 javac 编译 .java 文件,生成 .class 文件。然后使用 javap 针对 .class 文件反编译,不加 .java 后缀:
javac Cat.java
javap -c Cat
运行后显示就是 Java 字节码。我们需要找到 main 方法中的字节码。
invokespecial 指令绑定的是父类方法、私有方法或构造方法,这些在编译时已经明确目标,因此属于静态绑定。
9: invokespecial #39 // Method "<init>":(Ljava/lang/String;Ljava/lang/String;I)V
28: invokespecial #39 // Method "<init>":(Ljava/lang/String;Ljava/lang/String;I)V
45: invokespecial #39 // Method "<init>":(Ljava/lang/String;Ljava/lang/String;I)V
invokevirtual 指令绑定的是普通实例方法(如 eat()),这些方法会在运行时根据实际对象类型决定调用的目标,因此属于动态绑定。
16: invokevirtual #31 // Method cn/nyist/animal/Animal.eat:()V
33: invokevirtual #31 // Method cn/nyist/animal/Animal.eat:()V
62: invokevirtual #31 // Method cn/nyist/animal/Animal.eat:()V
toString 动态绑定
Dog dog = new Dog("小黑", 5);
System.out.println(dog);
这里创建了一个 Dog 类型的对象,并通过 System.out.println(dog) 输出。System.out.println() 方法内部接受的是一个 Object 类型的参数(也就是 dog 发生了向上转型,变成了 Object 类型)。
System.out.println() 的核心方法定义如下:
public void println(@Nullable Object x) {
String s = String.valueOf(x);
synchronized(this) {
print(s);
newLine();
}
}
调用 println(Object x) 方法时,dog 被向上转型为 Object 类型,作为参数传递进去。然后,String.valueOf(x) 将对象 x 转为字符串。
String.valueOf(Object obj) 是一个静态方法,其代码如下:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
这里会检查对象 obj 是否为 null:如果是 null,返回字符串 'null'。如果不是 null,调用对象的 toString() 方法,将对象转为字符串。
如果 Dog 类没有重写 toString() 方法,则默认会调用 Object 类的 toString() 方法。Object.toString() 的默认实现是:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
默认输出为 类的全限定名 + @ + 对象的哈希值,比如 Dog@1a2b3c。
向下转型
概念
向下转型(Downcasting)是指将父类的引用转换为子类的引用。这通常发生在需要调用子类特有的方法或属性时。
向下转型需要开发者明确知道父类引用所指向的实际对象是哪个子类,因为只有当实际对象是目标子类类型时,向下转型才是安全的。
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("小黑", 2);
Dog dog = new Dog("大黄", 1);
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
dog = (Dog) animal;
dog.bark();
}
}
正确使用
向下转型前提是对象实际类型必须匹配:向下转型的对象实际类型必须是目标类型,否则会抛出 ClassCastException。
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java 中为了提高向下转型的安全性,引入了 instanceof,如果该表达式为 true,则可以安全转换。
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.fetch();
} else {
System.out.println("无法转换为 Dog 类型");
}
import java.util.ArrayList;
class Animal {
void sound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
void fetch() {
System.out.println("狗在刨土");
}
}
class Cat extends Animal {
void climb() {
System.out.println("猫在爬树");
}
}
public class DowncastingExample {
public static void main(String[] args) {
ArrayList<Animal> animals = new ArrayList<>();
animals.add(new Dog());
animals.add(new Cat());
for (Animal animal : animals) {
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.fetch();
} else if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.climb();
}
}
}
}
小练
一段有坑的代码。我们创建两个类,B 是父类,D 是子类。D 中重写 func 方法。并且在 B 的构造方法中调用 func。
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
- 构造 D 对象的同时,会调用 B 的构造方法。
- B 的构造方法中调用了 func 方法,此时会触发动态绑定,会调用到 D 中的 func。
- 此时 D 对象自身还没有构造,此时 num 处在未初始化的状态,值为 0。如果具备多态性,num 的值应该是 1。
- 所以在构造函数内,尽量避免使用实例方法,除了 final 和 private 方法。
结论: '用尽量简单的方式使对象进入可工作状态', 尽量不要在构造器中调用方法 (如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。
相关免费在线工具
- 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