杰瑞科技汇

抽象类与接口,Java里该用哪个?

这是一个非常经典且重要的面试题,理解它们的区别有助于我们写出更优雅、更符合面向对象设计原则的代码。

抽象类与接口,Java里该用哪个?-图1
(图片来源网络,侵删)

核心思想:一句话概括

  • 抽象类:用于 "是一个" (is-a) 的关系,它代表的是一种继承层次中的抽象概念,是对一类事物的根本抽象,子类通过继承抽象类来获得其核心属性和行为。
  • 接口:用于 "能做" (can-do) 的关系,它定义了一组能力规范,任何类只要实现了这个接口,就承诺具备了这些能力。

详细对比表格

下面通过一个详细的表格来对比它们的主要区别:

特性 抽象类 接口
设计目的 提供一个基类,用于代码复用定义核心行为,强调的是“是什么”,是一种继承关系 定义一种能力或规范,强调的是“能做什么”,是一种实现关系(或称契约)。
继承/实现 使用 extends 关键字,单继承,一个类只能继承一个抽象类。 使用 implements 关键字,多实现,一个类可以实现多个接口。
成员变量 可以是各种修饰符(public, protected, private, static, final),没有默认修饰符,default 隐式为 public static final,即接口中的变量本质上就是公共静态常量
成员方法 可以包含抽象方法(没有方法体)。
也可以包含具体方法(有方法体)。
可以是 public, protected, private
Java 8 之前:只能包含抽象方法(隐式为 public abstract)。
Java 8 引入了 default 方法和 static 方法(可以有方法体)。
Java 9 引入了 private 方法(用于辅助 default 方法)。
没有方法体必须是 public
构造方法 可以有构造方法,虽然不能直接实例化,但构造方法会在子类构造时被调用,用于初始化抽象类的成员变量。 不能有构造方法
代码块 可以有静态代码块 (static { ... }) 和实例代码块 ()。 不能有代码块
访问修饰符 方法可以有 public, protected, private 等多种修饰符。 方法的默认修饰符是 public,不能使用 protectedprivateprivate 方法除外,但仅限于接口内部)。
与普通类的区别 不能被实例化,可以包含抽象方法。 在 Java 8 之前,与抽象类的主要区别就是不能有具体方法和构造方法,Java 8 之后,能力大大增强,但“无状态”和“多实现”的核心思想仍在。

Java 版本的演进(关键点)

理解接口的演变是掌握其区别的关键。

Java 7 及之前

这是最经典的区分时期:

  • 抽象类:像个“半成品”的类,既有抽象方法(需要子类实现),也有具体方法(子类可以直接用)。
  • 接口:像个“纯契约”,只包含 public static final 的常量和 public abstract 的方法,它完全没有实现。

示例:

抽象类与接口,Java里该用哪个?-图2
(图片来源网络,侵删)
// 抽象类
abstract class Animal {
    private String name; // 普通成员变量
    public Animal(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name; // 具体方法
    }
    public abstract void makeSound(); // 抽象方法
}
// 接口
interface Flyable {
    int MAX_ALTITUDE = 10000; // public static final
    void fly(); // public abstract
}

Java 8 的重大更新

为了支持 Lambda 表达式 并解决“接口演化”的问题(向现有接口添加新方法会破坏所有实现类),Java 8 给接口引入了两个新成员:

  • default 方法:可以有方法体的方法,这允许接口提供默认实现,实现类可以选择覆盖或直接使用。
  • static 方法:可以有方法体的静态方法,属于接口本身,通过 接口名.static方法名() 调用。

这使得接口的功能变得非常强大,几乎可以包含抽象类的所有功能(除了构造方法和实例字段)。

示例 (Java 8+):

interface Swimmable {
    void swim(); // 依然是抽象方法
    default void dive() {
        System.out.println("默认潜水方式:缓慢下沉");
    }
    static void checkWaterTemperature() {
        System.out.println("检查水温...");
    }
}

Java 9 的进一步补充

Java 9 引入了 private 方法,主要是为了提高代码的复用性,让 default 方法可以共享代码,同时避免它们被实现类直接调用。

