⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 小哈学Java 「犬小哈」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

这道题在筛选什么人

“如何理解多态”这个问题看起来很基础,但面试官用它做筛选器,能一口气测出四个层次:

  1. 能不能脱离课本讲清楚——“同一个方法调用,因对象不同而表现不同”,能用自己的话说出来就及格了。
  2. 知不知道底层怎么实现的——编译期看声明类型,运行时看实际类型,靠的是虚方法表(vtable)。能说到这一层的人,一般写过不少代码。
  3. 会不会用多态做设计——面向接口编程、策略模式、工厂模式……这些全靠多态撑着。
  4. 踩没踩过坑——属性有没有多态?静态方法呢?final 方法呢?答得出来说明真写过,答不出来说明只是背过。

一句话结论

多态 = 同一个方法签名,运行时根据实际对象类型执行不同的实现。

实现多态需要三个条件,缺一不可:

  • 继承或实现:得有父子关系(类继承或接口实现)。
  • 方法重写:子类必须覆盖父类的方法。
  • 向上转型:用父类引用指向子类对象——Animal a = new Dog()。

三个条件齐了,调用 a.makeSound() 时,JVM 才会在运行时去找 Dog 的实现,而不是 Animal 的。

JVM 怎么做到“同一行代码调不同方法”的

两步走,各管各的:

编译期——静态检查

编译器只看引用变量的声明类型。你写了 Animal a,编译器就只检查 Animal 类里有没有 makeSound() 方法、访问权限对不对。它完全不关心 a 到底指向哪个子类对象。

运行时——动态派发

JVM 拿到实际对象后,通过虚方法表(vtable) 找到该对象类型中 makeSound() 的真实入口地址。虚方法表是每个类在加载时就建好的,里面按序存着所有可被重写的方法的实际地址。子类重写了哪个方法,对应位置就替换成子类的实现。

这就是“编译看左边,运行看右边”的本质——左边决定你能调什么,右边决定实际执行什么。

一段代码讲透多态的全部要素

interface Animal {
void makeSound();
}

class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}

class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}

public class Demo {
public static void main(String[] args) {
Animal a;

a = new Dog();
a.makeSound(); // Woof! —— 运行时绑定到 Dog

a = new Cat();
a.makeSound(); // Meow! —— 同一个引用,行为完全不同

// 多态的杀手级应用:集合 + 循环
Animal[] animals = {new Dog(), new Cat()};
for (Animal animal : animals) {
animal.makeSound(); // 不需要 if-else 判断类型,新增动物只要实现接口
}
}
}

重点不是这段代码本身,而是最后那个 for 循环。 未来新增一个 Bird 类,只要实现 Animal 接口,循环体一行代码不用改——这就是多态带来的开闭原则:对扩展开放,对修改关闭。

重写 vs 重载:一个纵向一个横向,别混了

维度 重写(Override) 重载(Overload)
发生时机 运行时 编译时
发生位置 父类与子类之间 同一个类内部
方法签名 必须相同 名字相同,参数必须不同
本质 纵向——子类改变父类的行为 横向——同一类提供多种参数选择
与多态的关系 多态的核心机制 和多态无关,只是语法便利

面试时常见的坑:面试官问“重载算不算多态?”答案是不算。 重载在编译期就确定了调哪个方法,不存在“运行时动态派发”。

多态的正确打开方式和常见翻车现场

正确打开方式:

  1. 面向接口编程——声明类型用接口或抽象类,具体实现交给子类。Spring 的依赖注入本质上就是在玩多态。
  2. 遵守里氏替换原则(LSP)——子类必须能完全替代父类,不能偷偷改变父类方法的预期行为。你重写 compareTo() 时把大小关系反过来,调用方的代码就全废了。
  3. 善用设计模式——策略模式(运行时切换算法)、模板方法模式(父类定骨架,子类填细节)、工厂模式(返回接口类型,隐藏创建细节)——全靠多态支撑。

翻车现场:

1. 属性没有多态

class Parent { String name = "Parent"; }
class Child extends Parent { String name = "Child"; }
Parent p = new Child();
System.out.println(p.name); // 输出 "Parent",不是 "Child"!

成员变量的访问在编译期就定了,看声明类型,不看实际类型。这和方法的动态绑定完全不同。

2. 静态方法没有多态

static 方法属于类而不是对象,调用时看引用变量的声明类型。所以父类引用调静态方法,永远调的是父类的版本——这叫“隐藏”,不叫“重写”。

3. private/final 方法不参与多态

private 方法对子类不可见,final 方法禁止重写。没有重写就没有多态,这两种方法的调用在编译期就完全确定了。构造器同理。

文章目录
  1. 1. 这道题在筛选什么人
  2. 2. 一句话结论
  3. 3. JVM 怎么做到“同一行代码调不同方法”的
  4. 4. 一段代码讲透多态的全部要素
  5. 5. 重写 vs 重载:一个纵向一个横向,别混了
  6. 6. 多态的正确打开方式和常见翻车现场