Skip to content

这是我学习 Java 抽象机制时整理的笔记。接口(interface)和抽象类(abstract class)是面试必问的对比题,也是理解"抽象"的关键——先搞清两者各自的定位,再记区别,最后会选。

一、解决什么问题:都是"抽象"

两者都是抽象手段——只描述"是什么/能做什么",不暴露具体实现,让上层代码依赖抽象、不依赖实现(呼应《SOLID 五大设计原则》里的 D 依赖倒置)。

但它们的语义定位完全不同

interfaceabstract class
语义能做什么(can-do)——能力契约是什么(is-a)——类族骨架
典型例子Flyable(能飞)、Runnable(能跑线程)Animal(动物)、Shape(形状)

二、interface:能力契约

纯契约:只声明"能做什么",默认不实现(JDK 8 后可加默认方法)。

java
// 能力:能飞、能说话
interface Flyable {
    void fly();                    // 抽象方法:实现类必须实现
}
interface Speakable {
    void speak();
}

// 一个类可以实现多个接口(Java 唯一的"多继承")
class Bird implements Flyable, Speakable {
    @Override public void fly()   { System.out.println("鸟会飞"); }
    @Override public void speak() { System.out.println("鸟会叫"); }
}

关键点(全从"接口没有实例"这个本质推出——接口是能力契约,不是实物):

① 字段只能是 public static final 常量 接口没有实例,所以不能有"每个对象各不相同的值"(实例变量)——接口里的变量只能是写死的常量

java
interface Shape {
    int MAX_SIDES = 100;   // 编译器自动补全为 public static final int MAX_SIDES = 100
}

类比:接口像合同,数字是写死的条款,不是"每个签合同的人自己的数据"。

② 没有构造器(不能 new、不能有实例状态)new 一个对象要调构造器"初始化对象的数据";接口连数据都没有(只有常量)→ 没必要构造器 → 不能 new

java
new Flyable();   // ❌ 编译错误:接口不能实例化
new Bird();      // ✅ 只能 new 实现类

"实例状态"是什么:说白了,就是 new 出来的每个对象各自带着的那包数据。比如:

java
Dog a = new Dog("旺财");   // a.name = "旺财"
Dog b = new Dog("小黑");   // b.name = "小黑"

name 这个值,a 和 b 各存各的(互不影响)——这种"每个对象各存各的值"就叫实例状态

接口没有实例状态:它不能 new(没有对象),所以不存在"每个对象各存各的值",接口里的字段只能是写死的常量(所有人共享同一个值)。对比抽象类有字段、有构造器,能存实例状态(每个子类对象各存各的)。

③ JDK 8+:default 默认方法(加新方法不破坏实现类) 假设已有 100 个类实现了 Speakable,你想加个 speak()——不写 default,100 个实现类全要改(不实现就编译错误)。default 给接口方法配一个"默认实现",实现类可以不重写直接用

java
interface Speakable {
    default void speak() { System.out.println("默认叫声"); }
}
class Bird implements Speakable { }   // 没写 speak() 也能用 → "默认叫声"

意义:接口升级加方法,老实现类零改动——这就是"给已有接口加方法不破坏实现类"(第六节的验证代码里 Bird 没重写 speak() 却输出"默认叫声",就是它生效)。

④ JDK 9+:private 私有方法(接口内部抽公共逻辑) 多个 default 方法有重复逻辑怎么办?抽出来——用 private 方法:

java
interface Service {
    default void doA() { log("A");  commonStep(); }
    default void doB() { log("B");  commonStep(); }
    private void commonStep() { System.out.println("公共步骤"); }   // 内部工具,外部看不到
    private void log(String s)    { System.out.println("记录:" + s); }
}

为什么必须 private:这些只是接口内部实现的辅助工具,不该暴露给实现类——private 保证"接口内部的私有逻辑"。

一句话串起来:接口是"能力契约"不是"实物"→ 所以没数据(只有常量)、不能 new(无构造器);default 是为兼容升级,private 是为内部整洁。

