Java 中接口和抽象类的区别是什么?怎么选择?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 基础概念掌握:面试官不仅仅是想知道你会不会定义接口和抽象类,更是想知道你是否理解它们在设计层面的本质区别—— "契约" 与 "模板" 的不同定位。

  2. 设计能力考察:通过这个问题,面试官想评估你的面向对象设计思维,能否根据实际场景做出合理的抽象选择,而不是滥用继承或接口。

  3. 版本演进意识:JDK 8 之后接口引入了默认方法和静态方法,这让接口和抽象类的界限变得模糊,考察你是否跟上 Java 的演进,以及在新特性下的选择逻辑。

核心答案

一句话总结:接口定义 "能做什么"(行为契约),抽象类定义 "是什么"(本质模板)。

核心区别对比

对比维度接口抽象类
设计理念行为契约,定义 "能做什么"模板模式,定义 "是什么"
继承关系implements,可多实现extends,单继承
成员变量只能是 public static final 常量任意访问修饰符,可定义普通成员变量
方法实现JDK 8 前:只能抽象方法
JDK 8+:可有 defaultstatic 方法
JDK 9+:可有 private 方法
可有抽象方法和具体实现
构造方法❌ 没有✅ 有(供子类调用)
访问修饰符默认 public任意(publicprotected、默认)

选择口诀

  • 多实现用接口,单继承用抽象;
  • 行为规范用接口,代码复选用抽象;
  • 跨层次用接口,同家族用抽象。

深度解析

一、从设计层面理解本质区别

接口和抽象类最大的区别在于 设计意图,用一个生活化的例子来理解:

