抽象类与接口

上一章我们用 interface PaymentMethod 设计了支付系统,体验了多态的威力。但你可能有个疑问:接口和上一章学的抽象父类有什么区别?什么时候该用接口,什么时候该用抽象类?这一章,我们就来彻底厘清这两个”抽象工具”。

想象一位咖啡大师在编写《咖啡制作手册》。有些内容她写得很具体——“水温 92℃”、“萃取 25 秒”;有些内容她只写了目录,具体配方留给徒弟们去填——“第一章:浓缩咖啡的萃取(请自行完善)“。抽象类就像这本手册,可以既有”具体内容”也有”待填空白”;而接口更像一张”配方清单”,只列出”必须做哪些步骤”,不关心你怎么做。

本章我们将深入抽象类与接口的本质,学习 Java 8 引入的 default 方法、Java 9 引入的私有方法,以及函数式接口——这是 Java 函数式编程的基石。

一、抽象类(abstract class)

1.1 为什么需要抽象类

考虑一个 Shape 类,它有 area() 方法。但”形状”本身是抽象的——矩形、圆形、三角形都有面积,但”形状的面积”是什么?没意义。Shapearea() 方法体该写什么?

class Shape {
    public double area() {
        return 0;   // 没意义的占位实现,纯粹是浪费
    }
}

这种”父类没法给出有意义的实现,但子类必须实现”的方法,就是抽象方法。包含抽象方法的类必须声明为抽象类

1.2 抽象类的定义

abstract 关键字修饰:

public abstract class Shape {
    // 抽象方法:只有声明,没有方法体
    public abstract double area();

    public abstract double perimeter();

    // 普通方法:可以有具体实现
    public void describe() {
        System.out.println("面积:" + area() + ",周长:" + perimeter());
    }
}

1.3 抽象类的规则

  • 不能实例化new Shape() 编译错误。抽象类是”不完整的”,必须由子类补全才能用。
  • 可以没有抽象方法:纯当”不可实例化的父类”用。
  • 有抽象方法的类必须声明为 abstract
  • 子类必须实现所有抽象方法,否则子类也得声明为 abstract。
public class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}

public class Rectangle extends Shape {
    private double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() { return width * height; }

    @Override
    public double perimeter() { return 2 * (width + height); }
}

现在可以多态地使用:

Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.describe();   // 面积:78.54,周长:31.42
s2.describe();   // 面积:12.0,周长:14.0

1.4 抽象类的”模板方法”模式

抽象类有个经典用法——模板方法模式(Template Method):父类定义算法骨架,子类填充细节。

public abstract class Beverage {
    // 模板方法:final 防止子类改写流程
    public final void prepare() {
        boilWater();
        brew();          // 抽象方法,子类实现
        pourInCup();
        addCondiments(); // 抽象方法,子类实现
    }

    private void boilWater() { System.out.println("烧水"); }
    private void pourInCup() { System.out.println("倒入杯中"); }

    protected abstract void brew();
    protected abstract void addCondiments();
}

public class Tea extends Beverage {
    @Override
    protected void brew() { System.out.println("泡茶叶"); }
    @Override
    protected void addCondiments() { System.out.println("加柠檬"); }
}

public class Coffee extends Beverage {
    @Override
    protected void brew() { System.out.println("冲咖啡粉"); }
    @Override
    protected void addCondiments() { System.out.println("加糖和奶"); }
}

prepare() 定义了”烧水→冲泡→倒杯→加料”的固定流程,子类只关心”泡什么""加什么”。这就是抽象类的价值——既提供共性实现,又留下扩展点

二、接口(interface)

2.1 接口的定义

接口是比抽象类更”纯粹”的抽象——它只定义契约(方法签名),不关心实现。Java 8 之前,接口中所有方法都是抽象的;Java 8 之后可以有 defaultstatic 方法;Java 9 之后可以有 private 方法。

public interface Flyable {
    void fly();    // 默认 public abstract,无需写修饰符
}

public interface Swimmable {
    void swim();
}

2.2 实现接口

implements 关键字实现接口,必须实现所有抽象方法:

public class Duck implements Flyable, Swimmable {
    @Override
    public void fly() { System.out.println("鸭子飞"); }