三、abstract class:类族骨架

半成品类:既有抽象方法(子类实现),也有具体方法和字段(子类复用)。

java
// 模板方法模式:骨架定好,子类填细节
abstract class Animal {
    protected String name;         // 有字段(状态)

    Animal(String name) { this.name = name; }   // 有构造器

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

    void showInfo() {              // 具体方法:子类直接复用
        System.out.print(name + ":");
        eat();
    }
}

class Dog extends Animal {
    Dog() { super("狗"); }
    @Override void eat() { System.out.println("吃骨头"); }
}

关键点

  • 单继承(一个类只能继承一个抽象类)
  • 有字段、有构造器、有具体方法——可以存状态、复用实现
  • 可以有没有抽象方法的抽象类(下面展开讲)
  • 经典应用:模板方法模式(骨架方法定义流程,抽象方法留给子类)

为什么会有"没有抽象方法的抽象类"abstract 的真正含义不是"必须要有抽象方法",而是"这个类不能 new,只能被继承"。有没有抽象方法是另一码事:

  • 有抽象方法 → 编译器强制要求类必须是 abstract(方法没实现,new 出来没法调用);
  • 没有抽象方法、但类仍然标 abstract → 唯一目的就是禁止实例化

什么时候需要这种"纯禁止 new"的类?当一个类只是用来做分类、共享字段和方法,它本身是个笼统概念、不该被单独 new 出来。比如:

java
// 没有抽象方法的抽象类:纯"禁止实例化"
abstract class Fruit {
    String color;
    Fruit(String color) { this.color = color; }
    String getColor() { return color; }   // 全是具体方法,子类直接复用
}

class Apple extends Fruit {
    Apple() { super("红色"); }
}
class Banana extends Fruit {
    Banana() { super("黄色"); }
}

现实中没人会"new 一个纯粹的水果"——你说"我要一个水果",最后拿到的不是苹果就是香蕉。Fruit 存在的意义只是给子类当爹(共享 color 字段和 getColor() 方法),所以用 abstract 告诉编译器:你不能 new 我,只能继承我

真实案例:HttpServlet 就是这种类。后端天天写 Servlet,但注意看 JDK 源码——它是 abstract,却没有任何抽象方法

java
// jakarta.servlet.http.HttpServlet(真实源码简化)
public abstract class HttpServlet extends GenericServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        // 默认实现:返回 405 "HTTP method GET is not supported"
    }
    protected void doPost(...) { /* 同样默认 405 */ }
    // doPut/doDelete... 全部都有默认实现,一个 abstract 都没有
}

那它为什么还要标 abstract?因为单独 new 一个 HttpServlet 没有任何意义——一个方法都没重写的 Servlet,收到任何请求都返回 405,什么都干不了。它存在的意义就是强制你继承:写 class MyServlet extends HttpServlet 后按需重写 doGet/doPost

java
class MyServlet extends HttpServlet { }   // ✅ 编译通过:没有抽象方法,一行不写也合法
// HttpServlet s = new HttpServlet();     // ❌ 编译错误:abstract 禁止实例化

这就是"没有抽象方法的抽象类"在真实代码里的样子:类本身禁止 new,但每个方法都给了默认实现,子类按需覆盖(Java AWT 的 MouseAdapter 也是这种"适配器/钩子类")。

四、核心区别(对照表)

维度interfaceabstract class
语义能做什么(能力)是什么(类族)
多继承✅ 一个类实现多个❌ 只能继承一个
字段只能 public static final 常量✅ 实例字段(有状态)
构造器❌ 没有✅ 有(子类 super 调用)
方法抽象 + default/static/private抽象 + 具体方法
实例化❌(但可以被子类 new)
JDK 演变8 默认方法 / 9 私有方法 / 17 sealed一直如此

