接口、接口实现与面向接口编程 | JavaSE
接口、接口实现与面向接口编程
一、学习目标
学完本章,你应该能够:
- 能够解释接口(Interface)解决的核心问题,以及它与普通类、抽象类的主要区别。
- 能够使用
interface定义接口,并正确声明接口常量和抽象方法。 - 能够使用
implements编写接口实现类,并理解为什么一个类可以同时实现多个接口。 - 能够通过接口引用接收不同实现类对象,理解接口多态。
- 能够理解并实践“面向接口编程”,降低业务代码对具体实现类的依赖。
- 能够判断一个需求应该使用继承、抽象类还是接口进行建模。
二、核心知识
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
并不一定自动让设计变好。
接口真正有价值的时候通常包括:
- 存在多个实现。
- 需要替换具体实现。
- 需要定义模块边界。
- 需要统一一组行为。
- 需要进行接口多态。
- 框架要求通过接口生成代理或进行扩展。
- 希望调用方与具体实现解耦。
所以应该记住:
接口是一种设计工具,而不是项目中所有类都必须拥有的“固定搭档”。
五、实践应用
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 知识问答
- 什么是接口?
interface关键字如何使用? - 接口能否直接创建对象?为什么?
- 普通类如何使用
implements实现接口? - 一个类能否同时实现多个接口?
- 实现类没有实现完全部抽象方法时应该如何处理?
- 接口中的字段默认具有什么修饰含义?
- 接口中的普通抽象方法默认具有什么修饰含义?
- 为什么实现接口的方法通常必须声明为
public? - 接口相对类单继承机制解决了什么能力组合问题?
- 什么叫“面向接口编程”?
- 接口作为方法参数有什么价值?
- 接口作为成员变量如何降低业务代码与具体实现之间的耦合?
- 类可以同时
extends一个父类并implements多个接口吗? - 接口之间使用
extends还是implements? - 抽象类与接口分别更适合表达什么关系?
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();
}
}
回答:
Duck一共实现了几个接口?a的编译时类型是什么?a指向的实际对象是什么类型?a.swim()能否直接调用?为什么?b.fly()能否直接调用?为什么?- 这段代码体现了哪些面向对象特性?
阅读:
interface Config {
int MAX_SIZE = 100;
}
回答:
MAX_SIZE完整的隐式修饰符是什么?- 是否可以通过
Config.MAX_SIZE访问? - 是否可以重新赋值为
200? - 为什么?
7.3 手写代码
- 定义
Flyable、Swimmable两个接口,并让Duck同时实现。 - 设计
USB接口,让Mouse和Keyboard分别实现。 - 设计
Payment接口,至少提供两种支付实现,并使用接口参数统一调用。 - 设计
ClassDataService接口,并提供两套不同的学生成绩统计实现。 - 编写一个类,同时继承一个普通父类并实现两个接口。
- 编写一个接口继承两个父接口,并由普通类实现最终接口。
7.4 Debug
下面代码存在问题:
interface Runner {
void run();
}
class Student implements Runner {
void run() {
System.out.println("running");
}
}
要求:
- 判断错误发生的原因。
- 指出涉及哪一条方法重写规则。
- 修改代码使其通过编译。
下面代码存在问题:
interface A {
void test();
}
class B implements A {
}
要求:
- 解释为什么
B不能作为普通类通过编译。 - 给出两种不同的修改思路。
下面代码存在问题:
interface Flyable {
void fly();
}
class Bird extends Flyable {
}
要求:
- 找出关键字使用错误。
- 解释类与接口之间应该建立什么关系。
- 修复代码。
7.5 综合训练
设计一个简单的订单支付模块。
要求:
- 定义
Payment接口。 - 至少编写两种支付实现。
- 定义
OrderService。 OrderService不允许直接依赖某一个具体支付实现。- 通过构造器向
OrderService提供支付方式。 - 使用相同的
checkout()方法完成不同支付方式的调用。 - 新增第三种支付方式时,尽量不修改已有
OrderService。 - 画出接口与三个实现类之间的关系图。
- 用自己的语言解释该设计为什么比在
OrderService内部直接new AlipayPayment()更容易扩展。
7.6 本章验收
完成本章后,应能够在不查看资料的情况下完成:
- [ ] 口述接口的定义和主要作用。
- [ ] 手写
interface基本语法。 - [ ] 手写
implements实现接口。 - [ ] 解释接口常量隐式的
public static final。 - [ ] 解释接口普通抽象方法隐式的
public abstract。 - [ ] 手写一个类同时实现两个接口。
- [ ] 使用接口类型接收实现类对象。
- [ ] 使用接口作为方法参数完成统一调用。
- [ ] 解释什么是接口多态。
- [ ] 使用“支付系统 / USB / 智能家居”中的一个案例解释面向接口编程。
- [ ] 说明接口与抽象类的主要应用边界。
- [ ] 能够 Debug 常见的
implements、访问权限以及未实现抽象方法错误。
如果这些内容仍然只能“看懂”,而无法闭卷写出代码,则本章还没有达到真正掌握标准。