接口和抽象类
# 09.接口和抽象类
# 目录介绍
# 9.1 抽象类
# 9.1.1 抽象类概念
抽象类是不能被实例化的类,用 abstract 关键字修饰。抽象类可以包含抽象方法(没有方法体的方法),子类必须实现这些抽象方法。
抽象类的设计哲学:抽象类代表一种"不完整"的类定义。它提供了部分实现(共同的属性和方法),把不确定的部分(抽象方法)留给子类去完成。这就是模板方法模式(Template Method Pattern)的基础。
对比 C++:C++ 通过纯虚函数 virtual void func() = 0; 定义抽象类,Java 用 abstract 关键字更加明确。
# 9.1.2 抽象类语法
// 抽象类
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// 抽象方法:没有方法体,子类必须实现
public abstract double area();
public abstract double perimeter();
// 普通方法:抽象类可以有具体实现的方法
public void describe() {
System.out.println("这是一个" + color + "的形状,面积为:" + area());
}
}
// 子类:必须实现所有抽象方法
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
}
# 9.1.3 抽象类特点
- 不能被实例化(
new Shape()编译错误)。 - 可以有构造方法(供子类调用)。
- 可以有成员变量。
- 可以有普通方法和抽象方法。
- 子类必须实现所有抽象方法,除非子类也是抽象类。
抽象类的底层原理:在字节码层面,抽象类使用 ACC_ABSTRACT 标志修饰。抽象方法只有方法签名而没有 Code 属性。JVM 在加载抽象类时不会为其分配虚方法表中的方法入口,而是等到子类实现后才填充。如果通过反射调用 newInstance() 尝试实例化抽象类,JVM 会抛出 InstantiationException。
疑惑:抽象类既然不能实例化,为什么还需要构造方法?
答疑:抽象类的构造方法不是给自己用的,而是给子类调用的。子类在构造时必须先调用父类构造方法(super(...)),完成父类字段的初始化。这正是 Java 对象构造的链式初始化机制。
论证:
abstract class Database {
protected String url;
protected int timeout;
// 抽象类的构造方法:初始化共有字段
public Database(String url, int timeout) {
this.url = url;
this.timeout = timeout;
System.out.println("Database 构造完成");
}
public abstract void connect();
}
class MySQLDatabase extends Database {
private String charset;
public MySQLDatabase(String url, int timeout, String charset) {
super(url, timeout); // 必须调用父类构造方法
this.charset = charset;
System.out.println("MySQLDatabase 构造完成");
}
@Override
public void connect() {
System.out.println("连接 MySQL: " + url);
}
}
结果展示:new MySQLDatabase("jdbc:mysql://localhost", 30, "UTF-8") 的输出顺序是 "Database 构造完成" → "MySQLDatabase 构造完成",证明抽象类的构造方法在子类之前执行。
模板方法模式(Template Method Pattern):
抽象类最经典的应用就是模板方法模式。父类定义算法骨架,子类实现具体步骤:
abstract class DataProcessor {
// 模板方法(final 防止子类重写算法骨架)
public final void process() {
readData();
parseData();
validate();
save();
}
// 各步骤由子类实现
protected abstract void readData();
protected abstract void parseData();
// 钩子方法(hook):有默认实现,子类可选择重写
protected void validate() {
System.out.println("默认校验通过");
}
protected void save() {
System.out.println("保存到默认存储");
}
}
class CSVProcessor extends DataProcessor {
@Override
protected void readData() { System.out.println("读取 CSV 文件"); }
@Override
protected void parseData() { System.out.println("按逗号分割解析"); }
@Override
protected void validate() { System.out.println("CSV 格式校验"); }
}
class JSONProcessor extends DataProcessor {
@Override
protected void readData() { System.out.println("读取 JSON 文件"); }
@Override
protected void parseData() { System.out.println("JSON 反序列化"); }
}
JDK 中模板方法模式的典型应用:
AbstractList.get()、InputStream.read(byte[])(调用抽象的read()单字节方法)。
# 9.1.4 综合案例:数据处理框架
本案例综合运用抽象类的核心知识:抽象方法、构造方法链、模板方法模式、钩子方法。
/**
* 数据处理框架 —— 抽象类综合案例
* 知识点:抽象类、抽象方法、构造方法、模板方法、钩子方法
*/
abstract class DataSource {
protected String name;
protected int recordCount;
// 抽象类的构造方法(供子类 super 调用)
public DataSource(String name) {
this.name = name;
this.recordCount = 0;
System.out.println("[构造] DataSource(" + name + ")");
}
// 抽象方法:由子类决定如何读取和解析
public abstract String[] readData();
public abstract String parseRecord(String raw);
// 钩子方法:有默认实现,子类可选择重写
protected boolean validate(String record) {
return record != null && !record.isEmpty();
}
// 模板方法(final 防止子类打乱流程)
public final void process() {
System.out.println("\n===== 处理 " + name + " =====");
String[] rawData = readData();
System.out.println("读取到 " + rawData.length + " 条原始数据");
int valid = 0, invalid = 0;
for (String raw : rawData) {
String record = parseRecord(raw);
if (validate(record)) {
System.out.println(" ✓ " + record);
valid++;
} else {
System.out.println(" ✗ 无效: " + raw);
invalid++;
}
}
recordCount = valid;
System.out.printf("结果:有效 %d 条,无效 %d 条%n", valid, invalid);
}
// 普通方法
public String getSummary() {
return name + ":共处理 " + recordCount + " 条有效记录";
}
}
// 子类1:CSV数据源
class CSVDataSource extends DataSource {
private String[] csvLines;
public CSVDataSource(String name, String[] csvLines) {
super(name); // 调用抽象类构造方法
this.csvLines = csvLines;
System.out.println("[构造] CSVDataSource 准备就绪");
}
@Override
public String[] readData() {
return csvLines;
}
@Override
public String parseRecord(String raw) {
// CSV 解析:取第一个逗号前后的内容
String[] parts = raw.split(",");
if (parts.length >= 2) {
return parts[0].trim() + " -> " + parts[1].trim();
}
return null;
}
// 重写钩子方法:增加额外校验
@Override
protected boolean validate(String record) {
return super.validate(record) && record.contains("->");
}
}
// 子类2:键值对数据源
class KeyValueDataSource extends DataSource {
private String[] kvLines;
public KeyValueDataSource(String name, String[] kvLines) {
super(name);
this.kvLines = kvLines;
}
@Override
public String[] readData() {
return kvLines;
}
@Override
public String parseRecord(String raw) {
if (raw.contains("=")) {
String[] parts = raw.split("=", 2);
return "[" + parts[0].trim() + "] = " + parts[1].trim();
}
return null;
}
// 使用默认的 validate 钩子方法
}
public class DataProcessDemo {
public static void main(String[] args) {
// 1. 构造方法链演示
String[] csv = {"张三,90", "李四,85", "无效数据", "王五,92"};
DataSource csvSource = new CSVDataSource("学生成绩CSV", csv);
String[] kv = {"host=localhost", "port=3306", "=空键", "db=mydb"};
DataSource kvSource = new KeyValueDataSource("数据库配置", kv);
// 2. 模板方法 + 多态
DataSource[] sources = {csvSource, kvSource};
for (DataSource source : sources) {
source.process(); // 同一个 process(),不同的读取和解析行为
}
// 3. 普通方法调用
System.out.println("\n===== 汇总 =====");
for (DataSource source : sources) {
System.out.println(source.getSummary());
}
// 4. 抽象类不能实例化
// new DataSource("test"); // 编译错误!
}
}
运行结果:
[构造] DataSource(学生成绩CSV)
[构造] CSVDataSource 准备就绪
[构造] DataSource(数据库配置)
===== 处理 学生成绩CSV =====
读取到 4 条原始数据
✓ 张三 -> 90
✓ 李四 -> 85
✗ 无效: 无效数据
✓ 王五 -> 92
结果:有效 3 条,无效 1 条
===== 处理 数据库配置 =====
读取到 4 条原始数据
✓ [host] = localhost
✓ [port] = 3306
✗ 无效: =空键
✓ [db] = mydb
结果:有效 3 条,无效 1 条
===== 汇总 =====
学生成绩CSV:共处理 3 条有效记录
数据库配置:共处理 3 条有效记录
案例知识点映射:
| 知识点 | 案例中的体现 |
|---|---|
| 抽象类不能实例化 | new DataSource("test") 编译错误 |
| 抽象方法 | readData()、parseRecord() 由子类实现 |
| 构造方法链 | CSVDataSource → super(name) → DataSource |
| 模板方法模式 | process() 定义流程,子类填充细节 |
| 钩子方法 | validate() 有默认实现,CSV子类重写增强 |
| 多态 | DataSource[] 数组统一调用 process() |
# 9.1.5 抽象类训练题
训练1:创建一个抽象类 Employee,包含属性 name 和 baseSalary,以及抽象方法 double calculateSalary()。然后创建 FullTimeEmployee(全职,月薪制)和 PartTimeEmployee(兼职,按小时计薪)两个子类。
训练2:以下代码能编译通过吗?为什么?
abstract class A {
abstract void method1();
abstract void method2();
}
abstract class B extends A {
void method1() { System.out.println("B.method1"); }
// 没有实现 method2
}
class C extends B {
void method2() { System.out.println("C.method2"); }
}
思考:抽象类可以没有任何抽象方法吗?如果可以,这样做有什么意义?
# 9.2 接口
# 9.2.1 接口概念
接口(Interface)定义了一组行为规范,类通过实现接口来承诺遵循这些规范。接口是 Java 实现"多继承"的方式。
接口的底层原理:在字节码层面,接口方法的调用使用 invokeinterface 指令(而非普通虚方法的 invokevirtual)。这是因为一个类可以实现多个接口,接口方法在虚方法表中的位置不固定,需要在运行时通过接口方法表(itable) 进行二次查找。JDK 8 引入的 default 方法在字节码中标记为 ACC_PUBLIC,由实现类的虚方法表继承;如果多个接口有同名 default 方法,编译器会要求实现类显式重写以消除歧义——这就是 Java 在语言层面避免"菱形继承"问题的方式。
# 9.2.2 接口语法
// 定义接口
public interface Payable {
// 接口中的变量默认是 public static final(常量)
double TAX_RATE = 0.06;
// 接口中的方法默认是 public abstract
void pay(double amount);
double getBalance();
}
// 实现接口
public class Account implements Payable {
private double balance;
@Override
public void pay(double amount) {
balance -= amount;
System.out.println("支付 " + amount);
}
@Override
public double getBalance() {
return balance;
}
}
# 9.2.3 接口的多实现
一个类可以实现多个接口:
public interface Printable {
void print();
}
public interface Exportable {
void export(String format);
}
// 一个类可以同时实现多个接口
public class Report implements Printable, Exportable {
@Override
public void print() {
System.out.println("打印报表");
}
@Override
public void export(String format) {
System.out.println("导出为 " + format + " 格式");
}
}
# 9.2.4 接口默认方法(JDK8+)
JDK 8 允许接口中定义有具体实现的 default 方法:
public interface Loggable {
// 默认方法:有具体实现,实现类可以选择重写或直接使用
default void log(String message) {
System.out.println("[LOG] " + message);
}
}
public class Account implements Loggable {
// 不需要重写 log 方法,直接继承默认实现
public void deposit(double amount) {
log("存款 " + amount); // 使用默认方法
}
}
# 9.2.5 接口静态方法(JDK8+)
public interface StringUtils {
static boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
}
// 通过接口名调用
boolean result = StringUtils.isEmpty(""); // true
注意:接口静态方法不会被继承,只能通过接口名调用,不能通过实现类调用。这和类的静态方法行为不同。
# 9.2.6 接口私有方法(JDK9+)
JDK 9 允许接口中定义 private 方法,用于在多个 default 方法之间共享代码:
public interface Loggable {
default void logInfo(String msg) {
log("INFO", msg);
}
default void logError(String msg) {
log("ERROR", msg);
}
// 私有方法:提取公共逻辑,不对外暴露
private void log(String level, String msg) {
System.out.printf("[%s] %s: %s%n",
java.time.LocalTime.now(), level, msg);
}
}
疑惑:JDK 8 才给接口加了 default 方法,JDK 9 为什么又加了 private 方法?
答疑:当多个 default 方法有重复逻辑时,无法在接口内部复用。JDK 8 中只能把公共逻辑放到另一个 default 方法中,但这样会暴露给实现类。private 方法解决了这个封装问题。
# 9.2.7 接口的演进历史
| JDK 版本 | 接口新增能力 | 解决的问题 |
|---|---|---|
| JDK 1.0 | 抽象方法 + 常量 | 定义行为契约 |
| JDK 8 | default 方法、static 方法 | 接口演化不破坏兼容性 |
| JDK 9 | private 方法 | default 方法间代码复用 |
| JDK 14 | Record 类可实现接口 | 不可变数据载体 |
# 9.2.8 接口方法调用的底层原理
疑惑:接口方法调用为什么比普通虚方法慢?
答疑:普通虚方法调用使用 invokevirtual 指令,方法在虚方法表(vtable)中的偏移量是固定的,可以直接通过偏移量定位。但接口方法使用 invokeinterface 指令——由于一个类可以实现多个接口,同一个接口方法在不同类的虚方法表中位置不同,需要通过接口方法表(itable) 进行二次查找。
论证:
vtable(虚方法表):偏移量固定
Dog.vtable: [0]=toString [1]=hashCode [2]=makeSound [3]=eat
Cat.vtable: [0]=toString [1]=hashCode [2]=makeSound [3]=eat
→ invokevirtual 直接取 vtable[2] 即 makeSound
itable(接口方法表):需要二次查找
Dog 实现了 Flyable, Swimmable
Cat 只实现了 Swimmable
→ invokeinterface 需要先在 itable 列表中查找 Flyable,再找方法偏移
结果展示:JIT 编译器会通过内联缓存(Inline Cache) 优化这个过程。对于单态调用(monomorphic),JIT 会直接缓存目标方法地址,性能与 invokevirtual 几乎无差别。只有多态调用(megamorphic,>2种类型)时才会退化为完整的 itable 查找。
# 9.2.9 综合案例:插件系统
本案例综合运用接口的核心知识:接口定义、多实现、default 方法、static 方法、private 方法(JDK9+)、接口演进。
import java.util.ArrayList;
import java.util.List;
/**
* 插件系统 —— 接口综合案例
* 知识点:接口、多实现、default方法、static方法、接口常量
*/
// 接口1:定义插件基本能力
interface Plugin {
// 接口常量(public static final)
String VERSION = "1.0";
// 抽象方法:插件必须实现
String getName();
void execute(String input);
// default 方法:提供默认的初始化行为
default void init() {
log("初始化插件: " + getName());
}
// default 方法:提供默认的关闭行为
default void shutdown() {
log("关闭插件: " + getName());
}
// JDK 9 private 方法:多个 default 方法共享的日志逻辑
// 如果使用 JDK 8,可以改为 default 方法
private void log(String msg) {
System.out.printf(" [Plugin %s] %s%n", VERSION, msg);
}
// static 方法:通过接口名调用的工具方法
static Plugin findByName(List<Plugin> plugins, String name) {
for (Plugin p : plugins) {
if (p.getName().equals(name)) return p;
}
return null;
}
}
// 接口2:定义可配置能力
interface Configurable {
void configure(String key, String value);
String getConfig(String key);
default void loadDefaults() {
System.out.println(" 加载默认配置...");
}
}
// 实现类1:文本转换插件(只实现 Plugin)
class UpperCasePlugin implements Plugin {
@Override
public String getName() { return "UpperCase"; }
@Override
public void execute(String input) {
System.out.println(" 转换结果: " + input.toUpperCase());
}
// 使用 default 的 init() 和 shutdown()
}
// 实现类2:加密插件(实现 Plugin + Configurable,多实现)
class CaesarPlugin implements Plugin, Configurable {
private int shift = 3; // 默认移位
@Override
public String getName() { return "Caesar"; }
@Override
public void execute(String input) {
StringBuilder sb = new StringBuilder();
for (char c : input.toCharArray()) {
if (Character.isLetter(c)) {
char base = Character.isUpperCase(c) ? 'A' : 'a';
sb.append((char) ((c - base + shift) % 26 + base));
} else {
sb.append(c);
}
}
System.out.println(" 加密结果(移位" + shift + "): " + sb);
}
// 重写 default 方法
@Override
public void init() {
System.out.println(" [Caesar] 自定义初始化,加载密钥...");
}
@Override
public void configure(String key, String value) {
if ("shift".equals(key)) {
shift = Integer.parseInt(value);
System.out.println(" [Caesar] 设置移位: " + shift);
}
}
@Override
public String getConfig(String key) {
if ("shift".equals(key)) return String.valueOf(shift);
return null;
}
}
// 实现类3:统计插件(实现 Plugin + Configurable)
class WordCountPlugin implements Plugin, Configurable {
private boolean ignoreCase = true;
@Override
public String getName() { return "WordCount"; }
@Override
public void execute(String input) {
String text = ignoreCase ? input.toLowerCase() : input;
String[] words = text.split("\\s+");
System.out.println(" 单词数: " + words.length + " (忽略大小写: " + ignoreCase + ")");
}
@Override
public void configure(String key, String value) {
if ("ignoreCase".equals(key)) {
ignoreCase = Boolean.parseBoolean(value);
}
}
@Override
public String getConfig(String key) {
if ("ignoreCase".equals(key)) return String.valueOf(ignoreCase);
return null;
}
}
public class PluginSystemDemo {
public static void main(String[] args) {
// 创建插件列表
List<Plugin> plugins = new ArrayList<>();
plugins.add(new UpperCasePlugin());
plugins.add(new CaesarPlugin());
plugins.add(new WordCountPlugin());
String testInput = "Hello World Java";
System.out.println("╔═══════════════════════════════╗");
System.out.println("║ 插件系统演示 ║");
System.out.println("╚═══════════════════════════════╝");
// 1. 初始化所有插件(default 方法 vs 重写)
System.out.println("\n--- 初始化插件 ---");
for (Plugin p : plugins) {
p.init(); // UpperCase/WordCount 用 default,Caesar 用重写版
}
// 2. 配置(多实现:Configurable 接口)
System.out.println("\n--- 配置插件 ---");
for (Plugin p : plugins) {
if (p instanceof Configurable) {
Configurable cfg = (Configurable) p;
cfg.loadDefaults(); // Configurable 的 default 方法
}
}
// 具体配置
CaesarPlugin caesar = (CaesarPlugin) Plugin.findByName(plugins, "Caesar");
if (caesar != null) {
caesar.configure("shift", "5");
}
// 3. 执行所有插件(多态)
System.out.println("\n--- 执行插件(输入: \"" + testInput + "\")---");
for (Plugin p : plugins) {
System.out.println("[" + p.getName() + "]");
p.execute(testInput);
}
// 4. 接口静态方法
System.out.println("\n--- 接口静态方法查找 ---");
Plugin found = Plugin.findByName(plugins, "WordCount");
if (found != null) {
System.out.println("找到插件: " + found.getName());
}
// 5. 关闭(default 方法)
System.out.println("\n--- 关闭插件 ---");
for (Plugin p : plugins) {
p.shutdown();
}
}
}
运行结果:
╔═══════════════════════════════╗
║ 插件系统演示 ║
╚═══════════════════════════════╝
--- 初始化插件 ---
[Plugin 1.0] 初始化插件: UpperCase
[Caesar] 自定义初始化,加载密钥...
[Plugin 1.0] 初始化插件: WordCount
--- 配置插件 ---
加载默认配置...
加载默认配置...
[Caesar] 设置移位: 5
--- 执行插件(输入: "Hello World Java")---
[UpperCase]
转换结果: HELLO WORLD JAVA
[Caesar]
加密结果(移位5): Mjqqt Btwqi Ofaf
[WordCount]
单词数: 3 (忽略大小写: true)
--- 接口静态方法查找 ---
找到插件: WordCount
--- 关闭插件 ---
[Plugin 1.0] 关闭插件: UpperCase
[Plugin 1.0] 关闭插件: Caesar
[Plugin 1.0] 关闭插件: WordCount
案例知识点映射:
| 知识点 | 案例中的体现 |
|---|---|
| 接口定义 | Plugin、Configurable 接口 |
| 接口常量 | Plugin.VERSION = "1.0" |
| 抽象方法 | getName()、execute()、configure() |
| 多实现 | CaesarPlugin implements Plugin, Configurable |
| default 方法 | init()、shutdown()、loadDefaults() |
| default 方法重写 | Caesar 的 init() 覆盖默认实现 |
| private 方法(JDK9+) | Plugin.log() 被多个 default 方法共享 |
| static 方法 | Plugin.findByName() 通过接口名调用 |
| 多态 + instanceof | for (Plugin p : plugins) + 类型检查 |
# 9.2.10 接口训练题
训练1:定义一个 Sortable<T> 接口,包含 void sort() 方法。实现一个 SortableList<T extends Comparable<T>> 类,在 sort() 中实现冒泡排序。
训练2:如果一个类同时实现了两个接口,且两个接口有相同签名的 default 方法,写代码演示如何解决冲突:
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
class C implements A, B {
// 如何解决 hello() 的冲突?
}
思考:JDK 8 引入 default 方法的主要动机是什么?(提示:想想 Collection 接口中新增的 stream() 方法。如果没有 default 方法,所有实现 Collection 的类都要改代码。)
# 9.3 抽象类和接口区别
# 9.3.1 核心区别
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 关键字 | abstract class | interface |
| 继承/实现 | 单继承(extends) | 多实现(implements) |
| 构造方法 | 有 | 没有 |
| 成员变量 | 可以有各种成员变量 | 只能有 public static final 常量 |
| 方法 | 可以有抽象和具体方法 | JDK 8 前只能有抽象方法,JDK 8+ 可以有 default 和 static 方法 |
| 设计思想 | "是什么"(is-a 关系) | "能做什么"(has-a 能力) |
# 9.3.2 选择建议
- 如果多个类有共同的属性和行为 → 用抽象类(如 Animal 有 name、age)
- 如果只是定义能力/行为规范 → 用接口(如 Flyable、Swimmable)
- 需要多继承效果 → 用接口
# 9.3.3 综合案例:日志框架设计
本案例通过设计一个简单的日志框架,直观展示抽象类和接口的不同使用场景和配合方式。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 日志框架设计 —— 抽象类与接口对比综合案例
* 用接口定义"能力",用抽象类定义"共同属性和模板"
*/
// 接口:定义"可格式化"能力(行为规范)
interface Formattable {
String format(String level, String message);
}
// 接口:定义"可过滤"能力
interface Filterable {
boolean shouldLog(String level);
}
// 抽象类:日志器基类(有共同属性、构造方法、模板方法)
abstract class Logger implements Formattable, Filterable {
protected String name;
protected String minLevel;
private static final String[] LEVELS = {"DEBUG", "INFO", "WARN", "ERROR"};
// 抽象类构造方法
public Logger(String name, String minLevel) {
this.name = name;
this.minLevel = minLevel;
}
// 抽象方法:具体输出由子类决定
protected abstract void output(String formattedMsg);
// 模板方法:定义日志流程
public final void log(String level, String message) {
if (!shouldLog(level)) return; // 接口方法:过滤
String formatted = format(level, message); // 接口方法:格式化
output(formatted); // 抽象方法:输出
}
// Filterable 接口的默认实现
@Override
public boolean shouldLog(String level) {
return levelIndex(level) >= levelIndex(minLevel);
}
private int levelIndex(String level) {
for (int i = 0; i < LEVELS.length; i++) {
if (LEVELS[i].equalsIgnoreCase(level)) return i;
}
return 0;
}
// 快捷方法
public void debug(String msg) { log("DEBUG", msg); }
public void info(String msg) { log("INFO", msg); }
public void warn(String msg) { log("WARN", msg); }
public void error(String msg) { log("ERROR", msg); }
}
// 具体类1:控制台日志器
class ConsoleLogger extends Logger {
public ConsoleLogger(String name, String minLevel) {
super(name, minLevel);
}
@Override
protected void output(String formattedMsg) {
System.out.println(formattedMsg);
}
@Override
public String format(String level, String message) {
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
return String.format("[%s] [%-5s] [%s] %s", time, level, name, message);
}
}
// 具体类2:简洁日志器(不同的格式化方式)
class SimpleLogger extends Logger {
public SimpleLogger(String name, String minLevel) {
super(name, minLevel);
}
@Override
protected void output(String formattedMsg) {
System.out.println(formattedMsg);
}
@Override
public String format(String level, String message) {
return level.charAt(0) + "/" + name + ": " + message;
}
}
public class LogFrameworkDemo {
public static void main(String[] args) {
System.out.println("===== 抽象类 vs 接口的使用对比 =====\n");
// 1. is-a 关系:ConsoleLogger/SimpleLogger "是" Logger
Logger console = new ConsoleLogger("App", "DEBUG");
Logger simple = new SimpleLogger("DB", "WARN");
System.out.println("--- ConsoleLogger (minLevel=DEBUG) ---");
console.debug("调试信息");
console.info("启动成功");
console.warn("内存使用率较高");
console.error("连接超时");
System.out.println("\n--- SimpleLogger (minLevel=WARN) ---");
simple.debug("这条不会打印");
simple.info("这条也不会打印");
simple.warn("慢查询警告");
simple.error("数据库连接失败");
// 2. 接口引用:has-a 能力
System.out.println("\n--- 通过接口引用使用 ---");
Formattable formatter = console;
System.out.println("格式化测试: " + formatter.format("INFO", "测试消息"));
Filterable filter = simple;
System.out.println("DEBUG可记录? " + filter.shouldLog("DEBUG"));
System.out.println("ERROR可记录? " + filter.shouldLog("ERROR"));
// 3. 多态数组
System.out.println("\n--- 多态统一处理 ---");
Logger[] loggers = {console, simple};
for (Logger logger : loggers) {
logger.error("系统异常"); // 相同调用,不同格式输出
}
// 4. 设计对比总结
System.out.println("\n===== 设计选择对比 =====");
System.out.println("抽象类 Logger:");
System.out.println(" ✓ 有共同属性(name, minLevel)");
System.out.println(" ✓ 有构造方法初始化状态");
System.out.println(" ✓ 模板方法定义流程");
System.out.println(" ✓ is-a 关系(ConsoleLogger 是 Logger)");
System.out.println("接口 Formattable/Filterable:");
System.out.println(" ✓ 只定义行为规范,无状态");
System.out.println(" ✓ 可以被任意类实现(多实现)");
System.out.println(" ✓ has-a 能力(Logger 有格式化能力)");
}
}
运行结果:
===== 抽象类 vs 接口的使用对比 =====
--- ConsoleLogger (minLevel=DEBUG) ---
[14:30:00] [DEBUG] [App] 调试信息
[14:30:00] [INFO ] [App] 启动成功
[14:30:00] [WARN ] [App] 内存使用率较高
[14:30:00] [ERROR] [App] 连接超时
--- SimpleLogger (minLevel=WARN) ---
W/DB: 慢查询警告
E/DB: 数据库连接失败
--- 通过接口引用使用 ---
格式化测试: [14:30:00] [INFO ] [App] 测试消息
DEBUG可记录? false
ERROR可记录? true
--- 多态统一处理 ---
[14:30:00] [ERROR] [App] 系统异常
E/DB: 系统异常
===== 设计选择对比 =====
抽象类 Logger:
✓ 有共同属性(name, minLevel)
✓ 有构造方法初始化状态
✓ 模板方法定义流程
✓ is-a 关系(ConsoleLogger 是 Logger)
接口 Formattable/Filterable:
✓ 只定义行为规范,无状态
✓ 可以被任意类实现(多实现)
✓ has-a 能力(Logger 有格式化能力)
案例知识点映射:
| 对比项 | 抽象类(Logger) | 接口(Formattable/Filterable) |
|---|---|---|
| 用途 | 定义共同属性和行为模板 | 定义行为规范(能力) |
| 有状态 | ✓ name, minLevel | ✗ 只有常量 |
| 构造方法 | ✓ 初始化共有字段 | ✗ 没有 |
| 继承方式 | 单继承 extends | 多实现 implements |
| 引用方式 | Logger log = new ConsoleLogger(...) | Formattable f = console |
| 设计思想 | "是什么"(is-a) | "能做什么"(has-a) |
# 9.3.4 抽象类和接口区别训练题
训练1:以下需求应该用抽象类还是接口?说明理由:
- (a)定义"可序列化"的能力
- (b)定义所有"交通工具"的共同属性和行为
- (c)定义"可比较"的能力
- (d)定义所有"银行账户"的共同属性和模板流程
训练2:JDK 8 之后接口可以有 default 方法和 static 方法。请列出接口和抽象类现在还剩的关键区别(至少3条)。
思考:在设计模式中,"面向接口编程"是一个核心原则。为什么 List<String> list = new ArrayList<>() 比 ArrayList<String> list = new ArrayList<>() 更好?