主题
界面
主题
《多态》 里父类的 feeInCents() 返回 0、name() 返回"通用渠道"——那些默认实现其实没有意义,只是为了占位。
本节解决这个问题:当父类的方法无法给出有意义的实现时,就该把它声明为抽象方法。同时讲清另一个复用机制——接口,以及最常见的一个选型问题:抽象类和接口到底该用哪个。
完成本节后,你应该能够:
问题一:父类的方法写不出实现。
public class Document {
public String render() {
return "文档内容"; // 占位实现,对发票和报告都不对
}
}Document 本身是概念,不是具体文档。给它一个"能跑但没意义"的实现,会隐藏错误:忘了重写的子类不会报错,只是输出错内容。
问题二:一个类只能继承一个父类,但可能需要多种能力。
Invoice 既是"文档"(有标题、能渲染),也是"可打印的"(能预览)。单继承装不下第二种身份——接口解决这个。
public abstract class Document {
private final String title;
protected Document(String title) {
this.title = title;
}
public abstract String render(); // 抽象方法:没有方法体
protected abstract String type();
public String summary() { // 具体方法:可直接复用
return "[" + type() + "] " + title + " → " + render();
}
}三条规则:
new(new Document("x") 编译错误)——它只作为父类存在;abstract。抽象类可以同时拥有:抽象方法(强制子类实现)、具体方法(共享给子类)、字段(共享状态)、构造方法(供子类调用)。
summary() 就是模板方法的雏形:
父类固定:拼接顺序 = "[" + 类型 + "] " + 标题 + " → " + 渲染结果
子类提供:type() 与 render()好处:流程只有一份,改流程只改父类;细节各不相同,各子类自己实现。
《继承与重写》 综合任务里的 payrollLine() 也是同一个模式:格式固定,算法可变。
public interface Printable {
String preview(); // 抽象方法(默认就是 public abstract)
default String printHint() { // 默认方法:可继承、可重写
return "可直接打印";
}
static Printable empty() { // 静态方法:工具方法
return () -> "(空文档)";
}
}接口的三个关键点:
implements);public static final,方法默认是 public abstract——所以常把常量放进接口(但更好的做法是放在专门的常量类里)。public interface Printable {
String preview();
default String printHint() { // 已有实现类不需要改动
return "可直接打印";
}
}Java 8 引入默认方法的真实动机:让 JDK 能给已有接口(如 Collection)加方法(如 stream())而不破坏所有实现类。
对使用者的意义:设计接口时可以先只定义核心方法,后续再加带默认实现的方法——接口可以演进了。
| 标准 | 抽象类 | 接口 |
|---|---|---|
| 是什么 / 能做什么 | "是一个"(发票是一个文档) | "能做什么"(发票能被打印) |
| 能否持有状态 | 能(可以有字段与构造方法) | 不能(字段都是常量) |
| 能继承/实现几个 | 只能一个 | 可以多个 |
选型建议:
一个常见的组合用法
抽象类实现接口,提供通用的默认实现:
public abstract class AbstractDocument implements Printable {
@Override
public String preview() {
return "默认预览:" + render();
}
protected abstract String render();
}这样具体子类只需要实现 render(),preview() 已经免费获得。JDK 里 AbstractList、AbstractMap 都是这个套路。
| 特性 | 说明 | 出现版本 |
|---|---|---|
| 默认方法 | default 修饰,有实现 | Java 8 |
| 静态方法 | 接口内定义工具方法 | Java 8 |
| 私有方法 | 供接口内部复用 | Java 9 |
| 函数式接口 | 只有一个抽象方法,可用 lambda | Java 8 |
Printable 只有一个抽象方法(preview()),所以它是函数式接口,可以用 lambda 实现:
Printable empty = () -> "(空文档)";这就是第 7 节代码里 Printable.empty() 能那样子写的原因。
public abstract class Document {
public abstract String render();
}
class Invoice extends Document {
@Override
public String render() { return "发票内容"; }
}
// 忘记实现 render() 的子类会编译失败 —— 这正是抽象方法的价值public class Invoice extends Document implements Printable {
@Override
public String render() { ... } // 来自抽象类
@Override
protected String type() { return "发票"; }
@Override
public String preview() { ... } // 来自接口
}Invoice 同时拥有两种身份:是一个文档(继承来的)与能被打印(实现来的)。
// 不好:想表达"能打印",却用抽象类占掉了唯一的继承名额
public abstract class Printable {
public abstract String preview();
}
public class Invoice extends Printable { } // 那还能继承 Document 吗?不能了修法:把"能做什么"的能力用接口表达,把"是一个"的层级关系用抽象类表达。
文件:labs/oop-basics/src/main/java/cn/kb/learn/oop/ 下的 Document.java、Printable.java、Invoice.java、Report.java、InterfaceLesson.java
package cn.kb.learn.oop;
/** 抽象类:定义"文档是什么",并持有子类共享的状态 */
public abstract class Document {
private final String title;
protected Document(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("标题不能为空");
}
this.title = title;
}
public final String title() {
return title;
}
public abstract String render();
protected abstract String type();
/** 模板方法:父类定流程,子类只需实现细节 */
public String summary() {
return "[" + type() + "] " + title + " → " + render();
}
}package cn.kb.learn.oop;
/** 接口:定义"能做什么",不关心对方是什么类型 */
public interface Printable {
String preview();
default String printHint() {
return "可直接打印";
}
static Printable empty() {
return () -> "(空文档)";
}
}package cn.kb.learn.oop;
public class Invoice extends Document implements Printable {
private final long amountInCents;
public Invoice(String title, long amountInCents) {
super(title);
this.amountInCents = amountInCents;
}
@Override
public String render() {
return String.format("发票金额 %.2f 元", amountInCents / 100.0);
}
@Override
protected String type() {
return "发票";
}
@Override
public String preview() {
return "发票预览:" + title() + " / " + render();
}
@Override
public String printHint() {
return "需加盖发票专用章后打印";
}
}运行命令:
cd labs
./mvnw -pl oop-basics compile
java -cp oop-basics/target/classes cn.kb.learn.oop.InterfaceLesson预期输出:
[发票] 2026-09 服务费 → 发票金额 1288.00 元
[报告] 2026 年第三季度总结 → 报告共 24 页
发票预览:2026-09 服务费 / 发票金额 1288.00 元 | 需加盖发票专用章后打印
报告预览:2026 年第三季度总结 / 报告共 24 页 | 可直接打印
空文档: (空文档) | 可直接打印
默认提示: 可直接打印第 4、5 行值得对比:Report 没有重写 printHint(),于是用了接口的默认实现;Invoice 重写了它。这就是默认方法的意义——可以不写,也可以覆盖。
document.summary() 处断点,按 F7 进入:summary() 在抽象类中执行,但里面调用的 type() 与 render() 会跳到具体子类——一个方法里两段代码分别落在父类和子类;printable.printHint() 处断点,对 Invoice 与 Report 分别 F7:一个进入 Invoice 的重写版本,一个进入接口的默认实现;document,观察实际类型是 Invoice 而声明类型是 Document;Document 加一个新的抽象方法(如 int pageCount()),不修改子类,观察编译错误信息——抽象方法就是通过这种方式"逼"子类跟上需求变化。abstract 类声明 public class Document {
public abstract String render(); // 编译错误:抽象方法必须在抽象类中
}修复:类上加 abstract。
Document document = new Document("标题"); // 编译错误修复:抽象类只能作为类型使用(Document document = new Invoice(...);)。
public interface Printable {
String name = "打印"; // 实际是 public static final,所有实现共享
}修复:接口不适合存状态,需要状态就用抽象类。
interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }
class C implements A, B { } // 编译错误:冲突修复:在类中显式重写并指定用哪个:
class C implements A, B {
@Override
public String hello() {
return A.super.hello(); // 明确选择 A 的实现
}
}目标:设计一个"抽象类 + 接口"组合的体系。
步骤:
Note extends Document implements Printable:render() 返回"便签内容",type() 返回"便签";InterfaceLesson 的列表里加上它,验证输出(循环体不用改);Printable 加一个新的默认方法 boolean isArchived() 返回 false,观察所有实现类不需要改动就能编译通过;Printable.isArchived() 改成抽象方法(去掉 default),观察编译错误——体会默认方法在接口演进中的价值。验收方式:能解释第 4 步与第 5 步的差别。
扩展任务:设计一个 Comparable 风格的接口 Rankable,包含 int score() 与默认方法 boolean isBetterThan(Rankable other),用两个类实现并排序输出。
基础题
代码阅读题
abstract class Base {
public String describe() {
return "类型=" + type() + ",值=" + value();
}
protected abstract String type();
protected abstract String value();
}
class Impl extends Base {
@Override protected String type() { return "实现类"; }
@Override protected String value() { return "42"; }
}
System.out.println(new Impl().describe());describe() 在哪个类中执行?编程题
PaymentChannel:字段 name,抽象方法 feeInCents(long),具体方法 actualReceive(long)(= 金额 - 手续费);Refundable:方法 refund(long) 与默认方法 boolean supportsRefund() 返回 true;AlipayChannel 实现两者;BankCardChannel 只继承抽象类;综合任务
Exporter:final void export(List<String> rows) 作为模板方法,内部依次调用 open()、writeRow(row)、close();writeRow 为抽象方法;CsvExporter(逗号分隔)与 MarkdownExporter(| 分隔);Compressible:String compress() 与默认方法 boolean useGzip() 返回 false;CsvExporter 实现 Compressible;export() 要是 final?提示 1(方向):第 4 题的 describe() 是抽象类里的具体方法,但里面的抽象方法调用会落到子类。
提示 2(关键点):第 6 题的模板方法把"打开 → 逐行写 → 关闭"的流程固化,子类只提供"怎么写一行"。
提示 3(接近答案):第 6 题的 export() 设为 final 是为了防止子类改变流程(比如忘记 close() 导致资源泄漏)。
输出 类型=实现类,值=42。
describe() 定义在抽象类 Base 中(是具体方法,不需要子类重写),但它内部调用的 type() 与 value() 都是抽象方法,运行时按实际类型(Impl)分发到子类实现。
这就是"模板方法 + 多态"的组合:流程在父类,细节在子类。
package cn.kb.learn.oop;
public abstract class PaymentChannel {
private final String name;
protected PaymentChannel(String name) {
this.name = name;
}
public final String name() {
return name;
}
public abstract long feeInCents(long amountInCents);
public long actualReceive(long amountInCents) {
return amountInCents - feeInCents(amountInCents);
}
}
interface Refundable {
long refund(long amountInCents);
default boolean supportsRefund() {
return true;
}
}
class AlipayChannel extends PaymentChannel implements Refundable {
AlipayChannel() {
super("支付宝");
}
@Override
public long feeInCents(long amountInCents) {
return Math.round(amountInCents * 0.006);
}
@Override
public long refund(long amountInCents) {
return amountInCents; // 全额退款
}
}
class BankCardChannel extends PaymentChannel {
BankCardChannel() {
super("银行卡");
}
@Override
public long feeInCents(long amountInCents) {
return Math.min(Math.round(amountInCents * 0.005), 2_500L);
}
}
class PaymentRunner {
public static void main(String[] args) {
PaymentChannel[] channels = {new AlipayChannel(), new BankCardChannel()};
long amount = 10_000L;
int refundable = 0;
for (PaymentChannel channel : channels) {
System.out.printf("%s 实收 %d 分%n", channel.name(), channel.actualReceive(amount));
if (channel instanceof Refundable refundable1) {
System.out.println(" 支持退款:" + refundable1.supportsRefund());
refundable++;
}
}
System.out.println("支持退款的渠道数:" + refundable);
}
}预期输出:
支付宝 实收 9940 分
支持退款:true
银行卡 实收 9950 分
支持退款的渠道数:1package cn.kb.learn.oop;
import java.util.List;
public abstract class Exporter {
/** 模板方法:流程固定,子类只实现"怎么写一行" */
public final void export(List<String> rows) {
open();
for (String row : rows) {
writeRow(row);
}
close();
}
protected void open() {
System.out.println("-- 开始导出 --");
}
protected abstract void writeRow(String row);
protected void close() {
System.out.println("-- 导出结束 --");
}
}
interface Compressible {
String compress();
default boolean useGzip() {
return false;
}
}
class CsvExporter extends Exporter implements Compressible {
@Override
protected void writeRow(String row) {
System.out.println(String.join(",", row.split("")));
}
@Override
public String compress() {
return "zip";
}
}
class MarkdownExporter extends Exporter {
@Override
protected void writeRow(String row) {
System.out.println("| " + row + " |");
}
}
class ExportRunner {
public static void main(String[] args) {
List<String> rows = List.of("张三 90", "李四 85");
new CsvExporter().export(rows);
new MarkdownExporter().export(rows);
Exporter exporter = new CsvExporter();
if (exporter instanceof Compressible compressible) {
System.out.println("压缩方式:" + compressible.compress() + ",默认 gzip:" + compressible.useGzip());
}
}
}为什么 export() 要 final:它表达了"导出流程不允许被改变"这条设计约束。流程里包含资源管理(open / close 配对),一旦子类重写,就可能忘记 close() 造成资源泄漏。用 final 把"不能改的"钉死,把"可以改的"(writeRow)开放——这就是模板方法的核心权衡。
《枚举》:一个被低估的语法特性——用类型安全的方式表达"有限个取值"。