前言
抽象类(abstract class)和接口(interface)都能定义"没有实现的方法",都能实现多态,初学者经常分不清:它们到底有什么区别?什么时候该用抽象类?什么时候该用接口?
这篇文章不打算简单罗列语法规则,而是从设计定位 出发,逐条对比两者的核心差异,并用完整的代码示例演示每一条差异的实际影响。读完之后,你不仅能背住区别,更能理解为什么会有这些区别。
一、设计定位:is-a vs can-do
抽象类和接口的根本区别,不在于语法,而在于设计定位:
抽象类 ------ "是什么"(is-a) ------ 你是一种动物、一种交通工具
接口 ------ "能做什么"(can-do) ------ 你能飞、能游泳、能序列化
用一个现实世界的例子来感受:
抽象类 Vehicle(交通工具)------ 定义"是什么":所有交通工具都有品牌、都能移动
接口 Drivable(可驾驶) ------ 定义"能做什么":能转向
接口 Chargeable(可充电) ------ 定义"能做什么":能充电
公交车 Bus extends Vehicle implements Drivable ------ 一种交通工具,能驾驶
特斯拉 Tesla extends Vehicle implements Drivable, Chargeable ------ 一种交通工具,能驾驶、能充电
java
// 抽象类:定义"是什么"------所有交通工具的通用骨架
// 有品牌(状态)、有移动能力(抽象方法)、有获取品牌的方法(公共方法)
public abstract class Vehicle {
private String brand;
public Vehicle(String brand) {
this.brand = brand;
}
// 抽象方法:不同交通工具移动方式不同,子类必须实现
public abstract void move();
// 公共方法:所有子类共享
public String getBrand() {
return brand;
}
}
java
// 接口:定义"能做什么"------驾驶能力
// 不关心"是什么交通工具",只关心"能转向"
public interface Drivable {
void steer(String direction);
}
java
// 接口:定义"能做什么"------充电能力
// 公交车不需要充电,特斯拉需要------能力可以按需组合
public interface Chargeable {
void charge();
}
java
// 公交车:一种交通工具,具备驾驶能力
public class Bus extends Vehicle implements Drivable {
public Bus(String brand) {
super(brand); // 调用抽象类的构造方法初始化品牌
}
@Override
public void move() {
System.out.println(getBrand() + " 公交车在路上行驶");
}
@Override
public void steer(String direction) {
System.out.println("方向盘转向:" + direction);
}
}
java
// 特斯拉:一种交通工具,同时具备驾驶和充电能力
public class Tesla extends Vehicle implements Drivable, Chargeable {
public Tesla(String brand) {
super(brand);
}
@Override
public void move() {
System.out.println(getBrand() + " 在电动行驶");
}
@Override
public void steer(String direction) {
System.out.println("电子方向盘转向:" + direction);
}
@Override
public void charge() {
System.out.println(getBrand() + " 正在超级充电...");
}
}
使用:
java
// 抽象类引用------看到的是"是什么"
Vehicle v1 = new Bus("宇通");
Vehicle v2 = new Tesla("Model Y");
v1.move(); // 宇通 公交车在路上行驶
v2.move(); // Model Y 在电动行驶
// 接口引用------看到的是"能做什么"
Drivable d1 = new Bus("宇通");
Drivable d2 = new Tesla("Model Y");
d1.steer("左"); // 方向盘转向:左
d2.steer("右"); // 电子方向盘转向:右
// 只有特斯拉能充电------接口让你按需组合能力
Chargeable c = new Tesla("Model Y");
c.charge(); // Model Y 正在超级充电...
// Bus 实现了 Drivable 但没实现 Chargeable,所以不能充电
// Chargeable c2 = new Bus("宇通"); // 编译错误!Bus 没实现 Chargeable
一句话总结:抽象类定义"你是谁"(is-a),接口定义"你能做什么"(can-do)。一个类只能"是"一种东西(单继承),但可以"能"做很多事(多实现)。
二、语法对比:全维度特性对照
| 对比维度 | 抽象类 | 接口 |
|---|---|---|
| 关键字 | abstract class |
interface |
| 设计定位 | "是什么"(is-a) | "能做什么"(can-do) |
| 能否实例化 | 不能 | 不能 |
| 构造方法 | 能,给子类通过 super() 调用 |
不能 |
| 成员变量 | 任意类型和访问修饰符 | 只有 public static final 常量 |
| 抽象方法 | abstract 方法 |
隐式 public abstract |
| 普通方法 | 有 | Java 8+(default 方法) |
| 静态方法 | 有 | Java 8+ |
| 私有方法 | 有 | Java 9+ |
| 继承/实现 | 单继承(extends 一个类) |
多实现 (implements 多个接口) |
| 接口继承接口 | 不适用 | 多继承 (extends 多个接口) |
| this/super | 能用 | 有限制(接口没有实例) |
| 典型设计模式 | 模板方法模式 | 策略模式、观察者模式、工厂方法模式 |
从这个表可以看出一个规律:抽象类"像类",接口"像契约"。
- 抽象类拥有类的全部能力(构造方法、成员变量、任意方法),只是多了"抽象方法"和"不能实例化"。
- 接口做了大幅减法------去掉构造方法、去掉实例变量,只保留"行为定义"这一核心能力,换来的是"多实现"。
下面的章节逐条展开这些差异,用完整代码演示每一条差异的实际影响。
三、核心差异逐条解析
3.1 状态:抽象类能持有,接口不能
什么是"状态"? 状态就是一个对象持有的数据,也就是成员变量(字段)。比如一个数据库连接
connection,多个方法都要用它,它就是状态------状态是多个方法共享的数据。
这是最根本的区别之一。
抽象类持有状态------多个方法可以共享成员变量:
java
// 抽象类:日志器,有名称和级别
// name 和 level 是"状态"------log() 和 error() 都依赖它们
public abstract class AbstractLogger {
protected String name; // 状态:日志器名称
protected int level; // 状态:日志级别(0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR)
public AbstractLogger(String name, int level) {
this.name = name; // 构造方法初始化状态
this.level = level;
}
// log() 和 error() 都能用 name 和 level------这就是"共享状态"
public void log(String message) {
if (level <= 2) write("[INFO] [" + name + "] " + message);
}
public void error(String message) {
if (level <= 4) write("[ERROR] [" + name + "] " + message);
}
// 抽象方法:子类决定输出方式
protected abstract void write(String message);
}
java
// 控制台日志------输出到屏幕
public class ConsoleLogger extends AbstractLogger {
public ConsoleLogger(String name, int level) {
super(name, level);
}
@Override
protected void write(String message) {
System.out.println(message);
}
}
使用:
java
ConsoleLogger logger = new ConsoleLogger("MyApp", 2); // INFO 级别
logger.log("服务启动"); // [INFO] [MyApp] 服务启动
logger.error("连接超时"); // [ERROR] [MyApp] 连接超时
ConsoleLogger debugLogger = new ConsoleLogger("Debug", 0); // TRACE 级别(全部输出)
debugLogger.log("调试信息"); // [INFO] [Debug] 调试信息
接口不能持有状态------只能有常量,没有实例变量:
java
// 接口:日志器,定义记录和错误方法
public interface Logger {
int MAX_LOG_SIZE = 10000; // 只能有 public static final 常量
void log(String message);
void error(String message);
// 如果用 default 方法尝试做类似的事情:
default void logWithLevel(int level, String name, String message) {
// 每次调用都要把 level 和 name 传进来------因为没有地方存储它们
// 方法一多,每个参数都要传一遍,非常繁琐
if (level <= 2) System.out.println("[INFO] [" + name + "] " + message);
if (level <= 4) System.out.println("[ERROR] [" + name + "] " + message);
}
}
对比结论 :当多个方法需要共享数据(如 name、level)时,抽象类可以自然地用成员变量存储,接口只能被迫把数据作为参数传来传去。
3.2 继承:抽象类单继承,接口多实现
抽象类只能继承一个:
java
// 抽象类:动物,有名字、会叫
public abstract class Animal {
private String name;
public Animal(String name) { this.name = name; }
public abstract void makeSound();
public String getName() { return name; }
}
java
// 抽象类:机器,能启动
public abstract class Machine {
public abstract void start();
}
java
// 编译错误!Java 不允许同时继承两个类
// public class Robot extends Animal, Machine { } // 不行!
接口可以实现多个:
java
// 接口:定义"飞"的能力
public interface Flyable {
void fly();
}
java
// 接口:定义"游泳"的能力
public interface Swimable {
void swim();
}
java
// 接口:定义"跑"的能力
public interface Runnable {
void run();
}
java
// 一个类同时实现三个接口------三种能力兼得
public class Duck implements Flyable, Swimable, Runnable {
private String name;
public Duck(String name) {
this.name = name;
}
@Override
public void fly() {
System.out.println(name + " 扑棱扑棱飞起来了");
}
@Override
public void swim() {
System.out.println(name + " 在水里游");
}
@Override
public void run() {
System.out.println(name + " 摇摇晃晃跑起来");
}
}
使用:
java
// 同一个对象,不同接口类型的引用看到不同的"能力"
Duck duck = new Duck("唐老鸭");
Flyable flyable = duck;
flyable.fly(); // 唐老鸭 扑棱扑棱飞起来了
Swimable swimable = duck;
swimable.swim(); // 唐老鸭 在水里游
Runnable runnable = duck;
runnable.run(); // 唐老鸭 摇摇晃晃跑起来
对比结论:如果需要让一个类同时具备多种能力(如"飞"+"游泳"+"跑"),必须用接口,因为抽象类只允许单继承。
3.3 构造方法:抽象类有,接口没有
抽象类有构造方法------可以在创建子类时执行初始化逻辑(参数校验、资源分配等):
java
// 抽象类:交通工具,有品牌、能移动
public abstract class Vehicle {
private String brand;
public Vehicle(String brand) {
// 可以在构造方法中做参数校验
if (brand == null || brand.isEmpty()) {
throw new IllegalArgumentException("品牌不能为空");
}
this.brand = brand;
}
public abstract void move();
public String getBrand() {
return brand;
}
}
java
// 公交车:一种交通工具
public class Bus extends Vehicle {
public Bus(String brand) {
super(brand); // 调用抽象类的构造方法,触发参数校验
}
@Override
public void move() {
System.out.println(getBrand() + " 公交车在路上行驶");
}
}
使用:
java
Bus bus = new Bus("宇通");
bus.move(); // 宇通 公交车在路上行驶
// Bus invalid = new Bus(""); // 抛出 IllegalArgumentException: 品牌不能为空
接口没有构造方法------接口不参与对象的创建和初始化:
java
// 接口:可驾驶,有转向方法
public interface Drivable {
void steer(String direction);
// 没有构造方法,也放不了参数校验逻辑
}
java
// 实现类的构造完全由自己或父类负责
public class Car implements Drivable {
private String brand;
public Car(String brand) {
// 参数校验只能在这里做,接口帮不上忙
if (brand == null) throw new IllegalArgumentException("品牌不能为空");
this.brand = brand;
}
@Override
public void steer(String direction) {
System.out.println(brand + " 转向:" + direction);
}
}
使用:
java
Car car = new Car("宝马");
car.steer("左"); // 宝马 转向:左
对比结论:如果需要在创建对象时做统一的初始化或校验逻辑,用抽象类的构造方法更合适------所有子类自动继承这套逻辑。
3.4 this 和 super:抽象类自由使用,接口有限制
抽象类中 this 和 super 的用法:
java
// 抽象类:动物,有名字、会叫
public abstract class Animal {
private String name;
public Animal(String name) {
this.name = name; // this 指向当前子类实例
}
public void eat() {
// this 指向实际的子类对象
System.out.println(this.getName() + " is eating");
}
public String getName() {
return name;
}
}
java
// 狗:一种动物,重写了吃东西的方法
public class Dog extends Animal {
public Dog(String name) {
super(name); // super 调用父类构造方法
}
@Override
public void eat() {
super.eat(); // super 调用父类的 eat() 方法
System.out.println(getName() + " 吃的是狗粮");
}
}
使用:
java
Dog dog = new Dog("旺财");
dog.eat();
// 旺财 is eating ← super.eat() 的输出
// 旺财 吃的是狗粮 ← 子类自己的输出
接口中 this 的行为不同:
java
// 接口:可描述,有描述方法
public interface Describable {
String describe(); // 抽象方法
// default 方法中的 this 指向实现类的对象
default void printDescription() {
// this 调用的是实现类的 describe() 方法,不是接口自己的(接口没有实例)
System.out.println("描述:" + this.describe());
}
}
java
// 人类:实现了可描述接口
public class Person implements Describable {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public String describe() {
return "一个人叫" + name;
}
}
使用:
java
Person p = new Person("张三");
p.printDescription(); // 描述:一个人叫张三
// 接口中没有 super------接口不关心继承层次
// 接口也不能通过 this 访问"接口自己的字段"------因为接口没有实例字段
对比结论 :抽象类中 this 和 super 的用法和普通类完全一样。接口中 this 只能在 default 方法里使用,且指向的是实现类的对象,没有 super 的概念。
3.5 模板逻辑:抽象类擅长,接口不擅长
抽象类天然适合模板方法模式 ------用 final 锁住算法骨架,用 abstract 留出可变步骤:
java
// 抽象类:含咖啡因饮料,有冲泡和加调料的方法
public abstract class CaffeineBeverage {
/**
* 模板方法:用 final 修饰,子类不能重写
* 流程固定:烧水 → 冲泡 → 倒杯 → 加调料
*/
public final void prepareRecipe() {
boilWater(); // 1. 烧水(公共步骤)
brew(); // 2. 冲泡(子类实现)
pourInCup(); // 3. 倒杯(公共步骤)
if (customerWantsCondiments()) {
addCondiments(); // 4. 加调料(子类实现,可选)
}
}
// 公共实现:所有子类共享
private void boilWater() {
System.out.println("烧开水");
}
private void pourInCup() {
System.out.println("倒入杯中");
}
// 抽象方法:子类必须实现
protected abstract void brew();
protected abstract void addCondiments();
// 钩子方法:子类可以选择重写,也可以使用默认实现
protected boolean customerWantsCondiments() {
return true;
}
}
java
// 咖啡:冲泡咖啡粉,加糖和牛奶
public class Coffee extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("用沸水冲泡咖啡粉");
}
@Override
protected void addCondiments() {
System.out.println("加糖和牛奶");
}
}
java
// 茶:浸泡茶叶,不加调料
public class Tea extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("用沸水浸泡茶叶");
}
@Override
protected void addCondiments() {
System.out.println("加柠檬");
}
// 重写钩子方法------茶不加调料
@Override
protected boolean customerWantsCondiments() {
return false;
}
}
使用:
java
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();
// 烧开水
// 用沸水冲泡咖啡粉
// 倒入杯中
// 加糖和牛奶
CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
// 烧开水
// 用沸水浸泡茶叶
// 倒入杯中
接口做不到这件事------因为:
default方法没有final修饰符,实现类可以随意重写模板流程default方法之间无法通过共享字段协作(没有实例变量)default方法无法调用private辅助方法来封装公共步骤(Java 9 之前)
java
// 接口:饮料,定义冲泡和加调料的方法
public interface Beverage {
// 没有 final!实现类可以重写 prepareRecipe,破坏模板流程
default void prepareRecipe() {
boilWater(); // 没法设为 private(Java 8)
brew();
pourInCup();
addCondiments();
}
// 只能是 default,不能是 private(Java 9 之前)
default void boilWater() { System.out.println("烧开水"); }
default void pourInCup() { System.out.println("倒入杯中"); }
void brew();
void addCondiments();
}
对比结论 :模板方法模式的核心是"锁住骨架、开放步骤",这需要 final + private + 钩子方法 + 实例变量的配合,只有抽象类能完整实现。
3.6 函数式编程:接口擅长,抽象类不擅长
什么是函数式接口? 函数式接口(Functional Interface)就是只包含一个抽象方法 的接口。Java 8 引入了
@FunctionalInterface注解来标记它,编译器会检查是否真的只有一个抽象方法。只有函数式接口才能用 Lambda 表达式来创建实例。
接口天然适合函数式编程------只要"只有一个抽象方法",就能用 Lambda 表达式:
java
// 函数式接口:字符串处理器,接收一个字符串,返回处理后的字符串
@FunctionalInterface
public interface StringProcessor {
String process(String input);
// 函数式接口只能有一个抽象方法,但可以有 default 方法
default void printResult(String input) {
System.out.println(process(input));
}
}
java
// 使用 Lambda 表达式创建 StringProcessor 的实例
// 不需要写一个类 implements StringProcessor,一行搞定
public class Main {
public static void main(String[] args) {
// 用 Lambda 创建:转大写
StringProcessor toUpper = s -> s.toUpperCase();
toUpper.printResult("hello"); // HELLO
// 用 Lambda 创建:加前缀
StringProcessor addPrefix = s -> "[LOG] " + s;
addPrefix.printResult("服务启动"); // [LOG] 服务启动
// 也可以作为方法参数传递
processAndPrint("world", s -> "你好, " + s);
// 你好, world
}
// 方法参数类型是接口------可以接收任何 Lambda
public static void processAndPrint(String input, StringProcessor processor) {
System.out.println(processor.process(input));
}
}
抽象类不能用于 Lambda------即使只有一个抽象方法,抽象类也不能用 Lambda 表达式创建实例:
java
// 抽象类:字符串转换器,只有一个抽象方法
public abstract class StringTransformer {
public abstract String transform(String input);
public void printResult(String input) {
System.out.println(transform(input));
}
}
java
// 抽象类不能用 Lambda!必须老老实实写子类
public class Main {
public static void main(String[] args) {
// 编译错误!抽象类不能用 Lambda
// StringTransformer t = s -> s.toUpperCase(); // 不行!
// 只能用匿名内部类------比 Lambda 臃肿得多
StringTransformer t = new StringTransformer() {
@Override
public String transform(String input) {
return input.toUpperCase();
}
};
t.printResult("hello"); // HELLO
}
}
对比:
java
// 接口 + Lambda:一行搞定
StringProcessor p1 = s -> s.toUpperCase();
// 抽象类 + 匿名内部类:五、六行才能搞定
StringTransformer t1 = new StringTransformer() {
@Override
public String transform(String input) {
return input.toUpperCase();
}
};
对比结论:函数式编程的核心是"行为即参数"------把一段代码像数据一样传来传去。只有函数式接口能用 Lambda 表达式,抽象类不行。如果你的设计目标是用 Lambda 实现简洁的行为传递,必须用接口。
3.7 标记接口:接口能做到,抽象类做不到
接口可以什么都不定义,纯当"标签"用------JDK 中最典型的例子是 Serializable 和 Cloneable:
java
// 标记接口:可序列化,没有任何方法
// 实现它的类相当于"打了个标签",告诉 JVM:这个类的对象可以被序列化
public interface Serializable {
}
java
// 标记接口:可克隆,没有任何方法
public interface Cloneable {
}
java
// 用户类实现了 Serializable,相当于打上标签
// JVM 通过 instanceof 检查标签来决定行为
public class User implements Serializable {
// 序列化时 JVM 检查:user instanceof Serializable → true → 允许序列化
}
抽象类没有这种用法------抽象类代表"是什么",至少要有属性或方法才有意义,不会是空的。
对比结论 :当你需要给类"打标签",让框架或运行时通过 instanceof 识别,用标记接口。
3.8 访问控制粒度:抽象类更精细,接口只有 public
抽象类的方法可以用 protected、private 灵活控制可见性;接口的方法隐式是 public,实现类必须公开:
java
// 抽象类:订单处理器,有内部校验方法
public abstract class OrderProcessor {
// protected:只有子类能调用,外部看不到
protected abstract void validate();
// private:只有本类内部能调用,子类都看不到
private void log(String msg) {
System.out.println("[LOG] " + msg);
}
public void process() {
log("开始处理");
validate();
log("处理完成");
}
}
java
// 接口:订单处理器,定义校验方法
public interface OrderValidator {
// 隐式 public------实现类必须公开,无法隐藏
void validate();
// 没有 protected / private,无法控制访问范围
}
对比结论 :当你希望某些方法只在继承树内部可见、不对外暴露时,只有抽象类的 protected / private 能做到。接口的所有方法都是公开的,没有隐藏能力。
四、什么时候用抽象类,什么时候用接口
4.1 决策指南
需要定义"是什么" + 多个子类共享属性和行为? → 抽象类
需要定义"能做什么" + 让不同类具备同一能力? → 接口
需要持有状态(成员变量)? → 抽象类
需要多继承? → 接口
需要定义算法骨架(模板方法模式)? → 抽象类
需要解耦调用方和实现方? → 接口
需要 lambda 表达式? → 接口(函数式接口)
4.2 常见误区
误区一:有了 default 方法,接口可以替代抽象类了
不对。default 方法有几个致命限制:
- 不能访问实例变量(接口没有实例变量)
default方法之间无法通过共享字段协作- 没有
final机制,实现类可以随意重写
如果 default 方法之间需要共享数据,说明你需要的是抽象类。
误区二:抽象类比接口"高级"
不对。它们是不同维度的设计工具:
- 抽象类解决的是"纵向"问题------同一继承树上的代码复用和约束
- 接口解决的是"横向"问题------跨继承树的能力定义和解耦
误区三:一个类只能用其中一种
不对。实际开发中最常见的模式是同时使用------接口定义能力,抽象类提供骨架,具体类完成实现。
4.3 反面案例
反面案例一:纯行为定义用了抽象类
java
// 抽象类:飞行物,有飞的方法(但只有抽象方法、没有状态)
public abstract class Flyable {
public abstract void fly();
// 没有成员变量、没有具体方法 → 这个抽象类什么都没"做"
}
java
// 如果有人继承了它:
public class Bird extends Flyable {
@Override
public void fly() { System.out.println("飞"); }
}
java
// 另一个人也想"飞",但他已经继承了别的类:
public class Airplane extends Machine { // 已经继承了 Machine
// 没法再 extends Flyable 了!------单继承限制
}
java
// 接口:飞行物,定义飞的方法
public interface Flyable {
void fly();
}
java
// 鸟:实现了飞行接口
public class Bird implements Flyable {
@Override
public void fly() { System.out.println("鸟在飞"); }
}
java
// 飞机:继承了机器类,同时实现了飞行接口
public class Airplane extends Machine implements Flyable { // 没问题!
@Override
public void fly() { System.out.println("飞机在飞"); }
}
使用:
java
Flyable f1 = new Bird();
Flyable f2 = new Airplane();
f1.fly(); // 鸟在飞
f2.fly(); // 飞机在飞
反面案例二:有状态需求却用了接口
java
// 接口:用户仓库,定义用户操作方法(反例)
public interface UserRepository {
default User findById(Long id) {
// 需要数据库连接------但接口没有实例变量存它
// 只能每次都调 getConnection(),非常别扭
return getConnection().query("SELECT * FROM users WHERE id = " + id);
}
default void save(User user) {
// 也要调 getConnection()------每个方法都要重复这个模式
getConnection().execute("INSERT INTO users ...");
}
default void delete(Long id) {
getConnection().execute("DELETE FROM users WHERE id = " + id);
}
// 被迫加一个抽象方法来获取连接------但这就不是"接口"该干的事了
Connection getConnection();
}
java
// 实现类:
public class MySqlUserRepository implements UserRepository {
@Override
public Connection getConnection() {
// 每个 default 方法都会调这里------创建连接的逻辑散落在实现类中
return DriverManager.getConnection("jdbc:mysql://localhost/mydb");
}
}
java
// 抽象类:用户仓库,有数据库连接和用户操作方法
public abstract class AbstractUserRepository {
protected Connection connection; // 可以持有状态!
public AbstractUserRepository(Connection connection) {
this.connection = connection; // 构造方法统一初始化
}
// 所有方法直接使用 connection,不需要每次获取
public User findById(Long id) {
return connection.query("SELECT * FROM users WHERE id = " + id);
}
public void save(User user) {
connection.execute("INSERT INTO users ...");
}
public void delete(Long id) {
connection.execute("DELETE FROM users WHERE id = " + id);
}
}
java
// 子类只需要提供连接,不需要关心 CRUD 逻辑
public class MySqlUserRepository extends AbstractUserRepository {
public MySqlUserRepository() {
super(DriverManager.getConnection("jdbc:mysql://localhost/mydb"));
}
}
使用:
java
AbstractUserRepository repo = new MySqlUserRepository();
User user = repo.findById(1L); // 直接调用,connection 在抽象类中统一管理
repo.save(new User("张三"));
repo.delete(2L);
对比结论 :当多个方法需要共享数据(如 connection),抽象类用成员变量自然地存储,接口只能被迫加抽象方法让实现类提供------设计非常别扭。
五、配合使用:三层结构
在实际项目中,接口 → 抽象类 → 具体类 三层结构是最常见的组合模式:
接口(做什么) → 抽象类(怎么做的一部分) → 具体类(完成的细节)
JDK 中随处可见这种结构:List → AbstractList → ArrayList,Map → AbstractMap → HashMap。
java
// 第一层:接口------日志器,定义记录和错误方法
public interface Logger {
void log(String message);
void error(String message);
}
java
// 第二层:抽象类------日志器,有名称、级别和输出方法
public abstract class AbstractLogger implements Logger {
protected String name;
protected int level; // 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=FATAL
public AbstractLogger(String name, int level) {
this.name = name;
this.level = level;
}
@Override
public void log(String message) {
// INFO 级别 ≤ 2 才输出
if (level <= 2) write("[INFO] [" + name + "] " + message);
}
@Override
public void error(String message) {
// ERROR 级别 ≤ 4 才输出
if (level <= 4) write("[ERROR] [" + name + "] " + message);
}
// 抽象方法:子类决定具体输出方式
protected abstract void write(String message);
}
java
// 第三层:具体类
// 控制台日志器:输出到屏幕
public class ConsoleLogger extends AbstractLogger {
public ConsoleLogger(String name, int level) {
super(name, level);
}
@Override
protected void write(String message) {
System.out.println(message);
}
}
java
// 文件日志:输出到文件
public class FileLogger extends AbstractLogger {
private String filePath;
public FileLogger(String name, int level, String filePath) {
super(name, level);
this.filePath = filePath;
}
@Override
protected void write(String message) {
System.out.println("写入 " + filePath + ": " + message);
}
}
使用:
java
// 调用方只依赖接口------完全解耦
// 不管底层用的是控制台还是文件,调用方代码一模一样
Logger logger = new ConsoleLogger("APP", 2); // INFO 级别
logger.log("服务启动"); // [INFO] [APP] 服务启动
logger.error("连接超时"); // [ERROR] [APP] 连接超时
// 切换为文件日志------调用方代码零修改
logger = new FileLogger("APP", 2, "/var/log/app.log");
logger.log("服务启动"); // 写入 /var/log/app.log: [INFO] [APP] 服务启动
logger.error("连接超时"); // 写入 /var/log/app.log: [ERROR] [APP] 连接超时
// 级别设为 ERROR(4),INFO 日志不再输出
Logger errorOnly = new ConsoleLogger("APP", 4);
errorOnly.log("这条不输出"); // (没有输出------level=4 > 2,INFO 被过滤)
errorOnly.error("这条会输出"); // [ERROR] [APP] 这条会输出
三层各司其职:
| 层次 | 角色 | 职责 |
|---|---|---|
| 接口 | 契约 | 定义"做什么"------调用方只依赖这一层 |
| 抽象类 | 骨架 | 提供"怎么做的一部分"------复用公共逻辑、持有状态 |
| 具体类 | 实现 | 完成"最后的细节"------只实现差异部分 |
设计思想:接口负责解耦,抽象类负责复用,具体类负责差异化。这种分工让代码既灵活(接口多实现)又高效(抽象类共享代码)。
六、速查清单
| 场景 | 抽象类 | 接口 |
|---|---|---|
| 多个类有相同属性和方法 | 共享 | 不适用 |
| 强制子类实现某方法 | 能 | 能 |
| 定义一种能力/行为 | 不适用 | 是 |
| 需要持有实例变量 | 是 | 否 |
| 需要多继承 | 否 | 是 |
| 需要构造方法做初始化 | 是 | 否 |
| 算法骨架固定、步骤可变(模板方法) | 是 | 否 |
| 策略互相替换、运行时切换(策略模式) | 否 | 是 |
| 跨继承树的共同行为 | 不适用 | 是 |
| lambda 表达式 | 否 | 是(函数式接口) |
| is-a 关系(是什么) | 是 | 否 |
| can-do 关系(能做什么) | 否 | 是 |
七、面试口述:抽象类和接口的区别
抽象类和接口都能定义抽象方法、都不能实例化,但设计定位完全不同:抽象类定义"是什么"(is-a),接口定义"能做什么"(can-do)。
从语法上看,核心区别有三个。第一,抽象类可以有构造方法和成员变量,能持有状态,多个方法可以通过共享字段协作;接口不能有构造方法和实例变量,只能有常量。第二,抽象类只支持单继承,一个类只能继承一个抽象类;接口支持多实现,一个类可以实现多个接口,接口之间也可以多继承。第三,抽象类可以有 final 方法和 private 方法,天然适合模板方法模式------用 final 锁住算法骨架,用 abstract 留出可变步骤;接口的 default 方法无法做到这一点,因为没有 final 机制,也无法通过共享状态协作。
什么时候用哪个?如果多个子类有相同的属性和公共逻辑,需要共享状态或定义算法骨架,用抽象类。如果只是定义一种能力、需要让不同的类具备相同的行为、需要解耦调用方和实现方、或者需要用 Lambda 表达式实现简洁的行为传递,用接口。实际开发中两者经常配合使用:接口定义契约,抽象类提供骨架实现,具体类完成细节------比如 JDK 中的 List → AbstractList → ArrayList。