Java学习笔记之抽象类与接口(设计思想)

前言

抽象类(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);
    }
}

对比结论 :当多个方法需要共享数据(如 namelevel)时,抽象类可以自然地用成员变量存储,接口只能被迫把数据作为参数传来传去。

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 访问"接口自己的字段"------因为接口没有实例字段

对比结论 :抽象类中 thissuper 的用法和普通类完全一样。接口中 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();
// 烧开水
// 用沸水浸泡茶叶
// 倒入杯中

接口做不到这件事------因为:

  1. default 方法没有 final 修饰符,实现类可以随意重写模板流程
  2. default 方法之间无法通过共享字段协作(没有实例变量)
  3. 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 中最典型的例子是 SerializableCloneable

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

抽象类的方法可以用 protectedprivate 灵活控制可见性;接口的方法隐式是 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 中随处可见这种结构:ListAbstractListArrayListMapAbstractMapHashMap

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 中的 ListAbstractListArrayList

相关推荐
qcx231 小时前
【系统学AI】09 Multi-Agent架构(2026版):从学术理论到工业级实践
java·人工智能·架构·multi-agent·claude agent
智者知已应修善业2 小时前
【proteus设计文氏正弦波信号发生器】2023-5-9
驱动开发·经验分享·笔记·硬件架构·proteus·硬件工程
土星碎冰机2 小时前
xxljob学习(大白话版本)
学习·运维开发
半旧夜夏2 小时前
【保姆级】微服务组件环境搭建(Docker Compose版)
java·linux·spring cloud·微服务·云原生·容器
吃好睡好便好2 小时前
说说免疫力的维护
学习·生活
云烟成雨TD3 小时前
Spring AI 1.x 系列【33】RAG Advisor 组件与四大分层架构
java·人工智能·spring
凉、介3 小时前
深入理解 ARMv8-A|处理器模式与寄存器
笔记·学习·嵌入式·arm
z200509303 小时前
【linux学习】深入理解linux文件I/O,从C标准库到内核态
linux·学习·操作系统
江南十四行4 小时前
并发编程(七)
java