主题
界面
主题
面向对象的三大特性是封装、继承、多态。封装管"数据怎么保护",继承管"代码怎么复用",多态管"代码怎么扩展"。
多态是这三个里最抽象、也最有价值的一个。它决定了你能不能写出"新增一种类型时,不需要修改已有代码"的程序。框架之所以能通过注解和接口把你写的类接进它们的流程,靠的也是多态。
完成本节后,你应该能够:
if-else 类型分支;instanceof 模式匹配;extends,知道 super 的含义吗?(《继承与重写》)@Override 注解的作用吗?假设做一个支付功能,有三种渠道,每种手续费不同。不假思索的写法:
public long fee(String channel, long amount) {
if ("alipay".equals(channel)) {
return Math.round(amount * 0.006);
} else if ("wechat".equals(channel)) {
return Math.round(amount * 0.006);
} else if ("bank".equals(channel)) {
return Math.round(amount * 0.005);
} else {
throw new IllegalArgumentException("未知渠道:" + channel);
}
}这段代码能跑,但每加一个渠道就要回到这个方法里改一次:
多态把"哪种渠道"的判断,从方法内部移到创建对象时:
PaymentChannel channel = new AlipayChannel(); // 只有这里知道具体类型
long fee = channel.feeInCents(amount); // 这里不再关心是哪种判断只发生一次,扩展点集中在类的体系里。这是多态的核心价值。
同时满足这三个条件,多态才会发生:
@Override);PaymentChannel channel = new AlipayChannel();)。缺任何一个,都退化成普通的方法调用。
PaymentChannel channel = new AlipayChannel();
// ↑ 编译时类型 ↑ 运行时类型| 编译时类型 | 运行时类型 | |
|---|---|---|
| 由谁决定 | 变量声明的类型 | new 出来的真实对象 |
| 决定什么 | 能调用哪些方法 | 实际执行哪个方法体 |
| 检查时机 | 编译期 | 运行期(动态绑定) |
记住两句话:
channel 只能用 PaymentChannel 声明过的方法,哪怕实际是支付宝渠道,也不能调支付宝特有的方法;AlipayChannel 重写后的方法体。PaymentChannel channel = new AlipayChannel(); // 向上转型:自动、安全
AlipayChannel alipay = (AlipayChannel) channel; // 向下转型:需要强转,可能失败向上转型永远安全,因为子类一定具备父类的所有能力。向下转型是"收回"能力,编译器无法保证对象真的是那个子类,转错了就在运行时抛 ClassCastException。
Java 16 之后用 instanceof 模式匹配更安全:
if (channel instanceof AlipayChannel alipay) {
System.out.println(alipay.alipaySpecificMethod()); // 变量已经转好,直接可用
}这是最容易踩坑的部分,记牢四条例外:
| 成员 | 是否多态 | 说明 |
|---|---|---|
| 普通方法(被重写) | 是 | 运行时看实际类型 |
| 字段(成员变量) | 否 | 编译时看声明类型 |
static 方法 | 否 | 静态方法属于类,不存在"重写",只有隐藏 |
private / 构造方法 | 否 | 子类根本访问不到,谈不上重写 |
public class Animal {
public String speak() {
return "...";
}
public static void main(String[] args) {
Animal[] animals = {new Animal(), new Dog(), new Cat()};
for (Animal animal : animals) {
System.out.println(animal.speak());
}
}
}
class Dog extends Animal {
@Override
public String speak() {
return "汪";
}
}
class Cat extends Animal {
@Override
public String speak() {
return "喵";
}
}输出是 ...、汪、喵。注意循环里只有一句 animal.speak(),没有任何 if 判断,却调到了三个不同的实现。
public class PaymentChannel {
public String name() {
return "通用渠道";
}
public long feeInCents(long amountInCents) {
return 0L;
}
public long actualReceive(long amountInCents) {
return amountInCents - feeInCents(amountInCents);
}
}
class AlipayChannel extends PaymentChannel {
@Override
public String name() {
return "支付宝";
}
@Override
public long feeInCents(long amountInCents) {
return Math.round(amountInCents * 0.006);
}
}关键点在 actualReceive:它写在父类里,调用的是 feeInCents(...)。由于动态绑定,父类的方法体里调用的重写方法,也会走到子类实现。这种"父类定流程、子类填细节"的写法,就是模板方法模式的雏形。
class Parent {
String label = "parent";
static String who() { return "Parent"; }
}
class Child extends Parent {
String label = "child";
static String who() { return "Child"; }
}
Parent reference = new Child();
System.out.println(reference.label); // parent —— 字段看编译时类型
System.out.println(reference.who()); // Parent —— 静态方法看编译时类型如果这里把 label 改成方法 label(),输出就会变成 child。这也是为什么 Java 编码规范建议字段用 private + getter 访问:方法有多态,字段没有。
文件:labs/polymorphism/src/main/java/cn/kb/learn/poly/PaymentRunner.javaJava 版本:21(javac --release 21) 依赖:无 运行命令:
javac --release 21 -d out PaymentChannel.java AlipayChannel.java WechatChannel.java BankCardChannel.java PaymentRunner.java
java -cp out cn.kb.learn.poly.PaymentRunnerpackage cn.kb.learn.poly;
public class PaymentRunner {
public static void main(String[] args) {
long amount = 10_000L; // 100.00 元,单位为分
PaymentChannel[] channels = {
new AlipayChannel(),
new WechatChannel(),
new BankCardChannel()
};
for (PaymentChannel channel : channels) {
printSettlement(channel, amount);
}
// 新增渠道不需要改动上面的循环,也不需要新增 if 分支
PaymentChannel newChannel = new PaymentChannel();
printSettlement(newChannel, amount);
}
private static void printSettlement(PaymentChannel channel, long amountInCents) {
System.out.printf(
"%s:手续费 %d 分,实收 %d 分%n",
channel.name(),
channel.feeInCents(amountInCents),
channel.actualReceive(amountInCents)
);
}
}package cn.kb.learn.poly;
public class PaymentChannel {
public String name() {
return "通用渠道";
}
public long feeInCents(long amountInCents) {
return 0L;
}
public long actualReceive(long amountInCents) {
return amountInCents - feeInCents(amountInCents);
}
}package cn.kb.learn.poly;
public class AlipayChannel extends PaymentChannel {
@Override
public String name() {
return "支付宝";
}
@Override
public long feeInCents(long amountInCents) {
return Math.round(amountInCents * 0.006);
}
}package cn.kb.learn.poly;
public class WechatChannel extends PaymentChannel {
@Override
public String name() {
return "微信支付";
}
@Override
public long feeInCents(long amountInCents) {
return Math.round(amountInCents * 0.006);
}
}package cn.kb.learn.poly;
public class BankCardChannel extends PaymentChannel {
private static final long FEE_CAP_IN_CENTS = 2_500L;
@Override
public String name() {
return "银行卡";
}
@Override
public long feeInCents(long amountInCents) {
long fee = Math.round(amountInCents * 0.005);
return Math.min(fee, FEE_CAP_IN_CENTS);
}
}预期输出:
支付宝:手续费 60 分,实收 9940 分
微信支付:手续费 60 分,实收 9940 分
银行卡:手续费 50 分,实收 9950 分
通用渠道:手续费 0 分,实收 10000 分printSettlement(channel, amount); 打断点,展开 channel,看 静态类型是 PaymentChannel,但 实际对象显示为 AlipayChannel@xxx;printSettlement,再 Step Into 到 channel.name() —— 观察编辑器跳到的是 AlipayChannel.name();channel.actualReceive(...),你会发现它先进入父类 PaymentChannel.actualReceive(因为没有重写),再在里面 Step Into 到子类的 feeInCents;BankCardChannel 的 feeInCents 参数改成 10_000_000L,观察手续费被 FEE_CAP_IN_CENTS 限制在 2500 分。第 3 步是整个多态最值得亲手体验一次的调用链。
ClassCastException PaymentChannel channel = new WechatChannel();
AlipayChannel alipay = (AlipayChannel) channel; // 运行时报错修复:转型前用 instanceof 判断,或优先用 instanceof 模式匹配:
if (channel instanceof AlipayChannel alipay) {
// 安全使用
}Parent reference = new Child();
System.out.println(reference.label); // 输出 parent,不是 child修复:不要在父类暴露可变字段;用 getter 方法(方法有多态)替代直接字段访问。
class Parent { static String who() { return "Parent"; } }
class Child extends Parent { static String who() { return "Child"; } }
Parent.who(); // Parent
Child.who(); // Child看起来像多态,其实是两个独立的方法各自绑定到自己的类。@Override 写在静态方法上会直接编译报错——这算是编译器帮你兜住了。
目标:用一个方法消除类型分支。
步骤:
ApplePayChannel,手续费为 0.004、单笔手续费上限 100 分;main 的数组里加上它,除这一行外不改任何代码,确认输出正确;System.out.println(channel.alipayOnly()); —— 观察编译错误,并解释为什么编译不通过。验收方式:能解释第 3 步"为什么新增类型不用改循环",以及第 4 步"为什么编译失败"。
扩展任务:给 PaymentChannel 加一个 supportsRefund() 方法,默认返回 false,让某两个渠道返回 true;在 main 里统计支持退款的渠道数量。
基础题
代码阅读题
class Base {
public String describe() { return "base"; }
public String call() { return "call-" + describe(); }
}
class Sub extends Base {
@Override
public String describe() { return "sub"; }
}
public class Quiz {
public static void main(String[] args) {
Base base = new Sub();
System.out.println(base.call());
}
}编程题
Shape 父类提供 area() 与 name();Circle、Rectangle、Triangle 分别重写。在 main 中用一个 Shape[] 数组遍历打印每种形状的名称与面积,并按面积降序输出。综合任务
Notification 体系(邮件、短信、站内信),父类提供 send(String content)。要求: NotificationService,它的方法签名是 int broadcast(List<Notification> notifications, String content),返回成功发送的数量;instanceof 模式匹配,统计每种具体类型的发送数量。提示 1(方向):第 5 题的面积计算里,圆的面积用 Math.PI * r * r,注意返回类型选 double。
提示 2(关键点):第 6 题的 broadcast 参数声明为 List<Notification>,所以方法内部只看得到父类方法;这正是封装变化的好处。
提示 3(接近答案):排序用 Arrays.sort(shapes, (a, b) -> Double.compare(b.area(), a.area()));instanceof 模式匹配写成 if (notification instanceof EmailNotification email) { ... }。
输出 call-sub。
调用链:base.call() 没有被子类重写,所以进入 Base.call();call() 内部调用的 describe() 是多态方法,按运行时类型 Sub 分发,返回 "sub",最终拼接成 "call-sub"。
这正是第 6.2 节讲的"父类定流程、子类填细节"。
package cn.kb.learn.poly;
public abstract class Shape {
public abstract String name();
public abstract double area();
}package cn.kb.learn.poly;
public class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public String name() {
return "圆形";
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}package cn.kb.learn.poly;
public class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public String name() {
return "矩形";
}
@Override
public double area() {
return width * height;
}
}package cn.kb.learn.poly;
import java.util.Arrays;
public class ShapeRunner {
public static void main(String[] args) {
Shape[] shapes = {new Circle(2), new Rectangle(3, 4), new Circle(1.5)};
Arrays.sort(shapes, (a, b) -> Double.compare(b.area(), a.area()));
for (Shape shape : shapes) {
System.out.printf("%s 面积 %.2f%n", shape.name(), shape.area());
}
}
}:::
package cn.kb.learn.poly;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class NotificationService {
public int broadcast(List<Notification> notifications, String content) {
int success = 0;
Map<String, Integer> counters = new HashMap<>();
for (Notification notification : notifications) {
try {
notification.send(content);
success++;
counters.merge(notification.getClass().getSimpleName(), 1, Integer::sum);
} catch (RuntimeException exception) {
System.out.println("发送失败:" + exception.getMessage());
}
}
for (Map.Entry<String, Integer> entry : counters.entrySet()) {
System.out.println(entry.getKey() + " 成功 " + entry.getValue() + " 条");
}
return success;
}
public static void main(String[] args) {
List<Notification> notifications = new ArrayList<>();
notifications.add(new EmailNotification());
notifications.add(new SmsNotification());
notifications.add(new EmailNotification());
int success = new NotificationService().broadcast(notifications, "今晚发布新版本");
System.out.println("总成功数:" + success);
}
}要点:broadcast 只依赖父类 Notification,新增通知类型时它完全不用改;用 getClass().getSimpleName() 或 instanceof 模式匹配都能统计具体类型,前者更简洁,后者在需要强转时更安全。
if-else 类型分支改写成类的体系;instanceof 模式匹配)。《抽象类与接口》 会把本节用到的 abstract 讲透:既然父类的方法体可能永远用不上,为什么还要写它?抽象类与接口该怎么选?