接口、接口实现与面向接口编程 | JavaSE

接口、接口实现与面向接口编程

一、学习目标

学完本章,你应该能够:

  1. 能够解释接口(Interface)解决的核心问题,以及它与普通类、抽象类的主要区别。
  2. 能够使用 interface 定义接口,并正确声明接口常量和抽象方法。
  3. 能够使用 implements 编写接口实现类,并理解为什么一个类可以同时实现多个接口。
  4. 能够通过接口引用接收不同实现类对象,理解接口多态。
  5. 能够理解并实践“面向接口编程”,降低业务代码对具体实现类的依赖。
  6. 能够判断一个需求应该使用继承、抽象类还是接口进行建模。

二、核心知识

2.1 什么是接口

接口(Interface)是 Java 中用于描述一组行为规范的类型。

接口重点回答的并不是:

“这个对象究竟是什么?”

而是:

“这个对象能够做什么?”

例如:

  • 鸟会飞;
  • 飞机会飞;
  • 无人机会飞。

它们在现实世界中并不存在统一、合理的具体父类,但是都具有“飞行”这种能力。

因此可以抽象出:

public interface Flyable {
    void fly();
}

任何具有飞行能力的类,都可以实现这个接口。

public class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("鸟通过翅膀飞行");
    }
}
public class Drone implements Flyable {
    @Override
    public void fly() {
        System.out.println("无人机通过旋翼飞行");
    }
}

从设计角度看:

类 / 抽象类
    更强调“是什么”

接口
    更强调“能做什么”

例如:

Dog extends Animal

表达:

Dog 是一种 Animal。

而:

Duck implements Flyable, Swimmable

表达:

Duck 具有飞行能力,同时具有游泳能力。


2.2 接口的定义

Java 使用 interface 关键字定义接口。

基本格式:

public interface 接口名 {

}

例如:

public interface USB {

}

接口也是一种 Java 类型,因此一般也采用大驼峰命名:

Flyable
Swimmable
Payment
Comparable
Runnable

2.3 接口不能直接创建对象

下面的代码是不允许的:

public interface Flyable {
    void fly();
}
Flyable flyable = new Flyable();

接口主要定义规范,并不代表一个具体对象,因此不能像普通类一样直接实例化。

正确的思路是:

定义接口
    ↓
具体类实现接口
    ↓
创建实现类对象
    ↓
使用接口类型引用该对象

例如:

public interface Flyable {
    void fly();
}
public class Bird implements Flyable {

    @Override
    public void fly() {
        System.out.println("鸟正在飞行");
    }
}

使用:

Flyable flyable = new Bird();
flyable.fly();

这里:

Flyable flyable

是接口类型引用。

new Bird()

是真正创建出来的对象。

这本质上也是一种多态

接口类型引用
        ↓
指向实现类对象

2.4 类使用 implements 实现接口

普通类通过:

implements

关键字实现接口。

格式:

public class 实现类 implements 接口 {
}

例如:

public interface Runner {
    void run();
}
public class Student implements Runner {

    @Override
    public void run() {
        System.out.println("学生正在跑步");
    }
}

其中:

Student

被称为 Runner 接口的实现类(Implementation Class)


2.5 实现类必须实现接口中的抽象方法

例如:

public interface AnimalBehavior {

    void eat();

    void sleep();
}

普通实现类:

public class Dog implements AnimalBehavior {

    @Override
    public void eat() {
        System.out.println("狗正在吃东西");
    }

    @Override
    public void sleep() {
        System.out.println("狗正在睡觉");
    }
}

如果普通类只实现其中一个方法:

public class Dog implements AnimalBehavior {

    @Override
    public void eat() {
        System.out.println("狗正在吃东西");
    }
}

则无法通过编译。

原因是:

非抽象实现类必须完成接口要求它实现的所有抽象方法。

如果确实暂时不准备实现全部方法,则这个类自身必须声明为抽象类:

public abstract class Dog implements AnimalBehavior {

    @Override
    public void eat() {
        System.out.println("狗正在吃东西");
    }
}

剩余行为可以继续交给子类实现。


2.6 接口中的常量

接口中声明的字段会隐式具有:

public static final

三个修饰符。

例如:

public interface SystemConfig {

    String SYSTEM_NAME = "星雨笔录";

}

等价于:

public interface SystemConfig {

    public static final String SYSTEM_NAME = "星雨笔录";

}

因此它具有三个重要特点:

