接口
飞翔科技·设计评审会:架构师白歌在黑板上画了一张图:"我们的薪资系统要支持多种支付方式——银行卡、支付宝、微信。同时,薪资单要支持多种打印格式——PDF 报表、Excel 报表、甚至直接打印到控制台。
Employee已经继承了抽象类,没法再继承PaymentMethod和ReportFormat了。" 后端开发小崔举手:"用接口!一个类可以实现多个接口,完美解决 Java 单继承的局限。"
什么是接口
接口(Interface) 是一种纯抽象契约。它只定义"能做什么"(方法签名),不关心"怎么做"(方法实现)。一个类通过 implements 关键字签署接口契约,承诺提供接口中所有方法的具体实现。
Java 的类只能继承一个父类(单继承),但可以实现多个接口——这是 Java 解决多继承局限的核心机制。
// 接口:定义支付能力
public interface Payable {
void pay(double amount); // 支付
String getPaymentMethod(); // 获取支付方式名
}
// 接口:定义打印能力
public interface Printable {
void print(); // 打印
String getFormat(); // 获取打印格式
}
// 类可以实现多个接口
public class FullTimeEmployee extends Employee implements Payable, Printable {
// 继承 Employee,同时拥有 Payable 和 Printable 两种能力
}
核心语法(JDK 8)
接口的定义与实现
| 语法元素 | 写法 | 说明 |
|---|---|---|
| 接口声明 | public interface InterfaceName { } | interface 关键字声明 |
| 抽象方法(JDK 7-) | void methodName(); | 默认 public abstract,可省略 |
| 实现接口 | class A implements I1, I2 { } | 一个类可实现多个接口 |
| 常量 | int MAX_COUNT = 100; | 默认 public static final |
| default 方法(JDK 8) | default void method() { ... } | 接口中的默认实现,子类可重写 |
| static 方法(JDK 8) | static void method() { ... } | 接口的静态工具方法 |
接口方法修饰符的省略规则
public interface Payable {
// 以下三种写法完全等价:
void pay(double amount); // 写法一:最简
public abstract void pay(double amount); // 写法二:完整形式(编译器会自动补全)
abstract void pay(double amount); // 写法三:省略 public
// 常量
double MIN_AMOUNT = 0.01;
// 等价于:public static final double MIN_AMOUNT = 0.01;
}
| 接口成员 | 省略修饰符时的默认值 | 完整形式 |
|---|---|---|
| 抽象方法 | public abstract | public abstract void method(); |
| 常量 | public static final | public static final int X = 1; |
| default 方法 | public | public default void method() {} |
| static 方法 | public | public static void method() {} |
深度原理分析
1. 接口如何解决 Java 单继承的局限?
C++ 支持多继承,但带来了菱形继承(Diamond Problem)的复杂性。Java 选择单继承 + 多接口的模型:
关键设计思想:接口只定义方法签名,不包含实现逻辑(JDK 8 之前)。实现类负责提供所有方法的具体代码,因此不存在"两个父类方法打架"的问题。JDK 8 引入 default 方法后,如果多个接口有同名 default 方法,编译器会强制实现类自行解决冲突。
2. 接口方法的字节码表示
在 .class 文件中,接口被标记为 ACC_INTERFACE 和 ACC_ABSTRACT:
// javap -v Payable.class
public interface Payable
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
public abstract void pay(double);
flags: ACC_PUBLIC, ACC_ABSTRACT
public static final double MIN_AMOUNT;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
接口的抽象方法在字节码层面与抽象类的抽象方法几乎一致——都标记了 ACC_ABSTRACT。区别在于接口的方法默认还标记了 ACC_PUBLIC。
3. JDK 8 default 方法的原理
JDK 8 允许在接口中定义 default 方法,核心动机是接口演化:
Java 核心类库中
Collection接口新增了stream()方法。如果这是一个抽象方法,所有自定义Collection实现类都会编译失败。因此 JDK 8 引入default方法——接口可以提供默认实现,旧代码无需修改即可通过编译。
JVM 调用 default 方法的机制:
// 字节码层面,调用接口的 default 方法使用 invokeinterface 指令
// JVM 会在当前类的虚方法表(vtable)中查找该方法的入口
// 如果当前类没有重写,则跳转到接口的 default 实现
invokeinterface #5 // InterfaceMethod Payable.printReceipt:()V
JVM 的 invokeinterface 指令在 JDK 8 中进行了升级,能够正确分发到接口的 default 方法实现——这是 JDK 8 在 JVM 层面的重要改动。
4. 为什么接口不能有构造方法?
接口是纯契约,不包含实例字段(所有变量都是 static final 常量)。构造方法的作用是初始化实例字段,而接口没有需要初始化的实例状态,因此构造方法对接口没有意义。
更深层的原因:一个类可以实现多个接口,如果每个接口都有构造方法,那么实例化时调用谁的构造?以什么顺序调用?这完全不可行。
完整示例:飞翔科技多接口系统
示例一:Payable + Printable 多接口实现
架构师白歌要求薪资单既支持多种支付方式,又支持多种打印格式。
// ============ Payable.java ============
/**
* 支付能力接口——任何需要支付的业务对象都实现它
*/
public interface Payable {
/** 执行支付 */
void pay(double amount);
/** 获取支付方式名称 */
String getPaymentMethod();
/** 获取收款账户 */
String getAccountInfo();
}
// ============ Printable.java ============
/**
* 打印能力接口——任何需要输出报表的对象都实现它
*/
public interface Printable {
/** 打印内容 */
void print();
/** 获取打印格式 */
String getFormat();
}
// ============ BankPayment.java ============
/**
* 银行卡支付实现
*/
public class BankPayment implements Payable {
private String bankName;
private String cardNumber;
public BankPayment(String bankName, String cardNumber) {
this.bankName = bankName;
this.cardNumber = cardNumber;
}
@Override
public void pay(double amount) {
System.out.println("【银行卡支付】从 " + bankName
+ " 卡号 " + maskCard(cardNumber)
+ " 扣款 " + String.format("%.2f", amount) + " 元");
}
@Override
public String getPaymentMethod() {
return "银行卡(" + bankName + ")";
}
@Override
public String getAccountInfo() {
return "卡号:" + maskCard(cardNumber);
}
private String maskCard(String card) {
// 银行卡号脱敏:显示前4后4,中间星号
return card.substring(0, 4) + " **** **** " + card.substring(card.length() - 4);
}
}
// ============ AlipayPayment.java ============
/**
* 支付宝支付实现
*/
public class AlipayPayment implements Payable {
private String alipayAccount;
public AlipayPayment(String alipayAccount) {
this.alipayAccount = alipayAccount;
}
@Override
public void pay(double amount) {
System.out.println("【支付宝支付】从账户 " + maskAccount(alipayAccount)
+ " 扣款 " + String.format("%.2f", amount) + " 元");
}
@Override
public String getPaymentMethod() {
return "支付宝";
}
@Override
public String getAccountInfo() {
return "支付宝账户:" + maskAccount(alipayAccount);
}
private String maskAccount(String account) {
if (account.contains("@")) {
// 邮箱:只显示第一个字符和域名
int atIndex = account.indexOf('@');
return account.charAt(0) + "***" + account.substring(atIndex);
}
// 手机号:显示前3后4
return account.substring(0, 3) + "****" + account.substring(account.length() - 4);
}
}
// ============ SalaryPrinter.java ============
/**
* 薪资单打印服务——面向接口编程,不依赖具体支付方式
*/
public class SalaryPrinter {
/**
* 打印薪资单
* @param employeeName 员工姓名
* @param amount 工资金额
* @param payable 支付方式(面向接口,不关心具体是银行卡还是支付宝)
* @param printable 打印格式(面向接口,不关心具体格式)
*/
public void printSalarySlip(String employeeName, double amount,
Payable payable, Printable printable) {
System.out.println("========== 飞翔科技薪资单 ==========");
System.out.println("员工:" + employeeName);
System.out.println("支付方式:" + payable.getPaymentMethod());
System.out.println("收款账户:" + payable.getAccountInfo());
printable.print();
payable.pay(amount);
System.out.println("====================================");
System.out.println();
}
}
// ============ PdfPrintable.java ============
public class PdfPrintable implements Printable {
@Override
public void print() {
System.out.println("【PDF 格式】正在生成薪资单 PDF 文件...");
}
@Override
public String getFormat() {
return "PDF";
}
}
// ============ ExcelPrintable.java ============
public class ExcelPrintable implements Printable {
@Override
public void print() {
System.out.println("【Excel 格式】正在生成薪资单 Excel 文件...");
}
@Override
public String getFormat() {
return "Excel";
}
}
// ============ Main.java ============
public class Main {
public static void main(String[] args) {
SalaryPrinter printer = new SalaryPrinter();
// 大翔:银行卡 + PDF
Payable bankPay = new BankPayment("招商银行", "6222021234567890");
Printable pdfPrint = new PdfPrintable();
printer.printSalarySlip("大翔", 28926.00, bankPay, pdfPrint);
// 小崔:支付宝 + Excel
Payable alipay = new AlipayPayment("xiaocui@feixiang.cn");
Printable excelPrint = new ExcelPrintable();
printer.printSalarySlip("小崔", 3982.75, alipay, excelPrint);
// 白歌:银行卡 + Excel
printer.printSalarySlip("白歌", 32000.00, bankPay, excelPrint);
}
}
运行输出:
========== 飞翔科技薪资单 ==========
员工:大翔
支付方式:银行卡(招商银行)
收款账户:卡号:6222 **** **** 7890
【PDF 格式】正在生成薪资单 PDF 文件...
【银行卡支付】从 招商银行 卡号 6222 **** **** 7890 扣款 28926.00 元
====================================
========== 飞翔科技薪资单 ==========
员工:小崔
支付方式:支付宝
收款账户:支付宝账户:x***@feixiang.cn
【Excel 格式】正在生成薪资单 Excel 文件...
【支付宝支付】从账户 x***@feixiang.cn 扣款 3982.75 元
====================================
========== 飞翔科技薪资单 ==========
员工:白歌
支付方式:银行卡(招商银行)
收款账户:卡号:6222 **** **** 7890
【Excel 格式】正在生成薪资单 Excel 文件...
【银行卡支付】从 招商银行 卡号 6222 **** **** 7890 扣款 32000.00 元
====================================
示例二:JDK 8 default 方法——接口演化
JDK 8 发布后,小崔想在 Payable 接口中新增一个"打印回执"功能,但不希望破坏已有的 BankPayment 和 AlipayPayment 实现。
// ============ Payable.java (JDK 8 增强版) ============
public interface Payable {
void pay(double amount);
String getPaymentMethod();
String getAccountInfo();
/**
* JDK 8 default 方法:打印支付回执
* 提供默认实现,旧代码无需修改
*/
default void printReceipt(double amount) {
System.out.println("--------- 支付回执 ---------");
System.out.println("支付方式:" + getPaymentMethod());
System.out.println("收款账户:" + getAccountInfo());
System.out.println("支付金额:" + String.format("%.2f", amount));
System.out.println("支付时间:" + java.time.LocalDateTime.now());
System.out.println("状态:支付成功");
System.out.println("---------------------------");
}
/**
* JDK 8 static 方法:工厂方法——根据支付类型创建支付实例
*/
static Payable createPayment(String type, String account) {
switch (type.toUpperCase()) {
case "BANK":
return new BankPayment("默认银行", account);
case "ALIPAY":
return new AlipayPayment(account);
default:
throw new IllegalArgumentException("不支持的支付类型:" + type);
}
}
/**
* JDK 8 static 方法:验证支付金额是否合法
*/
static boolean isValidAmount(double amount) {
return amount > 0 && amount < 1000000;
}
}
// ============ WechatPayment.java(新实现类,重写 default 方法) ============
public class WechatPayment implements Payable {
private String wechatId;
public WechatPayment(String wechatId) {
this.wechatId = wechatId;
}
@Override
public void pay(double amount) {
System.out.println("【微信支付】从微信 " + maskWechat(wechatId)
+ " 扣款 " + String.format("%.2f", amount) + " 元");
}
@Override
public String getPaymentMethod() {
return "微信支付";
}
@Override
public String getAccountInfo() {
return "微信账号:" + maskWechat(wechatId);
}
/**
* 微信支付重写回执格式——加上二维码信息
*/
@Override
public void printReceipt(double amount) {
System.out.println("--------- 微信支付回执 ---------");
System.out.println("支付方式:" + getPaymentMethod());
System.out.println("支付金额:" + String.format("%.2f", amount));
System.out.println("电子发票二维码:wx_receipt_"
+ java.util.UUID.randomUUID().toString().substring(0, 8));
System.out.println("-------------------------------");
}
private String maskWechat(String id) {
return id.substring(0, 3) + "****";
}
}
// ============ Main.java ============
public class Main {
public static void main(String[] args) {
// 使用 static 工厂方法创建支付实例
Payable bankPay = Payable.createPayment("BANK", "6222021234567890");
Payable alipay = Payable.createPayment("ALIPAY", "xiaocui@feixiang.cn");
Payable wechatPay = new WechatPayment("wxid_xiaocui");
// 使用 static 工具方法验证金额
System.out.println("金额验证 5000:" + Payable.isValidAmount(5000));
System.out.println("金额验证 -100:" + Payable.isValidAmount(-100));
System.out.println();
// 银行卡:使用默认回执格式
bankPay.pay(10000);
bankPay.printReceipt(10000);
System.out.println();
// 支付宝:使用默认回执格式
alipay.pay(3982.75);
alipay.printReceipt(3982.75);
System.out.println();
// 微信:使用自定义回执格式
wechatPay.pay(5000);
wechatPay.printReceipt(5000);
}
}
运行输出:
金额验证 5000:true
金额验证 -100:false
【银行卡支付】从 默认银行 卡号 6222 **** **** 7890 扣款 10000.00 元
--------- 支付回执 ---------
支付方式:银行卡(默认银行)
收款账户:卡号:6222 **** **** 7890
支付金额:10000.00
支付时间:2024-06-30T10:30:15.123
状态:支付成功
---------------------------
【支付宝支付】从账户 x***@feixiang.cn 扣款 3982.75 元
--------- 支付回执 ---------
支付方式:支付宝
收款账户:支付宝账户:x***@feixiang.cn
支付金额:3982.75
支付时间:2024-06-30T10:30:15.125
状态:支付成功
---------------------------
【微信支付】从微信 wxi**** 扣款 5000.00 元
--------- 微信支付回执 ---------
支付方式:微信支付
支付金额:5000.00
电子发票二维码:wx_receipt_a3f8b2c1
-------------------------------
接口层次结构图
易错场景
反例一:试图给接口定义构造方法
// ❌ 错误:接口不能有构造方法
public interface Payable {
public Payable() { // 编译错误!
// 初始化代码...
}
void pay(double amount);
}
编译错误:
Interfaces cannot have constructors
纠正:
// ✅ 正确:接口不需要构造方法,如有初始化逻辑用 static 工厂方法
public interface Payable {
void pay(double amount);
// 用 static 方法创建实例(JDK 8)
static Payable createBankPayment(String cardNumber) {
return new BankPayment("默认银行", cardNumber);
}
}
反例二:接口方法不是 public
// ❌ 错误:接口方法不能是 protected 或 private(JDK 8 中)
public interface Payable {
protected void pay(double amount); // 编译错误!
private void log(); // 编译错误!(JDK 9 才支持 private 接口方法)
}
编译错误(JDK 8):
Illegal modifier for the interface method pay; only public, abstract,
default, static and strictfp are permitted
纠正:接口中的方法必须是 public(可省略,编译器自动补全)。
反例三:多个接口 default 方法冲突
// 接口A
public interface A {
default void greet() {
System.out.println("Hello from A");
}
}
// 接口B
public interface B {
default void greet() {
System.out.println("Hello from B");
}
}
// ❌ 错误:同时实现 A 和 B,但两个接口有同名 default 方法
public class MyClass implements A, B {
// 编译错误:Duplicate default methods named greet
// 编译器不知道用哪个
}
编译错误:
Duplicate default methods named greet with the parameters () and ()
are inherited from the types B and A
纠正:实现类必须自己重写冲突的方法:
// ✅ 正确:实现类自行解决冲突
public class MyClass implements A, B {
@Override
public void greet() {
// 方案一:自己实现
System.out.println("Hello from MyClass");
// 方案二:指定调用某个接口的 default 实现
// A.super.greet();
// B.super.greet();
}
}
反例四:接口变量被修改
// ❌ 错误:接口中的变量是 public static final,不能修改
public interface Constants {
int MAX_RETRY = 3; // 默认 public static final
}
public class Main {
public static void main(String[] args) {
Constants.MAX_RETRY = 5; // 编译错误!
}
}
编译错误:
The final field Constants.MAX_RETRY cannot be assigned
纠正:接口中的变量是常量,只能读取不能修改。
面试考点
Q1:接口和抽象类在 JDK 8 之后是否趋同了?它们本质区别是什么?
JDK 8 的
default方法确实模糊了接口和抽象类的边界,但本质区别未变:
- 抽象类是类,可以有状态(成员变量)、构造方法、非 public 方法;代表 is-a 关系,是一种模板继承。
- 接口是契约,不能有实例状态(只有常量)、不能有构造方法;代表 can-do 能力,是一种行为约定。
- 一个类只能继承一个抽象类,但可以实现多个接口——这个核心差异永不会变。
Q2:JDK 8 的 default 方法解决了什么问题?
解决了接口向后兼容性问题。Java 核心库(如
java.util.Collection)需要新增方法(如stream()),如果只加抽象方法,所有现有的第三方实现类都会编译失败。default方法允许在接口中提供默认实现,旧代码无需修改即可通过编译,新代码可以按需重写。
Q3:为什么 class Object 中的方法(如 toString、equals)在接口中定义 default 版本无效?
这是 Java 语言规范(JLS 9.4.1.2)的明确规定:接口中的
default方法不能覆写java.lang.Object的public方法。因为方法调用的优先级是superclass > interface,而所有类都继承Object,接口中的 default 版本永远不会被调用——编译器直接禁止这种写法,避免开发者误解。
Q4:接口中的 static 方法和类的 static 方法有什么区别?
- 调用方式:接口的 static 方法只能通过
InterfaceName.method()调用,不能通过实现类的实例或子接口调用。- 继承关系:接口的 static 方法不会被实现类继承,这与类的 static 方法可被继承不同。
- 设计意图:接口的 static 方法通常用作工具方法或工厂方法,将相关功能内聚在接口本身。
Q5:一个类既继承了抽象类又实现了接口,初始化顺序是怎样的?
- 父类(抽象类)的静态代码块/静态变量初始化
- 接口的静态字段初始化(按
implements声明顺序)- 父类构造方法
super()- 子类构造方法
- 由于接口不能有实例字段和构造方法,它们在实例化阶段不参与
<init>流程。