Java 多态深度剖析:从向上转型到向下转型及动态绑定
本文深入解析 Java 多态机制。首先阐述多态定义及实现条件(继承、重写、父类引用)。接着详解向上转型的原理、场景及优缺点,说明如何通过父类引用调用子类方法。随后对比静态绑定与动态绑定,通过 Java 字节码指令 invokespecial 和 invokevirtual 进行底层分析。最后讲解向下转型的安全性与 instanceof 检查,并指出构造函数中调用被重写方法可能引发的隐患。内容涵盖理论、代码示例及最佳实践建议。

本文深入解析 Java 多态机制。首先阐述多态定义及实现条件(继承、重写、父类引用)。接着详解向上转型的原理、场景及优缺点,说明如何通过父类引用调用子类方法。随后对比静态绑定与动态绑定,通过 Java 字节码指令 invokespecial 和 invokevirtual 进行底层分析。最后讲解向下转型的安全性与 instanceof 检查,并指出构造函数中调用被重写方法可能引发的隐患。内容涵盖理论、代码示例及最佳实践建议。

举一个简单的例子:
小滑是一个比较狡诈的人,小刚是一个性格比较直的人,李华比较喜欢交朋友。当李华与小滑交朋友的时候就需要谨慎,当李华和小刚交朋友的时候需要柔和。

同样是交朋友,李华却需要表现出两种状态。
换言之,多态,它允许同一个行为在不同的对象上有不同的表现形式。
多态的定义:
多态(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
Animal cat = new Cat();
Animal dog = new Dog();
// 都调用 eat 方法
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 (, , );
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 Animal {
(, , );
}
{
(, , );
cat1;
animal1.eat();
(, , );
animal2.eat();
(, , );
F(cat2);
fAnimal();
animal4.eat();
}
}
说明:
向上转型的优点:让代码实现更简单灵活。 向上转型的缺陷:不能调用到子类特有的方法。
静态绑定:也称为前期绑定 (早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用哪个方法。典型代表函数重载。
动态绑定:也称为后期绑定 (晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。
用 Java 字节码来帮助我们理解 Java 字节码中有两种常见的方法调用指令:invokespecial 和 invokevirtual。这两种方法调用指令可以帮助我们区分静态绑定和动态绑定。下面逐一解释。
写一个继承类型的 java 程序:
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 Animal {
(, , );
}
{
(, , );
cat1;
animal1.eat();
(, , );
animal2.eat();
(, , );
F(cat2);
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
创建 Dog 对象并打印
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() 方法,将对象转为字符串。
调用 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 引用的是 Dog 类型
animal.eat();
// 向下转型
// 程序可以通过编程,但运行时抛出异常---因为:animal 实际指向的是狗
// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
// cat = (Cat) animal; cat.mew();
// animal 本来指向的就是狗,因此将 animal 还原为狗也是安全的
dog = (Dog) animal;
dog.bark();
}
}
向下转型前提是对象实际类型必须匹配:向下转型的对象实际类型必须是目标类型,否则会抛出 ClassCastException。
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java 中为了提高向下转型的安全性,引入了 instanceof,如果该表达式为 true,则可以安全转换。
使用 instanceof 检查:
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() {
// do nothing
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.func() 0
解析:
结论: '用尽量简单的方式使对象进入可工作状态', 尽量不要在构造器中调用方法 (如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online