public   → 对外公开
static   → 属于接口本身
final    → 初始化后不能重新赋值

通常通过接口名访问:

System.out.println(SystemConfig.SYSTEM_NAME);

不能重新赋值:

SystemConfig.SYSTEM_NAME = "New Name";

这种代码不能通过编译。

不过在现代工程设计中,不建议把接口简单当成“常量仓库”。

如果只是保存一组完全无关的常量,应该根据业务语义考虑:

  • 常量类;
  • 枚举;
  • 配置对象;
  • 对应领域类型。

接口的核心职责仍然应该是定义行为契约


2.7 接口中的抽象方法

接口中的普通抽象方法会隐式具有:

public abstract

修饰符。

因此:

public interface Payment {

    void pay(double amount);

}

等价于:

public interface Payment {

    public abstract void pay(double amount);

}

工程代码通常直接写:

void pay(double amount);

即可。


2.8 为什么实现方法必须写 public

考虑:

public interface Payment {
    void pay(double amount);
}

接口方法实际上是:

public abstract void pay(double amount);

所以实现类必须提供 public 方法:

public class AlipayPayment implements Payment {

    @Override
    public void pay(double amount) {
        System.out.println("支付:" + amount);
    }
}

不能写成:

void pay(double amount) {
}

原因在于:

方法重写不能降低原方法的访问权限。

接口要求的是一个公开行为,实现类不能把它缩小成包访问权限。


2.9 一个类可以实现多个接口

Java 类不支持多继承:

class C extends A, B

这是不允许的。

但是一个类可以实现多个接口:

class C implements A, B, C {
}

例如一只鸭子同时拥有:

  • 游泳能力;
  • 飞行能力。

定义:

public interface Flyable {
    void fly();
}
public interface Swimmable {
    void swim();
}

实现:

public class Duck implements Flyable, Swimmable {

    @Override
    public void fly() {
        System.out.println("鸭子正在飞");
    }

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

这样,一个类就可以组合多种能力。

可以理解为:

Duck
 ├── Flyable
 └── Swimmable

这正是接口非常重要的价值之一。


2.10 类可以同时继承父类并实现接口

Java 中可以:

class 子类 extends 父类 implements 接口1, 接口2 {
}

例如:

public class Animal {

    public void eat() {
        System.out.println("动物正在吃东西");
    }
}
public interface Flyable {

    void fly();

}
public class Bird extends Animal implements Flyable {

    @Override
    public void fly() {
        System.out.println("鸟正在飞");
    }
}

此时:

Bird 是 Animal
Bird 能 Flyable

这两种关系并不矛盾。

因此在 Java 中经常可以看到:

继承
    建立主体类型体系

接口
    增加能力、规则和角色

三、使用方法

3.1 接口开发的基本流程

通常按照下面的步骤设计:

第一步:识别一组公共行为
        ↓
第二步:定义接口
        ↓
第三步:编写不同实现类
        ↓
第四步:业务代码依赖接口
        ↓
第五步:运行时选择具体实现

例如设计一个支付系统。


3.2 定义 Payment 接口

public interface Payment {

    void pay(double amount);

}

接口只规定:

所有支付方式都必须能够执行 pay()

至于究竟:

  • 支付宝如何支付;
  • 微信如何支付;
  • 银行卡如何支付;

接口并不关心。


3.3 编写不同实现类

支付宝:

public class AlipayPayment implements Payment {

    @Override
    public void pay(double amount) {
        System.out.println("支付宝支付:" + amount);
    }
}

微信支付:

public class WeChatPayment implements Payment {

    @Override
    public void pay(double amount) {
        System.out.println("微信支付:" + amount);
    }
}

现在:

Payment
 ├── AlipayPayment
 └── WeChatPayment

它们拥有相同的对外调用规范:

pay(double amount)

3.4 使用接口类型统一调用

可以编写:

public class PaymentDemo {

    public static void checkout(Payment payment, double amount) {
        payment.pay(amount);
    }

    public static void main(String[] args) {
        checkout(new AlipayPayment(), 99.9);
        checkout(new WeChatPayment(), 199.0);
    }
}

checkout() 并不知道自己拿到的是:

AlipayPayment

还是:

WeChatPayment

它只关心:

Payment

这就是接口带来的统一抽象。


3.5 什么是面向接口编程

面向接口编程(Programming to an Interface)的核心思想可以概括成一句话:

业务代码尽量依赖抽象能力,而不是直接依赖某一个具体实现。

假设直接写:

public class OrderService {