    @Override
    public void swim() { System.out.println("鸭子游"); }
}

注意:一个类可以实现多个接口(弥补了 Java 单继承的限制),但只能继承一个类。鸭子既能飞又能游,所以同时实现 FlyableSwimmable

2.3 接口的特性

  • 接口中的方法默认是 public abstract(可省略)。
  • 接口中的字段默认是 public static final(即常量),必须初始化。
  • 接口不能有构造方法(不能 new,但可以有 static 工厂方法)。
  • 接口可以继承多个父接口:interface C extends A, B { ... }
public interface Constants {
    int MAX_SIZE = 100;   // 等价于 public static final int MAX_SIZE = 100
}

⚠️ 不要在接口里放字段——这是过时的设计。现代 Java 接口应该只定义行为,常量请放到专门的类或枚举中。

三、接口的默认方法(default,Java 8+)

3.1 解决接口演进问题

想象你写了一个 CoffeeMaker 接口,被 100 个类实现。某天你想给它加一个新方法 clean()——如果加成抽象方法,这 100 个类全要改!这就是”接口演进”难题。

Java 8 引入默认方法(Default Method):用 default 关键字修饰,提供默认实现,子类可以选择重写或直接使用。

public interface CoffeeMaker {
    void brew();

    // 默认方法:有方法体
    default void clean() {
        System.out.println("执行标准清洁流程");
    }
}
public class SimpleMaker implements CoffeeMaker {
    @Override
    public void brew() { System.out.println("冲咖啡"); }
    // 不重写 clean(),使用默认实现
}

public class PremiumMaker implements CoffeeMaker {
    @Override
    public void brew() { System.out.println("冲精品咖啡"); }

    @Override
    public void clean() { System.out.println("深度清洁"); }   // 重写
}

SimpleMaker 不重写 clean(),调用时执行默认实现;PremiumMaker 重写了,执行自己的版本。默认方法让接口能向后兼容地扩展——这就是 Java 8 能给 Collection 接口加 stream() 等方法而不破坏老代码的原因。

3.2 默认方法的多继承冲突

一个类实现两个接口,如果两个接口有同名默认方法,会怎样?

interface A {
    default void hello() { System.out.println("A"); }
}
interface B {
    default void hello() { System.out.println("B"); }
}

class C implements A, B {
    // 编译错误!必须解决冲突
    @Override
    public void hello() {
        A.super.hello();   // 显式选择 A 的版本
    }
}

解决方式:子类必须重写冲突方法,可以用 接口名.super.方法名() 调用指定接口的版本。

四、接口的静态方法(Java 8+)

接口可以有 static 方法,作为该接口的工具方法:

public interface CoffeeMaker {
    void brew();

    // 静态方法:通过接口名调用
    static CoffeeMaker getDefault() {
        return new SimpleMaker();
    }

    static void printUsage() {
        System.out.println("使用方法:先 brew(),再 clean()");
    }
}

// 调用:
CoffeeMaker.printUsage();
CoffeeMaker maker = CoffeeMaker.getDefault();

静态方法属于接口本身,不会被实现类继承。这与类的静态方法类似。java.util.Comparator 接口就有 Comparator.naturalOrder()Comparator.reverseOrder() 等静态工厂方法。

五、接口的私有方法(Java 9+)

Java 9 起,接口可以有 private 方法(包括私有静态方法),用于在接口内部复用代码,避免默认方法之间的重复:

public interface CoffeeMaker {
    default void brewLatte() {
        prepareEspresso();
        System.out.println("加牛奶");
    }

    default void brewCappuccino() {
        prepareEspresso();
        System.out.println("加奶泡");
    }

    // 私有方法:抽取公共逻辑,不暴露给实现类
    private void prepareEspresso() {
        System.out.println("萃取浓缩咖啡");
    }

    // 私有静态方法:供 static 方法复用
    private static void log(String msg) {
        System.out.println("[LOG] " + msg);
    }
}

私有方法只能在接口内部被默认方法或静态方法调用,实现类看不到也无法使用。

六、抽象类 vs 接口:何时用哪个

这是 OOP 设计的经典问题。核心区别在于”is-a”和”can-do”。

6.1 对比表

