如何理解 Java 中的多态?

#Java#面试八股

如何理解 Java 中的多态?

结论:多态(polymorphism)是"同一调用、不同行为":父类引用指向子类对象,调用被重写的方法时,运行期按对象的实际类型决定执行哪个实现。它需要三个条件——继承或实现、方法重写(override)、父类引用指向子类对象。JVM 通过**动态分派(dynamic dispatch)**查方法表完成,而重载(overload)是编译期静态分派,两者不要混淆。

1. 运行期如何找到正确的方法

Animal a = new Dog();
a.speak();                    // 字节码:invokevirtual Animal.speak

  a(栈上引用)──→ Dog 对象
                     │ 对象头 klass 指针
                     ▼
                 Dog.class 的虚方法表(vtable)
                 ┌─────────────────────────┐
                 │ speak() → Dog.speak     │  ← 子类重写,槽位被替换
                 │ eat()   → Animal.eat   │  ← 未重写,沿用父类
                 └─────────────────────────┘

invokevirtual 的执行:取实际类 → 查 vtable 对应槽位 → 调用

vtable(虚方法表)在类加载准备阶段构建,每个类一份,槽位顺序与父类一致,重写只是替换槽位中的方法指针。因此动态分派是一次数组下标访问,开销很小。

2. 重写与重载的本质不同

维度重载(overload)重写(override)
判定依据方法名相同、参数列表不同子类方法签名与父类相同
分派时机编译期(静态分派)运行期(动态分派)
决定因素变量的静态类型 + 参数类型对象的实际类型
返回类型可以不同必须相同或协变(covariant)
访问权限无约束不能比父类更严格
异常无约束不能抛出更宽的受检异常

3. 代码:三个条件与两个反例

class Animal {
    String name = "animal";                        // 字段
    String speak() { return "..."; }
    void eat() { System.out.println("animal eat"); }
    static String kind() { return "animal"; }      // 静态方法
}

class Dog extends Animal {
    @Override String speak() { return "woof"; }    // 重写
    @Override void eat() { System.out.println("dog eat"); }
}

public class PolyDemo {
    static void feed(Animal a) { a.eat(); }        // 参数是父类引用

    public static void main(String[] args) {
        Animal a = new Dog();                      // 向上转型
        System.out.println(a.speak());             // woof,动态分派
        feed(a);                                   // dog eat
    }
}

两个反例说明"多态不覆盖什么":

class Cat extends Animal {
    String name = "cat";                           // 隐藏父类字段
    static String kind() { return "cat"; }         // 隐藏父类静态方法
}

Animal a = new Cat();
System.out.println(a.name);     // animal —— 字段按静态类型取,没有多态
System.out.println(a.kind());   // animal —— 静态方法按静态类型调用

private 方法、final 方法、static 方法都不参与动态分派;构造器也不是多态调用点。

4. 经典陷阱:构造器里调用可重写方法

class Parent {
    Parent() { init(); }                 // 构造期调用可重写方法
    void init() { System.out.println("parent init"); }
}

class Child extends Parent {
    private final int value;

    Child(int value) {
        super();                         // 先执行父类构造器 → 此时 value 还没赋值
        this.value = value;
    }

    @Override void init() {
        System.out.println("child init, value=" + value); // 输出 0
    }
}

new Child(42);   // child init, value=0

原因:父类构造器执行时子类字段仍是默认值(0),init() 却已经被分派到子类。这类 bug 在框架里很常见,规范做法是构造器只做自身初始化,不要调用可重写方法。

5. 常见追问

追问回答要点
多态的好处是什么?面向接口编程、屏蔽实现差异、易于扩展(新增实现不改调用方)
字段为什么没有多态?字段访问由编译期静态类型决定,字节码是 getfield 固定偏移
static 方法能被"重写"吗?不能,只能隐藏(hiding),调用看引用类型
向上转型与向下转型?向上自动、安全;向下需强转,失败抛 ClassCastException,可用 instanceof 先判断
接口多态和继承多态有区别吗?机制相同,invokeinterface 查接口方法表(itable)
多态和反射的区别?多态是编译期就确定的合法调用,反射是运行期按名字查找,更灵活但更慢
为什么重载不能靠返回类型区分?调用 f() 时编译器无法从返回值推断选哪个重载,签名只含方法名与参数

小结

多态 = 继承/实现 + 重写 + 父类引用指向子类对象,运行期靠 vtable 动态分派。回答时务必区分重写的动态分派与重载的静态分派,再补上"字段、静态方法、private 方法没有多态"这个常见陷阱。

笔记加载中…