    private AlipayPayment payment = new AlipayPayment();

}

此时 OrderService 与:

AlipayPayment

高度绑定。

以后改成微信支付,需要修改 OrderService

更好的设计:

public class OrderService {

    private final Payment payment;

    public OrderService(Payment payment) {
        this.payment = payment;
    }

    public void checkout(double amount) {
        payment.pay(amount);
    }
}

创建对象时决定实现:

Payment payment = new AlipayPayment();

OrderService orderService = new OrderService(payment);

orderService.checkout(299.0);

换实现:

Payment payment = new WeChatPayment();

OrderService orderService = new OrderService(payment);

orderService.checkout(299.0);

OrderService 本身完全不需要修改。

关系变成:

OrderService
     │
     ↓
  Payment
   ↑    ↑
   │    │
Alipay WeChat

而不是:

OrderService
     │
     ↓
AlipayPayment

这就是降低耦合(Coupling)


3.6 接口作为方法参数

非常典型的面向接口设计是:

public static void checkout(Payment payment) {
    payment.pay(100);
}

调用方可以传入任何实现:

checkout(new AlipayPayment());
checkout(new WeChatPayment());

以后增加:

class BankCardPayment implements Payment

原方法仍然不需要改变。

因此接口非常适合充当:

  • 方法参数类型;
  • 成员变量类型;
  • 返回值类型;
  • 集合元素的统一上层类型。

3.7 接口作为成员变量

例如:

public class CheckoutService {

    private final Payment payment;

    public CheckoutService(Payment payment) {
        this.payment = payment;
    }

    public void checkout(double amount) {
        payment.pay(amount);
    }
}

这种设计意味着:

CheckoutService 需要一种“支付能力”,但不关心具体由谁完成。

以后学习 Spring 时,你会看到大量类似思想:

Controller
    ↓
Service 接口
    ↓
ServiceImpl

Service
    ↓
Mapper 接口

Java Web 中大量框架设计,本质上都建立在:

  • 接口;
  • 多态;
  • 面向抽象编程;
  • 依赖注入;

这些基础能力之上。


四、原理与进阶

4.1 接口为什么能够弥补类单继承的不足

Java 中:

class A extends B

一个类只能拥有一个直接父类。

这样可以避免复杂的类多继承问题。

但是现实中的一个对象经常拥有很多不同能力。

例如:

智能手机
 ├── 可以拍照
 ├── 可以联网
 ├── 可以定位
 └── 可以播放媒体

如果所有能力都依靠类继承表示,很快就会得到一个极其复杂的继承树。

接口提供了另一种建模方式:

class Phone implements Camera,
                       Networkable,
                       Locatable,
                       Playable {
}

所以:

继承:复用和建立主体类型体系
接口:组合能力与建立行为规范

4.2 接口本身也是一种引用类型

假设:

Payment payment = new AlipayPayment();

变量:

payment

的编译时类型是:

Payment

实际对象类型是:

AlipayPayment

因此它同样满足多态模型:

父类型 / 接口类型引用
        ↓
指向子类型 / 实现类对象

调用:

payment.pay(100);

最终执行的是实际对象对应的实现。


4.3 接口可以继承接口

接口之间使用:

extends

而不是 implements

例如:

public interface Readable {

    void read();

}
public interface Writable {

    void write();

}

接口甚至可以同时继承多个接口:

public interface ReadWriteable
        extends Readable, Writable {

}

实现:

public class FileService implements ReadWriteable {

    @Override
    public void read() {
        System.out.println("读取数据");
    }

    @Override
    public void write() {
        System.out.println("写入数据");
    }
}

可以形成:

Readable ──┐
           ├── ReadWriteable
Writable ──┘
                 ↑
             FileService

接口继承中的复杂方法冲突,将在下一章继续分析。


4.4 接口不是“万能抽象”

不要为了使用接口而使用接口。

假设一个项目只有:

UserService

一个极其简单、稳定的实现,而且未来并不存在:

  • 多实现;
  • 替换实现;
  • 测试替身;
  • 模块边界;
  • 框架代理;

等需求。

机械地创造:

UserService
UserServiceImpl

并不一定自动让设计变好。

接口真正有价值的时候通常包括:

  1. 存在多个实现。
  2. 需要替换具体实现。
  3. 需要定义模块边界。
  4. 需要统一一组行为。
  5. 需要进行接口多态。
  6. 框架要求通过接口生成代理或进行扩展。
  7. 希望调用方与具体实现解耦。

所以应该记住:

接口是一种设计工具,而不是项目中所有类都必须拥有的“固定搭档”。


五、实践应用

5.1 USB 设备模型

可以设计:

public interface USB {

    void connect();

    void disconnect();
}

鼠标:

public class Mouse implements USB {

    @Override
    public void connect() {
        System.out.println("鼠标已连接");
    }

    @Override
    public void disconnect() {
        System.out.println("鼠标已断开");
    }

    public void click() {
        System.out.println("鼠标点击");
    }
}

键盘:

public class Keyboard implements USB {

    @Override
    public void connect() {
        System.out.println("键盘已连接");
    }

    @Override
    public void disconnect() {
        System.out.println("键盘已断开");
    }

    public void type() {
        System.out.println("键盘输入");
    }
}

电脑只需要依赖:

USB

而不需要针对每一种设备重新设计连接协议。

这就是接口在现实世界中的一个非常直观的类比:

标准统一,具体设备各自实现。


5.2 学生成绩统计策略

例如定义:

public interface ClassDataService {

    void printAllStudentInfo();

    void printAverageScore();
}

可以出现两套实现:

DefaultClassDataService
AdvancedClassDataService

调用端只保留:

ClassDataService service;

需要切换算法时,只切换实现对象。

这比把所有判断逻辑写成:

if (mode == 1) {
    ...
} else if (mode == 2) {
    ...
}

具有更好的扩展性。


5.3 接口与抽象类如何选择

可以先使用下面这个模型判断:

| 需求 | 更倾向 | | -------------------------------- | ------ | | 多个对象拥有共同父类型和共同状态 | 抽象类 | | 多个无关类型拥有同一种能力 | 接口 | | 需要保存公共实例字段 | 抽象类 | | 需要一个类同时拥有多种角色 | 接口 | | 强调“是什么” | 抽象类 | | 强调“能做什么” | 接口 | | 需要统一行为规范 | 接口 | | 需要复用较多对象状态和基础实现 | 抽象类 |

例如:

Dog 是 Animal

适合:

class Dog extends Animal

而:

Bird 可以飞
Plane 可以飞
Drone 可以飞

适合:

implements Flyable

六、常见问题

6.1 接口可以 new 吗?

不能直接:

new Payment();

但是可以:

Payment payment = new AlipayPayment();

前者是在创建接口对象;

后者是在创建实现类对象,并使用接口引用接收。

两者完全不同。


6.2 implements 和 extends 有什么区别?

类继承类:

class B extends A

类实现接口:

class B implements A

接口继承接口:

interface B extends A

不要写成:

interface B implements A

6.3 为什么实现方法前经常写 @Override?

@Override
public void pay(double amount) {
}

@Override 表示:

这个方法应该是在重写父类型已有的方法。

如果方法名、参数等写错,编译器可以及时帮助发现问题。

因此建议保留。


6.4 一个类实现多个接口后,必须实现所有方法吗?

对于普通非抽象类:

必须实现所有需要它实现的抽象方法。

否则类本身必须声明:

abstract

6.5 接口里的字段为什么不能修改?

因为接口字段隐式是:

public static final

即常量。

因此:

interface Config {
    int MAX_SIZE = 100;
}

不能:

Config.MAX_SIZE = 200;

6.6 接口方法为什么必须 public 实现?

接口普通抽象方法隐式是:

public abstract

所以:

interface A {
    void test();
}

实现时:

class B implements A {

    @Override
    public void test() {
    }
}

必须保持 public


6.7 “面向接口编程”等于所有地方都必须写接口吗?

不等于。

面向接口编程真正强调的是:

让高层业务尽量依赖稳定抽象,而不是不必要地绑定具体实现。

是否建立接口,要根据:

  • 是否存在多种实现;
  • 是否需要扩展;
  • 是否存在清晰模块边界;
  • 是否需要框架代理;
  • 是否需要降低耦合;

等实际需求决定。


七、练习与验收

7.1 知识问答