特性抽象类接口
关系is-a(是一种)can-do(能做某事)
继承单继承(一个类只能继承一个抽象类)多实现(一个类可实现多个接口)
字段任意类型,可以是实例变量只能是 public static final 常量
方法抽象+具体方法抽象+default+static+private
构造方法
状态有(可维护实例状态)无(纯契约)
设计语义”是什么”——定义本质”能做什么”——定义能力

6.2 设计抉择

  • 用抽象类当:一组类有强 is-a 关系,需要共享状态代码。例如 AbstractListArrayListLinkedList 的父类,它们共享大量列表操作代码。

  • 用接口当:想定义一种能力,跨越不同继承层级。例如 Comparable(可比较)、Iterable(可迭代)、Serializable(可序列化)——任何类都可能具备这些能力,与它们的类继承体系无关。

经典例子:ArrayList 继承 AbstractList(is-a,列表的本质),同时实现 ListCloneableSerializable 等接口(can-do,多种能力)。前者提供代码复用,后者提供能力契约。

6.3 一个常见的误区

“接口没有代码复用能力”——这是 Java 8 之前的认知。有了 default 方法,接口也能提供代码复用。例如 List 接口的 sortreplaceAll 等方法都是 default 实现,所有 List 实现类都能复用。但接口仍不能有状态(实例字段),这是与抽象类的根本区别。

七、函数式接口(@FunctionalInterface)

7.1 定义

函数式接口(Functional Interface)是只有一个抽象方法的接口(default、static 方法不算)。这种接口是 Java 8 Lambda 表达式的基础——Lambda 的类型就是函数式接口。

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);    // 唯一的抽象方法
    // 还可以有多个 default 方法
}

@FunctionalInterface 是一个注解,告诉编译器”这是一个函数式接口”。如果意外添加了第二个抽象方法,编译器立即报错。建议显式加上这个注解,类似 @Override 的作用。

7.2 Java 内置的函数式接口

java.util.function 包提供了大量常用函数式接口,避免重复定义:

接口抽象方法含义
Supplier<T>T get()提供者:无参,返回 T
Consumer<T>void accept(T)消费者:接收 T,无返回
Function<T,R>R apply(T)函数:T → R
Predicate<T>boolean test(T)断言:T → boolean
BiFunction<T,U,R>R apply(T,U)双参函数
UnaryOperator<T>T apply(T)一元运算:T → T

7.3 与 Lambda 配合

函数式接口最大的价值是配合 Lambda 表达式使用:

// 传统写法:匿名内部类
Comparator<String> cmp1 = new Comparator<>() {
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
};

// Lambda 写法:简洁得多
Comparator<String> cmp2 = (a, b) -> a.length() - b.length();

Lambda 的详细语法会在后续章节讲解,这里只需理解:函数式接口是 Lambda 的”类型”,没有函数式接口就没有 Lambda。

八、综合实战

把本章知识串联起来,看一个完整的例子:

Java · 在线运行

观察这段代码:Duck 既是 Animal(is-a),又能 SwimmableFlyable(can-do)。它继承抽象类获得 introduce 模板方法,实现接口获得游泳和飞行能力。Greeter 是函数式接口,可以用 Lambda 简洁地实例化。这就是现代 Java 抽象工具的完整图景。

九、本章小结

概念要点
抽象类abstract 修饰,不能实例化,可有抽象方法和具体方法
接口interface 定义,纯契约,支持多实现
default 方法Java 8+,接口的默认实现,解决接口演进问题
static 方法Java 8+,接口的工具方法
private 方法Java 9+,接口内部代码复用
is-a vs can-do抽象类表”是什么”,接口表”能做什么”
函数式接口只有一个抽象方法的接口,是 Lambda 的类型

结语

抽象类与接口,是 Java 提供的两种抽象工具。它们不是”二选一”的对立关系,而是”协同作战”的伙伴——抽象类提供代码复用和模板骨架,接口定义能力契约和跨层级多态。一个设计良好的系统,往往是抽象类与接口并用:抽象类承载共性,接口表达能力。

下一章,我们将目光转向类的”内部”——内部类。一个类可以定义在另一个类里面,这种看似奇怪的设计,却承载着 Builder 模式、闭包、事件处理等重要用途。它是 Java 语言中最细腻的一笔,也是通往高级 OOP 的必经之路。