记忆口诀

  • 能飞能跑 → 接口(Flyable/Runnable)
  • 是动物是形状 → 抽象类(Animal/Shape)
  • 接口管"能力清单",抽象类管"类族骨架 + 模板流程"

五、怎么选(决策流程)

interface vs abstract class 决策流程图

解读

  • 判断 1:只关心"能做什么",且可能被多个不相关的类实现 → interface(能力契约,可多实现)
    • 例:序列化 Serializable、比较 Comparable、回调 Runnable
  • 判断 2:描述"是什么"的类族,子类要共享字段/方法/模板流程 → abstract class(类族骨架,单继承)
    • 例:支付抽象类——子类微信/支付宝共享对账逻辑
  • 判断 3:契约和骨架都想要 → 抽象类实现接口(骨架 + 契约结合)

实战判断:Java 里 List/Map 都是接口(能力契约),AbstractList/AbstractMap 是抽象类(给实现类提供公共骨架)——接口定义 API,抽象类提供默认实现,这就是 JDK 大量"接口 + Abstract 抽象类"成对出现的原因。

六、实跑验证(javac 编译运行)

java
// 验证点:① 接口多实现 ② 抽象类单继承 ③ 模板方法 ④ 默认方法 ⑤ 无抽象方法的抽象类(禁止实例化)
interface Flyable { void fly(); }
interface Speakable { default void speak() { System.out.println("默认叫声"); } }  // JDK8 默认方法

abstract class Animal {
    protected String name;
    Animal(String name) { this.name = name; }
    abstract void eat();
    void show() { System.out.print(name + ":"); eat(); }   // 模板方法
}

class Bird extends Animal implements Flyable, Speakable {   // 单继承 + 多实现
    Bird() { super("鸟"); }
    @Override public void eat() { System.out.println("吃虫"); }
    @Override public void fly() { System.out.println(name + "会飞"); }
    // speak() 用默认实现,不重写
}

// ⑤ 没有抽象方法的抽象类:纯"禁止实例化"(new Fruit 编译报错,只能 new 子类)
abstract class Fruit {
    String color;
    Fruit(String color) { this.color = color; }
    String getColor() { return color; }
}
class Apple extends Fruit { Apple() { super("红色"); } }
class Banana extends Fruit { Banana() { super("黄色"); } }

public class Verify {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.show();        // 鸟:吃虫(模板方法:骨架调抽象方法)
        bird.fly();         // 鸟会飞(接口实现)
        bird.speak();       // 默认叫声(JDK8 默认方法,未重写)
        Flyable f = bird;   // 接口引用指向实现(多态)
        f.fly();

        Apple apple = new Apple();
        Banana banana = new Banana();
        System.out.println("苹果:" + apple.getColor());    // 子类复用父类字段/方法
        System.out.println("香蕉:" + banana.getColor());
        // new Fruit("红色") ❌ 编译错误:Fruit 是抽象的,不能实例化
    }
}

实测输出

鸟:吃虫
鸟会飞
默认叫声
鸟会飞
苹果:红色
香蕉:黄色

小结

  • 接口 = 能力契约(能做什么):多实现、无状态、JDK8 默认方法
  • 抽象类 = 类族骨架(是什么):单继承、有状态、模板方法模式
  • 怎么选:契约/能力 → 接口;共享实现/模板 → 抽象类;List/AbstractList 是 JDK 的经典搭配
  • 面向对象基础见《Java 面向对象:封装、继承、多态》(重载 vs 重写),接口的默认方法、代理等进阶见《Java 动态代理

验证说明:接口多实现、抽象类单继承 + 模板方法、JDK8 默认方法(未重写直接生效)、无抽象方法的抽象类(子类复用字段/方法,new Fruit 编译报错)全部 javac 编译运行实测,输出与注释一致;HttpServlet 案例(jakarta.servlet-api 6.0.0)同样实测——空子类编译通过 + 实例化成功,new HttpServlet() 编译报错 "HttpServlet是抽象的; 无法实例化"。