上图展示了接口和抽象类的核心设计差异:

  • 接口(Flyable:定义的是 "飞行" 这个 能力/行为。飞机、鸟、超人都可以飞行,但它们本质完全不同,没有共同的父类。接口强调的是 "能做什么"。

  • 抽象类(Bird:定义的是 "鸟" 这个 本质/家族。麻雀、老鹰、企鹅都是鸟,它们有共同的特征(羽毛、翅膀、产卵等)和可以复用的代码。抽象类强调的是 "是什么"。

关键点:企鹅虽然不会飞(不实现 Flyable),但它依然是鸟(继承 Bird)。这就是 "本质" 和 "能力" 的区别。

二、JDK 版本演进带来的变化

上图展示了 Java 接口从 JDK 7 到 JDK 9+ 的演进过程:

  • JDK 7 及之前:接口是纯粹的抽象契约,只能包含抽象方法和常量,没有任何实现代码。

  • JDK 8:引入 default 方法和 static 方法,允许接口提供默认实现。这是为了支持 API 演进,比如 Collection 接口新增 forEach()removeIf() 等方法,已有的 ArrayListHashSet 等实现类无需修改就能获得新功能。

  • JDK 9+:进一步引入 private 方法,用于复用 default 方法中的公共代码逻辑,避免代码重复。

面试加分点:JDK 8 之后,接口和抽象类的界限确实变模糊了,但核心区别依然存在——接口不支持成员变量和构造方法,仍然是"轻量级"的行为规范。

三、代码示例对比

// ==================== 接口定义 ====================
public interface Flyable {
    // 常量(自动 public static final)
    int MAX_SPEED = 1000;

    // 抽象方法(自动 public abstract)
    void fly();

    // JDK 8: 默认方法
    default void glide() {
        System.out.println("滑翔中...");
    }

    // JDK 8: 静态方法
    static void checkWeather() {
        System.out.println("检查天气状况");
    }
}

// ==================== 抽象类定义 ====================
public abstract class Animal {
    // 成员变量(可以有任何访问修饰符)
    protected String name;
    private int age;

    // 构造方法
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 具体方法(子类直接继承)
    public void eat() {
        System.out.println(name + " 正在进食");
    }

    // 抽象方法(子类必须实现)
    public abstract void makeSound();

    // 普通方法
    public int getAge() {
        return age;
    }
}

// ==================== 实际使用 ====================
// 飞机:实现 Flyable,继承 Object(默认)
public class Airplane implements Flyable {
    @Override
    public void fly() {
        System.out.println("飞机依靠引擎飞行");
    }
}

// 麻雀:继承 Bird(假设 extends Animal),实现 Flyable
public class Sparrow extends Animal implements Flyable {
    public Sparrow(String name, int age) {
        super(name, age);
    }

    @Override
    public void fly() {
        System.out.println(name + " 扇动翅膀飞行");
    }

    @Override
    public void makeSound() {
        System.out.println("叽叽喳喳");
    }
}

四、选择决策流程

上图展示了接口和抽象类的选择决策流程:

  • 第一步:如果需要定义成员变量或构造方法,只能选择抽象类。这是接口无法提供的核心能力。

  • 第二步:如果需要多实现(一个类同时具备多种能力),必须选择接口。Java 不支持多继承,但支持多实现。

  • 第三步:如果以上条件都不满足,优先选择接口。接口更轻量、更灵活,符合"面向接口编程"的设计原则。

典型应用场景

  • 抽象类:模板方法模式(如 AbstractList)、共享代码和状态(如 AbstractStringBuilder
  • 接口:策略模式(如 Comparator)、回调机制(如 RunnableCallable)、标记接口(如 Serializable

五、经典应用案例对比

// ==================== 案例1: 抽象类 - 模板方法模式 ====================
// JDK 源码:java.util.AbstractList
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    // 提供基础实现,子类只需实现少量方法
    public E get(int index) {
        // 基于 iterator() 实现
    }

    public int indexOf(Object o) {
        // 提供通用实现
    }

    // 子类必须实现的核心方法
    public abstract E get(int index);
}

// ArrayList 只需实现少量方法,继承大量通用逻辑
public class ArrayList<E> extends AbstractList<E> {
    public E get(int index) {
        // 具体实现
    }
}

// ==================== 案例2: 接口 - 行为规范 ====================
// JDK 源码:Comparator 函数式接口
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);

    // default 方法提供组合能力
    default Comparator<T> reversed() {
        return (o1, o2) -> compare(o2, o1);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        return (o1, o2) -> {
            int res = compare(o1, o2);
            return (res != 0) ? res : other.compare(o1, o2);
        };
    }
}

// 使用:灵活组合多种比较策略
Comparator<String> comparator = Comparator
    .comparing(String::length)
    .thenComparing(String::compareTo);

面试高频追问

  1. JDK 8 之后接口可以有默认实现,那接口和抽象类还有什么区别?

    核心区别依然存在:接口不能有成员变量(只能有常量)和构造方法,不支持状态维护;抽象类可以。接口强调"能力",抽象类强调"本质"。

  2. 一个类可以同时继承抽象类和实现接口吗?

    可以!而且这是常见的做法。比如 ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable

  3. 接口可以继承接口吗?抽象类可以实现接口吗?

    接口可以多继承接口(interface A extends B, C);抽象类可以实现接口(abstract class D implements A)。

  4. 什么时候用 default 方法,什么时候用抽象类?

    default 方法用于为已有接口添加新功能(向后兼容);如果需要共享状态或复杂的模板逻辑,仍然应该使用抽象类。

常见面试变体

  • "抽象类可以没有抽象方法吗?"(可以,比如 AbstractStringBuilder
  • "接口可以继承抽象类吗?"(不可以,接口只能继承接口)
  • "Java 为什么不支持多继承?"(菱形继承问题、复杂性、实际需求不大)
  • "什么是函数式接口?和普通接口有什么区别?"(只有一个抽象方法的接口,可用 @FunctionalInterface 注解)

记忆口诀

接口定义 "能做什么"(行为能力),抽象类定义 "是什么"(本质模板);
接口可多实现,抽象类只能单继承;
接口是契约(轻量),抽象类是半成品(重量);
需要状态/构造/成员变量 → 抽象类;
需要多实现/解耦/跨层次 → 接口。

总结

接口和抽象类的核心区别在于 设计意图:接口定义 "能力"(能做什么),抽象类定义 "本质"(是什么)。选择时,优先考虑是否需要成员变量/构造方法(用抽象类),是否需要多实现(用接口)。JDK 8 之后接口可以有默认实现,但无法改变"接口无状态"的本质,遵循"面向接口编程"原则,优先使用接口,必要时才用抽象类