Java 中的多态可以理解为:
同一个父类类型的引用,可以指向不同的子类对象;调用同一个被重写的方法时,会根据对象的实际类型执行不同的实现。
你这个案例中:
public void feed(Animal animal)
feed() 方法声明接收的是 Animal,但调用时既可以传入 Cat,也可以传入 Dog:
person.feed(cat);
person.feed(dog);
因为:
Cat extends Animal
Dog extends Animal
所以 Cat 和 Dog 都属于 Animal。
多态通常需要满足三个条件:
你的代码完全满足这些条件。
代码的继承关系是:
Animal
eat()
/ \
/ \
Cat Dog
eat() eat()
catchMouse() watch()
父类 Animal 定义了所有动物都可以进行的行为:
public class Animal {
public void eat() {
}
}
Cat 重写了 eat():
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("Cat eat fish");
}
public void catchMouse() {
System.out.println("Cat catch mouse");
}
}
Dog 也重写了 eat():
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog eat bone");
}
public void watch() {
System.out.println("Dog watch door");
}
}
建议在重写的方法上添加 @Override。这样如果方法名或参数写错,编译器可以及时提醒。
person.feed(cat) 的执行过程首先创建对象:
Person person = new Person();
Cat cat = new Cat();
内存关系可以简单表示为:
cat ──────────→ Cat对象
调用:
person.feed(cat);
但 feed() 的参数类型是:
public void feed(Animal animal)
于是传参时发生了自动向上转型:
Animal animal = cat;
也可以理解为:
Animal animal = new Cat();
此时:
animal的声明类型:Animal
animal的实际类型:Cat
引用关系是:
cat ─────┐
├──→ 同一个Cat对象
animal ──┘
传参不会创建新的 Cat 对象,只是把 cat 中保存的对象引用复制给形参 animal。
animal.eat() 执行的是 Cat.eat()进入 feed():
public void feed(Animal animal) {
animal.eat();
}
虽然 animal 的声明类型是 Animal,但是它实际指向的是 Cat 对象。
因此:
animal.eat();
最终执行的是:
Cat.eat()
输出:
Cat eat fish
这叫作:
可以暂时记成:
对于被重写的实例方法,编译时检查引用的声明类型,运行时根据对象的实际类型决定调用哪个方法。
也就是常说的:
编译看左边,运行看右边
这里“左边”是:
Animal animal
“右边”是它实际指向的:
new Cat()
这句话主要适用于被重写的实例方法,并不是所有成员都遵循这个规则。
catchMouse()虽然 animal 实际指向 Cat,但它的声明类型依然是 Animal:
Animal animal = new Cat();
因此下面的代码不能通过编译:
animal.catchMouse();
因为编译器只会根据声明类型 Animal 检查可用的方法,而 Animal 中没有:
catchMouse()
所以必须先把 Animal 类型的引用向下转型为 Cat:
Cat cat = (Cat) animal;
cat.catchMouse();
这个过程叫作向下转型。
需要注意,强制类型转换没有把对象变成另一个对象:
Cat cat = (Cat) animal;
它只是告诉编译器:
我确认
animal指向的对象是一个Cat,请把这个引用当成Cat类型使用。
instanceof 的作用如果直接进行错误的向下转型:
Animal animal = new Dog();
Cat cat = (Cat) animal;
代码可以通过编译,但运行时会出现:
ClassCastException
因为真正的对象是 Dog,不能把它当成 Cat。
所以你的代码先进行了判断:
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.catchMouse();
}
含义是:
判断
animal指向的对象是否是Cat,只有确定是Cat后才进行类型转换。
同理:
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.watch();
}
person.feed(cat)Cat cat = new Cat();
person.feed(cat);
进入方法后:
animal实际指向Cat对象
执行过程:
animal.eat()
↓
实际对象是Cat
↓
执行Cat.eat()
↓
输出:Cat eat fish
接下来:
animal instanceof Cat
结果为:
true
因此执行:
Cat cat = (Cat) animal;
cat.catchMouse();
输出:
Cat catch mouse
然后:
animal instanceof Dog
结果是:
false
所以不会执行 Dog 部分。
person.feed(dog)Dog dog = new Dog();
person.feed(dog);
执行过程:
animal实际指向Dog对象
↓
animal.eat()
↓
执行Dog.eat()
↓
输出:Dog eat bone
判断:
animal instanceof Cat
结果为 false。
判断:
animal instanceof Dog
结果为 true,于是执行:
Dog dog = (Dog) animal;
dog.watch();
输出:
Dog watch door
因此整个程序的最终输出为:
Cat eat fish
Cat catch mouse
Dog eat bone
Dog watch door
instanceof 写法在较新的 Java 版本中,可以使用模式匹配,判断和转换一起完成:
public void feed(Animal animal) {
animal.eat();
if (animal instanceof Cat cat) {
cat.catchMouse();
}
if (animal instanceof Dog dog) {
dog.watch();
}
}
这里:
animal instanceof Cat cat
同时完成两件事:
1. 判断animal是否指向Cat对象
2. 如果是,自动创建Cat类型的变量cat
因此不再需要手动写:
Cat cat = (Cat) animal;
真正体现多态的是:
public void feed(Animal animal) {
animal.eat();
}
同一条代码:
animal.eat();
当传入 Cat 时执行:
Cat.eat()
当传入 Dog 时执行:
Dog.eat()
也就是说:
相同的方法调用形式
↓
根据实际对象不同
↓
表现出不同的执行结果
而下面这部分主要展示的是向下转型,不是多态本身:
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.catchMouse();
}
如果希望减少 instanceof 和强制类型转换,可以把“吃完之后执行的行为”也抽象到父类中:
public abstract class Animal {
public abstract void eat();
public abstract void doSpecialAction();
}
Cat:
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("Cat eat fish");
}
@Override
public void doSpecialAction() {
System.out.println("Cat catch mouse");
}
}
Dog:
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog eat bone");
}
@Override
public void doSpecialAction() {
System.out.println("Dog watch door");
}
}
那么 Person 就不需要判断动物的具体类型:
public void feed(Animal animal) {
animal.eat();
animal.doSpecialAction();
}
无论传入猫还是狗,都只需要:
person.feed(new Cat());
person.feed(new Dog());
这更能体现多态的核心思想:
使用父类提供统一标准,把具体行为交给不同子类实现。
在 Java Language Specification, Java SE 25 Edition 中,没有一个单独标题为“Polymorphism(多态)”的章节。你这个案例中的“多态”由继承、向上转型、方法重写、编译期方法查找和运行期动态分派等规则共同构成。(Oracle Docs)
| 图中知识点 | JLS 位置 | 对应代码 |
|---|---|---|
父类与子类、extends |
§8.1.4 Superclasses and Subclasses | Cat extends Animal |
| 父类变量引用子类对象 | §4.12.2 Variables of Reference Type | Animal animal = new Cat() |
| 子类实参传给父类形参 | §5.3 Invocation Contexts | feed(cat) |
| 向上转型 | §5.1.5 Widening Reference Conversion | Cat → Animal |
| 方法重写 | §8.4.8.1 Overriding (by Instance Methods) | Cat.eat() 重写 Animal.eat() |
@Override 注解 |
§9.6.4.4 @Override | @Override public void eat() |
| 编译期根据声明类型查找方法 | §15.12.1 Compile-Time Step 1 | 为什么 animal.catchMouse() 编译失败 |
| 运行期动态方法分派 | §15.12.4.4 Locate Method to Invoke | animal.eat() 最终执行 Cat.eat() |
instanceof 判断 |
§15.20.2 The instanceof Operator | animal instanceof Cat |
| 强制类型转换语法 | §15.16 Cast Expressions | (Cat) animal |
| 向下转型及异常 | §5.1.6 Narrowing Reference Conversion | 错误转换可能抛出 ClassCastException |
Cat extends Animal:继承关系位置:
JLS §8.1.4 — Superclasses and Subclasses
这一节说明,普通类声明中的 extends 子句指定该类的直接父类,并定义什么是直接父类、直接子类、父类和子类。(Oracle Docs)
对应你的代码:
public class Cat extends Animal {
}
public class Dog extends Animal {
}
因此:
Animal
├── Cat
└── Dog
Animal 是 Cat 和 Dog 的直接父类;Cat 和 Dog 是 Animal 的直接子类。
位置:
JLS §4.12.2 — Variables of Reference Type
JLS 明确规定:
类类型
T的变量,可以保存null,也可以保存对T的实例或者T的任意子类实例的引用。(Oracle Docs)
因此下面的代码合法:
Animal animal1 = new Cat();
Animal animal2 = new Dog();
对应关系是:
变量的声明类型 对象的实际运行时类型
Animal animal1 → Cat
Animal animal2 → Dog
这正是多态引用的类型基础。
需要注意的是,这里存储的是对象引用。它不是把 Cat 对象复制成一个 Animal 对象,也不是把对象本身变成了 Animal。
person.feed(cat):方法传参中的向上转型你的方法是:
public void feed(Animal animal)
调用时传入:
Cat cat = new Cat();
person.feed(cat);
这里涉及两节。
JLS §5.3 规定,方法调用时,实参值会被赋给对应的形参变量,并允许发生特定的类型转换,其中包括扩大引用转换。(Oracle Docs)
因此可以把这个传参过程理解为:
Animal animal = cat;
JLS §5.1.5 规定:当引用类型 S 是引用类型 T 的子类型时,可以从 S 转换到 T。这种转换不需要特殊的运行时操作,也不会因为该转换抛出运行时异常。(Oracle Docs)
你的代码中:
Cat 是 Animal 的子类型
Dog 是 Animal 的子类型
所以都可以自动转换:
Cat → Animal
Dog → Animal
这就是通常所说的向上转型。
Cat.eat() 和 Dog.eat():方法重写位置:
JLS §8.4.8.1 — Overriding (by Instance Methods)
这一节定义了实例方法在什么条件下构成重写。核心条件包括:
C 是父类 A 的子类;在你的代码中:
public class Animal {
public void eat() {
}
}
Cat 声明:
public void eat() {
System.out.println("Cat eat fish");
}
Dog 声明:
public void eat() {
System.out.println("Dog eat bone");
}
因此:
Cat.eat() 重写 Animal.eat()
Dog.eat() 重写 Animal.eat()
@Override 在哪里定义位置:
JLS §9.6.4.4 — @Override
JLS 说明,@Override 用来帮助程序员尽早发现“本来想重写,实际上却写成了重载或其他方法”的错误。如果标注的方法不符合重写等规定,就会发生编译错误。(Oracle Docs)
因此建议写成:
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("Cat eat fish");
}
}
以及:
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog eat bone");
}
}
@Override 并不产生重写关系;它只是让编译器检查这个方法是否真的构成重写。
animal.catchMouse() 不能直接调用位置:
JLS §15.12.1 — Compile-Time Step 1: Determine Type to Search
这一节规定,在编译一个方法调用时,编译器首先确定要在哪个类型中查找方法。
对于:
animal.catchMouse();
如果 animal 是一个变量名,那么 JLS 规定,编译器使用该变量的声明类型来搜索方法。(Oracle Docs)
假设:
Animal animal = new Cat();
那么:
animal 的声明类型:Animal
animal 指向对象的实际类型:Cat
编译器检查:
animal.catchMouse();
时会先去 Animal 类型中寻找 catchMouse()。但 Animal 没有该方法:
public class Animal {
public void eat() {
}
}
所以不能通过编译。
这就是“编译看左边”比较准确的 JLS 依据:
Animal animal = new Cat();
// ↑ 编译期使用这个声明类型检查可调用的方法
animal.eat() 执行子类方法最核心的位置:
JLS §15.12.4.4 — Locate Method to Invoke
JLS 规定,对于允许重写的实例方法,需要执行动态方法查找。查找从目标对象的实际运行时类开始。(Oracle Docs)
例如:
Animal animal = new Cat();
animal.eat();
分为两个阶段。
根据 animal 的声明类型 Animal,检查:
Animal 中是否存在 eat()
存在,所以编译通过。
运行时发现:
animal 实际指向 Cat 对象
动态查找从 Cat 开始。如果 Cat 中存在重写后的 eat(),就调用该方法。JLS 明确说明,动态查找从目标对象的实际运行时类开始,并优先选择重写的方法。(Oracle Docs)
所以执行:
Cat.eat()
同理:
Animal animal = new Dog();
animal.eat();
执行:
Dog.eat()
JLS 自己也提供了一个与此几乎相同的示例:
Point p = new ColoredPoint();
p.move(...);
其中被重写的方法根据对象的运行时类型选择。规范将其描述为方法在运行时“较晚”决定,而不是仅根据编译期类型提前决定。(Oracle Docs)
因此,“编译看左边,运行看右边”更完整地应写为:
对于被重写的实例方法,编译期根据引用表达式的声明类型检查方法是否可调用;运行期根据对象的实际类型动态选择最终实现。
instanceof 在哪里位置:
JLS §15.20.2 — The instanceof Operator
这一节说明,instanceof 可以执行:
你的代码:
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.catchMouse();
}
运行时规则是:
animal 为 null,结果为 false;null,并且可以安全转换成 Cat 而不抛出 ClassCastException,结果为 true;false。(Oracle Docs)所以:
Animal animal = new Cat();
animal instanceof Cat // true
animal instanceof Dog // false
instanceof 模式匹配你的代码可以写成:
if (animal instanceof Cat cat) {
cat.catchMouse();
}
这仍然主要对应:
JLS §15.20.2 — The instanceof Operator
JLS 将右侧为 Pattern 的形式称为模式匹配。当模式匹配成功时,模式变量会被初始化。(Oracle Docs)
因此:
animal instanceof Cat cat
同时完成:
1. 判断 animal 指向的对象是否符合 Cat 类型
2. 匹配成功后创建并初始化变量 cat
(Cat) animal:向下转型这里要看三个位置。
这一节定义了强制类型转换表达式:
(Cat) animal
对于引用类型,转换会在运行时检查引用指向的对象是否与目标类型兼容。(Oracle Docs)
从较一般的引用类型转换到更具体的引用类型,属于缩小引用转换,也就是通常说的向下转型。某些转换需要在运行时检查;检查失败时会抛出 ClassCastException。(Oracle Docs)
正确情况:
Animal animal = new Cat();
Cat cat = (Cat) animal;
错误情况:
Animal animal = new Dog();
Cat cat = (Cat) animal;
第二段代码可能通过编译,但运行时会因为实际对象是 Dog 而抛出:
ClassCastException
JLS 在动态方法调用示例中特别说明:强制转换不会改变对象所属的类,只会检查对象的类是否与指定类型兼容。(Oracle Docs)
所以:
Cat cat = (Cat) animal;
并不是把一个 Animal 对象“变成”新的 Cat 对象,而是得到一个声明类型为 Cat、仍然指向原对象的引用。
对于你当前这个案例,最重要的是依次阅读:
其中,真正解释 Java 运行时多态核心机制的是:
§15.12.4.4 Locate Method to Invoke
你可以把整个案例浓缩成:
§8.1.4:Cat、Dog 是 Animal 的子类
↓
§4.12.2、§5.1.5:Animal 引用可以指向 Cat、Dog
↓
§8.4.8.1:Cat、Dog 重写 eat()
↓
§15.12.1:编译时从 Animal 检查是否有 eat()
↓
§15.12.4.4:运行时根据实际对象调用 Cat.eat() 或 Dog.eat()