抽象类与接口,Java里该用哪个?-图3
(图片来源网络,侵删)

示例 (Java 9+):

interface Runnable {
    void run();
    default void start() {
        System.out.println("准备开始...");
        doRun(); // 调用私有方法
        System.out.println("结束。");
    }
    private void doRun() {
        // 一些通用的运行逻辑
        System.out.println("正在以默认速度奔跑...");
    }
}

如何选择:何时使用抽象类,何时使用接口?

这是一个设计决策问题,遵循以下原则:

使用抽象类的情况:

  1. “是一个”的关系:当多个类在本质上属于同一个家族,并且共享大量代码状态时。DogCat 都是 Animal,它们都有 name 属性,并且可以共享一个 getName() 方法。
  2. 需要定义非 public 的成员:如果你需要 protectedprivate 的方法或字段,抽象类是唯一的选择。
  3. 需要构造方法:如果需要在对象创建时执行一些初始化逻辑,抽象类可以提供构造方法。
  4. 版本控制:当你发布一个库,不希望破坏现有代码时,向抽象类添加新方法不会影响子类(除非是抽象方法),而向接口添加 default 方法是安全的,但添加抽象方法会破坏所有实现类。

使用接口的情况:

  1. “能做”的关系:当一个类需要具备某种能力,而这种能力可以跨越不同的类层次时。CarDuck 都能 move(),但它们显然不是一个家族。
  2. 实现多重继承:Java 不支持类之间的多重继承,但一个类可以实现多个接口,这是实现多行为组合的关键。
  3. 定义 API 契约:接口非常适合定义一个系统的“公共 API”或“插件”的规范。List, Set, Map 都是接口,不同的实现类(如 ArrayList, LinkedList)都遵循这个契约。
  4. 解耦:依赖接口而不是具体实现,可以极大地降低代码的耦合度,提高系统的灵活性和可测试性。

最佳实践与设计模式

一个现代的、灵活的设计模式是:“面向接口编程,同时使用抽象类作为代码复用的基础”

经典例子:Animal 抽象类 vs. Flyable/Swimmable 接口

// 1. 抽象类:定义核心身份和共享代码
abstract class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
    public abstract void makeSound(); // 每种动物都有叫声,但方式不同
    public void eat() { // 共享的具体行为
        System.out.println(name + " is eating.");
    }
}
// 2. 接口:定义额外的、可选的能力
interface Flyable {
    void fly();
}
interface Swimmable {
    void swim();
}
// 3. 具体实现:组合身份和能力
class Duck extends Animal implements Flyable, Swimmable {
    public Duck(String name) {
        super(name);
    }
    @Override
    public void makeSound() {
        System.out.println(name + " says: Quack!");
    }
    @Override
    public void fly() {
        System.out.println(name + " is flying with wings.");
    }
    @Override
    public void swim() {
        System.out.println(name + " is swimming on the water.");
    }
}
class Penguin extends Animal implements Swimmable {
    public Penguin(String name) {
        super(name);
    }
    @Override
    public void makeSound() {
        System.out.println(name + " says: Honk!");
    }
    @Override
    public void swim() {
        System.out.println(name + " is swimming underwater.");
    }
}
// 鸭子是动物,会飞,会游泳
// 企鹅是动物,不会飞,但会游泳

在这个例子中:

  • Animal 抽象类定义了所有动物共有的核心属性 (name) 和行为 (eat())。
  • FlyableSwimmable 接口定义了可被不同动物共享的“技能”。
  • DuckPenguin 通过继承 Animal 获得了基本身份,并通过实现不同的接口来获得特定的能力。
抽象类 接口
核心思想 is-a (继承) can-do (实现/契约)
关系 单继承 多实现
状态 可以有实例变量 只有静态常量
构造 有构造方法 无构造方法
方法 抽象、具体、privateprotectedpublic 抽象、defaultstaticprivate (默认 public)
选择 强调代码复用核心身份 强调能力定义多重行为组合

希望这个详细的解释能帮助你彻底理解 Java 中抽象类和接口的区别!

分享:
扫描分享到社交APP
上一篇
下一篇