×

上一篇我们认识了接口的基本语法和它与抽象类的区别。这篇来看看接口在工程中更高级的玩法——那些真正让你写出好代码的设计技法。

一、标记接口(Marker Interface)

有一种特殊的接口,里面没有任何方法。没错,空接口。那它是干嘛用的?

// 这是一个标记接口——没有方法,只有标记意义
public interface Serializable {
    // 空的
}

// 实现了 Serializable 的类,表示它可以被序列化
public class User implements Serializable {
    private String name;
    private int age;
    // getter/setter...
}

标记接口的作用就是给某个类打上一个「标签」,告诉 JVM 或者框架:「这个类具有某种能力」。最常见的例子:

  • java.io.Serializable:标记一个类可以被序列化(对象→字节流)
  • java.lang.Cloneable:标记一个类允许调用 clone() 方法
  • java.util.RandomAccess:标记一个集合支持高效随机访问(比如 ArrayList)

判断一个类有没有实现标记接口,用 instanceof

Object obj = new User("张三", 25);
if (obj instanceof Serializable) {
    // 可以安全地序列化
    System.out.println("这个对象可以序列化");
}

Java 从 5 开始引入了注解(Annotation),标记接口在大多数场景下已经被注解替代了——毕竟 @Serializable 比实现一个空接口更灵活。但 JDK 自带的标记接口广泛散布在标准库中,认识它们还是很有必要的。

二、函数式接口(Functional Interface)

函数式接口是 Java 8 引入的一个重要概念——它是指 只有一个抽象方法 的接口。它是 lambda 表达式的基础。

// @FunctionalInterface 是可选的编译器校验——保证这个接口只有一个抽象方法
@FunctionalInterface
public interface Runnable {
    void run();  // 只有一个抽象方法
}

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);  // 只有一个抽象方法
    // 可以有 default 方法或静态方法,不影响
    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }
}

2.1 匿名内部类 vs lambda

如果你的接口只有一个抽象方法,用 lambda 表达式写起来会极其清爽。来对比一下:

// 笨重的匿名内部类
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
}).start();

// 丝滑的 lambda
new Thread(() -> System.out.println("Hello World")).start();

是不是从三行变成了一行?这还只是 lambda 的基础用法。lambda 函数式编程的全面内容我们会在 Java 新特性系列中深入讲,这里先埋一颗种子:你只要能认出 @FunctionalInterface,说明你理解了接口的一种重要应用形式。

三、接口隔离原则(Interface Segregation Principle)

这是 SOLID 五大设计原则中的 「I」。它告诉我们:不应该强迫一个类实现它用不到的接口方法。

3.1 反面教材

// 一个臃肿的「万能」接口
public interface Worker {
    void work();           // 工作
    void eat();            // 吃饭
    void sleep();          // 睡觉
}

// 人类工人——好吧,三样都得实现
class HumanWorker implements Worker {
    @Override public void work() { System.out.println("人类在搬砖"); }
    @Override public void eat() { System.out.println("人类在吃饭"); }
    @Override public void sleep() { System.out.println("人类在睡觉"); }
}

// 机器人工人——But!机器人不需要吃饭和睡觉!
class RobotWorker implements Worker {
    @Override public void work() { System.out.println("机器人在搬砖"); }
    @Override public void eat() {
        // 机器人不吃饭啊!但接口逼着实现,只能抛个异常
        throw new UnsupportedOperationException("机器人不需要吃饭");
    }
    @Override public void sleep() {
        throw new UnsupportedOperationException("机器人不需要睡觉");
    }
}

这种设计显然有问题——RobotWorker 被迫实现了两个它不需要的方法。这就是接口设计得太「胖」了。

3.2 正解

// 把大接口拆成三个小接口
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

// 人类实现所有三个接口
class HumanWorker implements Workable, Eatable, Sleepable {
    @Override public void work() { System.out.println("人类在搬砖"); }
    @Override public void eat() { System.out.println("人类在吃饭"); }
    @Override public void sleep() { System.out.println("人类在睡觉"); }
}

// 机器人只实现需要的接口
class RobotWorker implements Workable {
    @Override public void work() { System.out.println("机器人在搬砖"); }
}

这就是接口隔离原则的精髓——接口要小、要专一。一个类可以实现多个小接口,但不应该被逼着实现一个胖接口。

四、回调与策略模式

接口在 Java 中的一个经典应用就是回调(Callback)。看一个最常见的例子:

