多态 | JavaSE
多态
一、学习目标
学完本章,你应该能够:
- 能够解释什么是多态(Polymorphism)。
- 能够理解“父类类型引用指向子类对象”的基本代码形式。
- 能够区分变量的编译时类型(Static Type)与对象的运行时类型(Runtime Class)。
- 能够解释重写实例方法为什么能够表现出不同运行时行为。
- 能够准确理解“编译看左边,运行看右边”这一教学口诀的适用范围。
- 能够解释为什么成员变量不存在与重写实例方法相同的动态绑定行为。
- 能够解释为什么父类类型变量不能直接调用子类独有方法。
- 能够使用父类类型作为方法参数统一接收不同子类对象。
- 能够理解多态如何提高扩展性、降低调用代码对具体子类的依赖。
- 能够为下一章对象类型转换与
instanceof建立知识基础。
二、核心知识
2.1 什么是多态
多态英文:
Polymorphism
字面含义可以理解为:
同一个抽象类型,在运行过程中可以表现出多种具体形态和行为。
例如:
Animal
├── Wolf
└── Tortoise
它们都属于:
Animal
但是:
Wolf.run()
可能是:
跑得非常快
而:
Tortoise.run()
可能是:
跑得非常慢
于是:
相同的
run()行为,在不同具体对象上表现出不同实现。
这就是理解 Java 多态最重要的入口。
2.2 第一个多态代码
父类:
public class Animal {
public void run() {
System.out.println("动物会跑");
}
}
狼:
public class Wolf extends Animal {
@Override
public void run() {
System.out.println("狼跑得非常快");
}
}
乌龟:
public class Tortoise extends Animal {
@Override
public void run() {
System.out.println("乌龟跑得非常慢");
}
}
以前可能这样创建:
Wolf wolf = new Wolf();
Tortoise tortoise = new Tortoise();
多态形式则可以:
Animal a1 = new Wolf();
Animal a2 = new Tortoise();
注意两边类型不同:
左边
Animal
右边
Wolf / Tortoise
这就是 Java 类继承多态非常典型的代码形式。
2.3 “父类引用指向子类对象”是什么意思
代码:
Animal animal = new Wolf();
一定不要错误理解成:
把 Wolf 对象变成了 Animal 对象。
真正发生的是:
实际创建的对象:
Wolf
保存该对象引用的变量类型:
Animal
可以画成:
Animal animal
│
│ 引用
▼
┌───────────────┐
│ Wolf 对象 │
└───────────────┘
右边:
new Wolf()
真正创建的是:
Wolf 对象
左边只是规定:
通过 animal 这个变量,
当前按照 Animal 类型来使用这个对象。
2.4 编译时类型与运行时类型
理解多态必须区分两个概念。
代码:
Animal animal = new Wolf();
变量:
animal
的编译时类型是:
Animal
而它当前引用对象的运行时类型是:
Wolf
即:
编译时类型
Animal
运行时对象
Wolf
这两个类型在普通:
Wolf wolf = new Wolf();
中相同。
但在多态代码中可以不同。
这正是后续理解:
为什么某些方法能调用
为什么某些方法不能调用
最终执行哪个重写方法
的关键。
2.5 多态与方法重写
父类:
public class Animal {
public void run() {
System.out.println("Animal.run");
}
}
子类:
public class Wolf extends Animal {
@Override
public void run() {
System.out.println("Wolf.run");
}
}
现在:
Animal animal = new Wolf();
animal.run();
最终执行:
Wolf.run
这就是典型的:
运行时方法分派。
变量:
animal
虽然声明为:
Animal
但它引用的实际对象是:
Wolf
因此对于可以被重写的实例方法,运行时会根据实际对象选择对应实现。
2.6 “编译看左边,运行看右边”是什么意思
Java 入门中经常使用口诀:
方法调用:编译看左边,运行看右边。
例如:
Animal animal = new Wolf();
animal.run();
编译阶段
编译器先根据:
左边变量类型 Animal
判断:
Animal
是否具有可调用的:
run()
如果 Animal 根本不存在可访问的 run(),代码就不能因为右边是 Wolf 而直接通过编译。
运行阶段
如果:
run()
属于可以发生重写动态分派的实例方法,那么运行时会根据:
实际对象 Wolf
寻找最终应该执行的实现。
所以得到:
Wolf.run()
2.7 口诀不能乱套
一定要注意:
“编译看左边,运行看右边”是帮助理解重写实例方法动态调用的教学口诀。
它不是所有 Java 成员访问的万能规则。
例如:
- 成员变量;
- static 方法;
- private 方法;
- 构造器;
都不能机械套用同一个口诀。
真正专业的理解应该是:
编译阶段
根据变量的静态类型检查
这个调用是否合法以及可见哪些成员
运行阶段
对于可动态分派的实例方法
根据实际对象类型选择最终重写实现
2.8 多态中的成员变量
父类:
public class Animal {
String name = "Animal";
}
子类:
public class Wolf extends Animal {
String name = "Wolf";
}
测试:
Animal animal = new Wolf();
System.out.println(animal.name);
结果访问的是:
Animal 中声明的 name
而不是根据实际对象动态选择:
Wolf.name
为什么?
因为:
字段访问不存在实例方法重写那样的动态查找。
字段解析主要依据表达式的编译时类型。
因此原课程总结:
方法:
编译看左边,运行看右边
成员变量:
编译看左边,运行也看左边
作为入门口诀是非常有用的。
2.9 完整字段 + 方法对比
class Animal {
String name = "Animal";
public void run() {
System.out.println("Animal.run");
}
}
class Wolf extends Animal {
String name = "Wolf";
@Override
public void run() {
System.out.println("Wolf.run");
}
}
测试:
Animal animal = new Wolf();
System.out.println(animal.name);
animal.run();
结果表现为:
字段:
Animal.name
实例方法:
Wolf.run()
这里是本章最核心的一组对比。
必须达到闭卷能够解释。
2.10 为什么字段没有“多态”
字段没有:
方法重写(Overriding)
这一机制。
例如:
Animal.name
和:
Wolf.name
属于两个字段声明。
子类字段不会像实例方法一样通过运行时动态分派“重写”父类字段。
所以原教学资料强调:
Java 中属性/成员变量不谈方法意义上的多态。
这是非常重要的区别。
2.11 多态需要哪些基础
按照当前课程主线,可以形成:
继承
↓
方法重写
↓
父类引用指向子类对象
↓
运行时表现不同的重写行为
例如:
Animal animal = new Wolf();
animal.run();
要看到:
Wolf.run()
需要:
Wolf extends AnimalWolf重写run()- 使用
Animal类型引用Wolf对象
于是才形成最典型、最直观的行为多态。
2.12 “方法重写是多态前提”要准确理解
课程中通常把多态前提总结为:
有继承 / 实现关系
父类引用指向子类对象
存在方法重写
这非常适合学习:
行为多态。
但是更严谨地说:
Animal animal = new Wolf();
即使 Wolf 当前没有重写任何方法,这种:
父类型变量引用子类型对象
本身仍然合法。
只是如果没有任何重写行为,就很难从方法执行结果上直观看到:
不同对象表现不同实现
所以可以这样记:
继承关系 + 父类型引用子对象,是类型多态的基础;方法重写让行为多态真正表现出来。
2.13 对象多态与行为多态
原课程把多态理解成两个层面。
对象多态
同一个父类型变量可以引用不同子类对象:
Animal a1 = new Wolf();
Animal a2 = new Tortoise();
同样都是:
Animal
变量。
右边实际对象却可以不同。
行为多态
调用:
a1.run();
a2.run();
如果子类分别重写:
run()
则最终产生:
Wolf 的行为
Tortoise 的行为
也就是:
同一个父类型方法调用形式,对不同对象产生不同实现行为。
2.14 多态最大的价值之一:统一调用
假设没有多态思维:
Wolf wolf = new Wolf();
Tortoise tortoise = new Tortoise();
如果需要一个“让动物跑起来”的方法,可能开始写:
public static void go(Wolf wolf) {
wolf.run();
}
还要:
public static void go(Tortoise tortoise) {
tortoise.run();
}
如果未来增加:
Tiger
Rabbit
Horse
就继续增加大量方法。
但它们全部是:
Animal
因此可以:
public static void go(Animal animal) {
animal.run();
}
于是:
go(new Wolf());
go(new Tortoise());
都可以。
2.15 父类类型参数为什么能接收多个子类
方法:
public static void go(Animal animal) {
animal.run();
}
形参类型:
Animal
所以任何属于:
Animal 子类型
的对象都可以传入。
例如:
go(new Wolf());
go(new Tortoise());
以后增加:
class Rabbit extends Animal {
}
同样:
go(new Rabbit());
通常不需要修改:
go(Animal animal)
的方法签名。
这就是多态带来的一个非常重要的扩展性优势。
2.16 多态降低调用方对具体子类的依赖
没有多态时:
public static void go(Wolf wolf) {
}
代码直接依赖:
Wolf
如果以后希望支持:
Tortoise
就必须继续修改设计。
使用:
public static void go(Animal animal) {
}
调用方主要依赖:
Animal 抽象
而不是某个具体实现。
于是:
Wolf
Tortoise
Rabbit
Dog
...
都可以作为实际对象进入。
这就是原课程所说的:
右边对象更加解耦,便于扩展与维护。
更专业一点,可以理解成:
调用代码依赖稳定父类型,而具体运行行为由实际子类对象决定。
2.17 多态并不是“彻底没有耦合”
看到:
Animal animal = new Wolf();
不能说:
程序已经完全没有任何耦合。
例如这里:
new Wolf()
仍然显式出现了具体类型。
多态真正减少的是:
使用对象的那部分代码对具体实现类型的直接依赖。
例如:
public static void go(Animal animal) {
animal.run();
}
go() 内部完全不需要知道当前是:
Wolf
还是
Tortoise
这就是非常重要的设计价值。
2.18 多态下为什么不能直接调用子类独有方法
假设:
public class Wolf extends Animal {
@Override
public void run() {
System.out.println("Wolf.run");
}
public void eatSheep() {
System.out.println("狼吃羊");
}
}
现在:
Animal animal = new Wolf();
可以:
animal.run();
但不能直接:
animal.eatSheep();
为什么?
因为编译器首先看到:
animal 的静态类型
Animal
然后检查:
Animal 中是否存在 eatSheep()
答案:
不存在
所以编译阶段已经失败。
编译器不会因为右边现在恰好:
new Wolf()
就允许通过父类型变量随意使用所有 Wolf 独有 API。
2.19 这不是说 Wolf 对象失去了独有方法
Animal animal = new Wolf();
实际对象仍然是:
Wolf
它仍然具有:
eatSheep()
这个能力。
问题只是:
当前我们持有的是一个
Animal类型视角的引用。
因此编译器只允许直接使用:
Animal 类型所承诺的成员
如何重新获得更加具体的子类类型视角,从而访问独有功能?
这是下一章:
02-11 类型转换与 instanceof
专门解决的问题。
2.20 多态真正依赖的是“公共能力”
为什么:
animal.run();
能够使用?
因为:
run()
首先是:
Animal 类型公开出来的共同能力
所以所有子类都可以:
- 继承默认实现;
- 或者提供自己的重写实现。
调用代码只需要知道:
它是 Animal
它可以 run()
不必知道:
到底是哪一种 Animal
这就是面向对象多态最核心的思想之一。
三、使用方法
3.1 Animal / Wolf / Tortoise
父类:
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void run() {
System.out.println("Animal.run");
}
}
狼:
public class Wolf extends Animal {
public Wolf(String name) {
super(name);
}
@Override
public void run() {
System.out.println(
getName() + "跑得非常快"
);
}
public void eatSheep() {
System.out.println(
getName() + "正在捕猎"
);
}
}
乌龟:
public class Tortoise extends Animal {
public Tortoise(String name) {
super(name);
}
@Override
public void run() {
System.out.println(
getName() + "跑得非常慢"
);
}
public void shrinkHead() {
System.out.println(
getName() + "缩进龟壳"
);
}
}
测试:
public class Test {
public static void main(String[] args) {
Animal a1 = new Wolf("灰太狼");
Animal a2 = new Tortoise("小龟");
a1.run();
a2.run();
}
}
同一个:
run()
调用形式:
a1.run()
a2.run()
最终却根据不同实际对象执行不同实现。
这就是行为多态。
3.2 使用父类型参数统一处理
public class AnimalService {
public static void go(Animal animal) {
System.out.println("比赛开始");
animal.run();
System.out.println("比赛结束");
}
}
调用:
AnimalService.go(
new Wolf("狼")
);
AnimalService.go(
new Tortoise("乌龟")
);
如果以后新增:
public class Rabbit extends Animal {
public Rabbit(String name) {
super(name);
}
@Override
public void run() {
System.out.println(
getName() + "快速跳跃"
);
}
}
原来的:
go(Animal animal)
仍然可以:
AnimalService.go(
new Rabbit("兔子")
);
不用为 Rabbit 再写一套:
go(Rabbit rabbit)
3.3 支付系统中的多态雏形
父类:
public class Card {
private double balance;
public Card(double balance) {
this.balance = balance;
}
protected void setBalance(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public void consume(double money) {
balance -= money;
}
}
金卡:
public class GoldCard extends Card {
public GoldCard(double balance) {
super(balance);
}
@Override
public void consume(double money) {
double actual = money * 0.8;
setBalance(
getBalance() - actual
);
}
}
银卡:
public class SilverCard extends Card {
public SilverCard(double balance) {
super(balance);
}
@Override
public void consume(double money) {
double actual = money * 0.9;
setBalance(
getBalance() - actual
);
}
}
收银机:
public static void pay(
Card card,
double money
) {
card.consume(money);
}
于是:
pay(
new GoldCard(5000),
100
);
pay(
new SilverCard(3000),
100
);
pay() 完全不需要分别写:
payGoldCard()
paySilverCard()
它只依赖共同父类型:
Card
这就是多态在业务建模中的直接价值。
四、原理与进阶
4.1 动态方法分派
对于:
Animal animal = new Wolf();
animal.run();
可以从两个阶段理解。
编译阶段
根据:
Animal
确认:
run()
是否存在且可访问。
运行阶段
实际对象:
Wolf
于是 JVM 按 Java 实例方法动态查找规则寻找最终重写实现。
最后执行:
Wolf.run()
这类行为通常称为:
动态方法分派(Dynamic Method Dispatch)
或者更宽泛地说:
动态绑定(Dynamic Binding / Late Binding)
这是 Java 运行时多态的重要机制。
4.2 为什么字段没有动态方法分派
JLS 对字段访问和实例方法调用采取不同规则。
字段:
animal.name
根据表达式的编译时类型确定字段声明。
而:
animal.run()
对于可重写的实例方法,可以进一步根据对象的实际运行时类做动态查找。
所以:
字段
静态解析
重写实例方法
运行时可动态分派
不能把二者混为一谈。
4.3 多态最强的地方不是“少写几个 if”
多态的价值不是单纯追求:
少写几行代码
更重要的是:
让调用代码面向稳定的父类型能力,而不是依赖每一种具体子类实现。
例如:
public void pay(Card card)
比:
public void payGoldCard(GoldCard card)
public void paySilverCard(SilverCard card)
更容易扩展。
未来新增:
DiamondCard
只要它符合父类型约定,就可以进入原有调用流程。
4.4 多态与开放扩展
假设:
public static void go(Animal animal) {
animal.run();
}
以后增加:
Horse
Tiger
Rabbit
通常可以通过增加新的子类实现扩展行为,而不必不断修改:
go()
这体现了面向对象设计中一个非常重要的方向:
让稳定代码依赖抽象的共同能力,把变化放到不同具体实现中。
正式的软件设计原则会在后续设计模式与框架学习中逐渐深入。
4.5 现在为什么不讲 instanceof
当前已经发现多态存在一个问题:
Animal animal = new Wolf();
// animal.eatSheep();
无法直接调用子类独有功能。
但解决这个问题需要学习:
对象类型转换
向上类型转换
向下类型转换
ClassCastException
instanceof
模式匹配
冻结课程已经单独安排:
02-11 类型转换与 instanceof
所以本章只认识问题:
多态变量只能直接使用其编译时类型允许访问的成员。
解决方案留到下一章。
五、实践应用
5.1 统一动物比赛入口
假设不断增加:
Wolf
Tortoise
Rabbit
Horse
不应该设计:
race(Wolf wolf)
race(Tortoise tortoise)
race(Rabbit rabbit)
race(Horse horse)
如果它们都是:
Animal
并共同拥有:
run()
那么:
public static void race(Animal animal) {
animal.run();
}
就能够形成稳定入口。
5.2 多态为后续接口编程打基础
本章使用:
Animal animal = new Wolf();
建立父类多态。
以后学习接口以后会出现:
USB device = new Mouse();
USB device = new Keyboard();
本质思想非常相似:
用更抽象的类型接收不同具体实现对象。
因此本章不是一个孤立知识。
后续:
多态
↓
抽象类
↓
接口
↓
面向接口编程
↓
设计模式
↓
Spring 等框架思想
都会不断使用这一思想。
六、常见问题
6.1 什么是多态?
在继承或实现体系中,同一个父类型或接口类型可以引用不同具体实现对象,并让可重写行为根据实际对象表现出不同实现。
当前阶段重点学习:
类继承多态。
6.2 多态最典型的代码是什么?
Animal animal = new Wolf();
即:
父类类型引用指向子类对象。
6.3 左边 Animal,右边 Wolf,到底创建了谁?
创建的是:
Wolf 对象
Animal 只是引用变量的静态类型。
6.4 animal.run() 为什么执行 Wolf.run()?
因为:
- 编译阶段确认
Animal中存在run(); - 运行阶段实际对象是 Wolf;
- Wolf 重写了
run(); - 因此动态调用 Wolf 的实现。
6.5 “编译看左边,运行看右边”任何时候都成立吗?
不成立。
它主要用于帮助理解:
可动态分派的重写实例方法。
字段、static 方法等不能机械套用。
6.6 成员变量有多态吗?
没有与重写实例方法相同的动态绑定行为。
例如:
Animal a = new Wolf();
System.out.println(a.name);
字段解析基于:
Animal
这一编译时类型。
6.7 有继承就一定有行为多态吗?
不一定。
如果子类没有提供不同重写实现,就未必能观察到:
同一方法在不同对象上表现不同
的行为。
6.8 方法重写是不是父类引用子类对象合法的绝对前提?
不是。
例如:
Animal animal = new Wolf();
本身只需要满足合法的类型兼容关系。
但要表现典型的:
运行时不同方法行为
通常需要方法重写。
6.9 多态有什么好处?
核心包括:
- 统一处理不同子类型对象;
- 提高方法参数扩展性;
- 减少调用代码对具体子类的依赖;
- 便于增加新的实现类型;
- 为面向抽象和接口编程奠定基础。
6.10 多态有什么限制?
通过:
Animal animal
只能直接使用:
Animal 类型允许访问的成员
不能直接调用:
Wolf 独有方法
6.11 Wolf 的独有方法是不是消失了?
没有。
对象真实类型仍然是 Wolf。
只是当前引用类型:
Animal
限制了编译阶段可直接使用的 API。
6.12 怎么调用子类独有功能?
需要进行对象类型转换并判断对象真实类型。
这是下一章:
02-11 类型转换与 instanceof
的内容。
七、练习与验收
7.1 知识问答
- 什么是多态?
- 多态英文是什么?
- 什么是父类引用指向子类对象?
Animal a = new Wolf();真正创建了什么对象?- 什么是变量的编译时类型?
- 什么是对象运行时类型?
a.run()在编译阶段主要看什么?a.run()在运行阶段为什么可能调用子类实现?- “编译看左边,运行看右边”适用于什么?
- 为什么不能把这个口诀应用到所有成员?
- 成员变量是否存在方法一样的动态重写?
- 什么叫对象多态?
- 什么叫行为多态?
- 方法重写在行为多态中发挥什么作用?
- 父类类型参数为什么能接收多个子类对象?
- 多态如何提升程序扩展性?
- 多态如何降低对具体子类的依赖?
- 为什么父类型变量不能直接调用子类独有方法?
- 子类独有方法是否真的从对象中消失?
- 这个问题将在下一章通过什么知识解决?
7.2 代码阅读
禁止运行:
class Animal {
String name = "Animal";
public void run() {
System.out.println("Animal.run");
}
}
class Dog extends Animal {
String name = "Dog";
@Override
public void run() {
System.out.println("Dog.run");
}
public void lookHome() {
System.out.println("lookHome");
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
animal.run();
System.out.println(animal.name);
// animal.lookHome();
}
}
回答:
animal的编译时类型是什么?- 实际对象类型是什么?
animal.run()最终执行哪个方法?- 为什么?
animal.name最终访问哪个字段?- 为什么字段与方法规则不同?
animal.lookHome()为什么不能编译?- Dog 对象是否真的没有
lookHome()?
继续阅读:
class Animal {
public void run() {
System.out.println("Animal");
}
}
class Wolf extends Animal {
@Override
public void run() {
System.out.println("Wolf");
}
}
class Tortoise extends Animal {
@Override
public void run() {
System.out.println("Tortoise");
}
}
public class Test {
public static void go(Animal animal) {
animal.run();
}
public static void main(String[] args) {
go(new Wolf());
go(new Tortoise());
}
}
回答:
go()的参数类型是什么?- 为什么 Wolf 可以传入?
- 为什么 Tortoise 可以传入?
- 两次
animal.run()是否执行相同实现? - 如果新增 Rabbit,
go()是否必须重载一份?
7.3 手写代码
任务一:Animal 多态
设计:
Animal
├── Dog
├── Cat
└── Rabbit
要求:
Animal提供eat()。- 三个子类分别重写
eat()。 - 使用:
Animal animal
分别接收三个子类对象。
- 观察相同调用:
animal.eat();
的不同结果。
任务二:统一方法参数
设计:
public static void feed(Animal animal)
要求:
- 能接收 Dog。
- 能接收 Cat。
- 能接收 Rabbit。
- 方法内部只调用父类共同能力。
- 禁止针对三个子类分别重载三个
feed()。
任务三:字段与方法对比
父类、子类都定义:
name
并都定义:
run()
其中子类重写 run()。
使用:
Parent p = new Child();
分别访问:
p.name;
p.run();
先写出预测,再运行验证。
最后用自己的话解释:
为什么成员变量和实例方法表现不同?
7.4 Debug
代码:
class Animal {
public void run() {
}
}
class Dog extends Animal {
public void lookHome() {
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
animal.lookHome();
}
}
要求:
- 真正对象是不是 Dog?
- 为什么仍然编译失败?
- 编译器判断成员可访问性时看什么类型?
- 不要在本章直接写类型转换解决,先说明下一章应该学习什么。
分析:
class Animal {
}
class Dog extends Animal {
}
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
}
}
问题:
- 这里是否合法?
- Dog 没有重写任何方法,赋值是否仍然成立?
- 为什么说“重写是行为多态的关键条件”比“没有重写就绝对不存在父类引用子对象”更加准确?
7.5 综合训练
设计一个加油站收银系统:
Card
├── GoldCard
└── SilverCard
要求:
Card 提供:
balance
consume(double money)
GoldCard:
8 折消费
SilverCard:
9 折消费
设计统一收银方法:
public static void pay(
Card card,
double money
)
要求:
- 金卡和银卡都可以传入。
pay()内部禁止使用if判断具体卡类型。pay()只调用:
card.consume(money);
- 最终由不同卡对象的重写方法完成对应消费规则。
- 新增一种
DiamondCard后,分析pay()是否需要修改。
完成后用一段话回答:
多态到底把“变化”放到了哪里,又让哪一部分代码保持了稳定?
7.6 本章验收
关闭资料和 AI 自动补全,确认自己能够:
- [ ] 30 秒解释多态是什么。
- [ ] 手写
Animal a = new Wolf();。 - [ ] 区分编译时类型与运行时类型。
- [ ] 解释父类引用为什么可以指向子类对象。
- [ ] 解释实例方法为什么“编译看左边、运行看实际对象”。
- [ ] 说明口诀适用范围。
- [ ] 解释字段为什么没有同样的动态分派。
- [ ] 分析
Animal a = new Dog(); a.run(); a.name;。 - [ ] 解释多态为什么提高方法参数扩展性。
- [ ] 使用一个
Animal参数接收多个子类。 - [ ] 解释为什么不能直接调用子类独有功能。
- [ ] 知道解决该问题需要下一章的类型转换与
instanceof。 - [ ] 解释“依赖父类型而非具体子类”的意义。
- [ ] 独立设计 Card / GoldCard / SilverCard 多态支付模型。
如果你只会背:
编译看左边,运行看右边
却无法解释:
什么在编译阶段决定、
什么在运行阶段决定、
为什么字段又不是这个规则,
那么这一章还没有真正掌握。