  1. 什么是接口?interface 关键字如何使用?
  2. 接口能否直接创建对象?为什么?
  3. 普通类如何使用 implements 实现接口?
  4. 一个类能否同时实现多个接口?
  5. 实现类没有实现完全部抽象方法时应该如何处理?
  6. 接口中的字段默认具有什么修饰含义?
  7. 接口中的普通抽象方法默认具有什么修饰含义?
  8. 为什么实现接口的方法通常必须声明为 public
  9. 接口相对类单继承机制解决了什么能力组合问题?
  10. 什么叫“面向接口编程”?
  11. 接口作为方法参数有什么价值?
  12. 接口作为成员变量如何降低业务代码与具体实现之间的耦合?
  13. 类可以同时 extends 一个父类并 implements 多个接口吗?
  14. 接口之间使用 extends 还是 implements
  15. 抽象类与接口分别更适合表达什么关系?

7.2 代码阅读

阅读下面代码,不运行程序:

interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

class Duck implements Flyable, Swimmable {

    @Override
    public void fly() {
        System.out.println("fly");
    }

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

public class Test {
    public static void main(String[] args) {
        Flyable a = new Duck();
        Swimmable b = new Duck();

        a.fly();
        b.swim();
    }
}

回答:

  1. Duck 一共实现了几个接口?
  2. a 的编译时类型是什么?
  3. a 指向的实际对象是什么类型?
  4. a.swim() 能否直接调用?为什么?
  5. b.fly() 能否直接调用?为什么?
  6. 这段代码体现了哪些面向对象特性?

阅读:

interface Config {
    int MAX_SIZE = 100;
}

回答:

  1. MAX_SIZE 完整的隐式修饰符是什么?
  2. 是否可以通过 Config.MAX_SIZE 访问?
  3. 是否可以重新赋值为 200
  4. 为什么?

7.3 手写代码

  1. 定义 FlyableSwimmable 两个接口,并让 Duck 同时实现。
  2. 设计 USB 接口,让 MouseKeyboard 分别实现。
  3. 设计 Payment 接口,至少提供两种支付实现,并使用接口参数统一调用。
  4. 设计 ClassDataService 接口,并提供两套不同的学生成绩统计实现。
  5. 编写一个类,同时继承一个普通父类并实现两个接口。
  6. 编写一个接口继承两个父接口,并由普通类实现最终接口。

7.4 Debug

下面代码存在问题:

interface Runner {
    void run();
}

class Student implements Runner {

    void run() {
        System.out.println("running");
    }
}

要求:

  1. 判断错误发生的原因。
  2. 指出涉及哪一条方法重写规则。
  3. 修改代码使其通过编译。

下面代码存在问题:

interface A {
    void test();
}

class B implements A {
}

要求:

  1. 解释为什么 B 不能作为普通类通过编译。
  2. 给出两种不同的修改思路。

下面代码存在问题:

interface Flyable {
    void fly();
}

class Bird extends Flyable {
}

要求:

  1. 找出关键字使用错误。
  2. 解释类与接口之间应该建立什么关系。
  3. 修复代码。

7.5 综合训练

设计一个简单的订单支付模块。

要求:

  1. 定义 Payment 接口。
  2. 至少编写两种支付实现。
  3. 定义 OrderService
  4. OrderService 不允许直接依赖某一个具体支付实现。
  5. 通过构造器向 OrderService 提供支付方式。
  6. 使用相同的 checkout() 方法完成不同支付方式的调用。
  7. 新增第三种支付方式时,尽量不修改已有 OrderService
  8. 画出接口与三个实现类之间的关系图。
  9. 用自己的语言解释该设计为什么比在 OrderService 内部直接 new AlipayPayment() 更容易扩展。

7.6 本章验收

完成本章后,应能够在不查看资料的情况下完成:

  • [ ] 口述接口的定义和主要作用。
  • [ ] 手写 interface 基本语法。
  • [ ] 手写 implements 实现接口。
  • [ ] 解释接口常量隐式的 public static final
  • [ ] 解释接口普通抽象方法隐式的 public abstract
  • [ ] 手写一个类同时实现两个接口。
  • [ ] 使用接口类型接收实现类对象。
  • [ ] 使用接口作为方法参数完成统一调用。
  • [ ] 解释什么是接口多态。
  • [ ] 使用“支付系统 / USB / 智能家居”中的一个案例解释面向接口编程。
  • [ ] 说明接口与抽象类的主要应用边界。
  • [ ] 能够 Debug 常见的 implements、访问权限以及未实现抽象方法错误。

如果这些内容仍然只能“看懂”,而无法闭卷写出代码,则本章还没有达到真正掌握标准。