// 1. 定义回调接口
public interface OnClickListener {
    void onClick(View view);
}

// 2. 框架提供一个设置回调的方法
public class Button {
    private OnClickListener listener;

    public void setOnClickListener(OnClickListener listener) {
        this.listener = listener;
    }

    public void click() {
        System.out.println("按钮被点击了");
        if (listener != null) {
            listener.onClick(this);  // 回调——调用实现类的方法
        }
    }
}

// 3. 调用方传入不同的实现
Button btn = new Button();
btn.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        System.out.println("打开登录页面");
    }
});

这个模式的精髓在于:Button 不需要知道「点击后干什么」,它只负责通知「我被点了」。具体干什么,由调用方通过接口来定义。

4.1 策略模式

回调接口的更系统化应用就是策略模式(Strategy Pattern)。假设你要实现一个文件压缩工具:

// 策略接口
public interface CompressionStrategy {
    byte[] compress(byte[] data);
    byte[] decompress(byte[] data);
}

// 具体策略
class ZipCompression implements CompressionStrategy {
    @Override public byte[] compress(byte[] data) {
        System.out.println("使用 ZIP 算法压缩");
        return data; // 实际压缩略
    }
    @Override public byte[] decompress(byte[] data) {
        System.out.println("使用 ZIP 算法解压");
        return data;
    }
}

class GzipCompression implements CompressionStrategy {
    @Override public byte[] compress(byte[] data) {
        System.out.println("使用 GZIP 算法压缩");
        return data;
    }
    @Override public byte[] decompress(byte[] data) {
        System.out.println("使用 GZIP 算法解压");
        return data;
    }
}

// 上下文——接受任意策略
class FileCompressor {
    private CompressionStrategy strategy;

    public FileCompressor(CompressionStrategy strategy) {
        this.strategy = strategy;
    }

    public void compressFile(String filename, byte[] data) {
        System.out.println("压缩文件: " + filename);
        byte[] compressed = strategy.compress(data);
        // 保存压缩文件...
    }
}

// 使用
public class StrategyDemo {
    public static void main(String[] args) {
        FileCompressor compressor = new FileCompressor(new ZipCompression());
        compressor.compressFile("report.txt", "some data".getBytes());

        // 换个策略
        compressor = new FileCompressor(new GzipCompression());
        compressor.compressFile("backup.txt", "backup data".getBytes());
    }
}

策略模式的核心就是:算法可以随时替换,而不需要改调用方的代码。这正是多态和接口力量的集中体现。

五、接口在 JDK 中的经典应用一览

看完这些理论,再看 JDK 里那些你天天用、但可能没注意到的接口:

接口一句话作用你熟悉的使用场景
Comparable可比较Collections.sort(list) 需要元素实现它
Comparator比较器Collections.sort(list, comparator)
Runnable可执行任务创建线程 new Thread(runnable)
Callable有返回值的任务线程池 submit(callable)
Iterable可迭代for (T t : collection) 都能用
Iterator迭代器遍历集合的标准方式
AutoCloseable可自动关闭try-with-resources 语法
Serializable可序列化对象写入/读出文件、网络传输
Cloneable可克隆调用 clone() 的前提

看到没?你每天写的 for (String s : list)try (InputStream in = ...)Collections.sort(),底层都是接口在支撑。Java 标准库本质上就是一个庞大的接口体系。

六、总结

  • 标记接口:空接口,打标签用的,现在多被注解替代但仍广泛存在
  • 函数式接口:只有一个抽象方法的接口,lambda 的前置条件
  • 接口隔离原则:接口要小、要专一,别让实现类被迫实现不需要的方法
  • 回调与策略模式:接口是解耦的利器,框架通知、算法替换都是接口的舞台
  • JDK 中的接口:Comparable、Runnable、Iterable……接口无处不在

到这,接口的两篇也讲完了。结合上一节的抽象类,你现在已经掌握了 Java 面向对象设计中最核心的两把武器——抽象类和接口。下一篇我们进入 ⑥ 内部类与匿名类,看看 Java 可以在类里面再写类这种「套娃」操作到底有什么用。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章

20. 接口基础:语法、特性与抽象类对比

抽象类讲完了。你知道父类可以只声明方法签名,...

读出全部

19. 多态的应用:参数多态化、工厂模式与支付系统实战

上一篇我们拆解了多态的底层机制——动态绑定、...

读出全部

18. 多态的本质:动态绑定、向上转型与向下转型

抽象类学完了,你现在应该知道 「父类只声明、...

读出全部