S02-08 面向对象-接口
[TOC]
接口
在 Java 中,接口(Interface) 是一种极度纯粹的“抽象”。如果说抽象类是建房子的“半成品”,那么接口就是一份“契约”或“能力说明书”。
它只规定“应该具备什么功能”,而绝不干涉“具体怎么去实现”。在面向对象设计中,接口是实现松耦合(Decoupling)和多态的最核心工具。
核心特征
核心特征与基本语法:
禁止实例化:和抽象类一样,接口绝对不能被
new出来。隐式的修饰符(经典规则,Java 8 之前):
- 变量:接口中定义的所有变量,默认且只能是
public static final(即全局静态常量)。你写int MAX = 100;实际上等于public static final int MAX = 100;。 - 方法:在 Java 8 之前,接口中的所有方法默认且只能是
public abstract(即公开的抽象方法),不能有方法体。
- 变量:接口中定义的所有变量,默认且只能是
接口作为标准:作为标准存在,接口中的成员非常单一。
多重实现(突破单继承):Java 的类只能单继承(一个儿子只能有一个亲爹),但是一个类可以实现多个接口(一个人可以拥有多种技能)。这是 Java 弥补单继承缺陷的主要方式。
接口之间支持多继承:接口没有实例变量,方法也没有或只有默认实现,不会产生像类多继承那样的严重冲突,即“菱形继承问题”在接口中可通过覆盖默认方法解决。
javainterface A { void methodA(); } interface B { void methodB(); } // 接口继承接口,使用 extends,且可以同时继承多个 interface C extends A, B { void methodC(); } // 此时如果一个类 implements C,它必须实现 methodA, methodB, methodC 三个方法类继承一个父类的同时可以实现一个或多个接口
javaclass Sun extends Father implements A, B { // 业务逻辑 }
接口的现代化演进(Java 8 & Java 9+ 的重大变革):
随着 Java 的发展,接口的形态发生了重大变化。现在的接口已经不再是 100% 的纯抽象了:
Java 8 引入
default(默认方法):接口中允许包含有具体实现的方法,用default修饰。- 目的:为了在不破坏现有实现类的情况下,向后兼容地给接口添加新功能。实现类可以直接继承该方法,也可以重写它。
Java 8 引入
static(静态方法):接口中可以定义带有方法体的静态方法,直接通过接口名.方法名()调用,通常用于提供工具方法。Java 9 引入
private(私有方法):接口中可以定义私有方法,专门用来提取default方法中重复的代码逻辑,不对外暴露。
基本语法
基本语法:
接口使用 interface 关键字来定义,子类使用 implements(实现)关键字来遵守这份契约。
使用
interface定义接口java[修饰符] interface 接口名 { // 1. 常量 [public static final] 类型 常量名 = 常量值 // 2. 抽象方法 [public abstract] 类型 抽象方法名 ([参数]); // 没有方法体 // 3. 默认方法 [public] default 类型 默认方法名 ([参数]) { // 有方法体 } // 4. 静态方法 [public] static 类型 静态方法名 ([参数]) { // 有方法体 } // 5. 私有方法 private 类型 私有方法名 ([参数]) { // 有方法体 } }实现类
implements接口javaclass 实现类 implements 接口名,接口名2... { // 1. 必须实现接口中的抽象方法,其他方法可实现可不实现 [public abstract] 类型 抽象方法名 ([参数]) { // 实现方法体} // 2. 多实现接口时,如果遇到接口中的默认方法冲突,必须重写冲突的默认方法 }
代码示例:基本示例

代码示例:多重能力与现代接口特性:
下面用一个例子展示接口如何赋予对象“能力”(Can-Do),以及 Java 8 的新特性:
创建接口
java// 接口1:飞行能力说明书 interface Flyable { // 1. 隐式 public static final 常量 int MAX_SPEED = 1000; // 2. 隐式 public abstract 抽象方法(必须被实现类重写) void fly(); // 3. Java 8 默认方法(带有具体实现,实现类可选重写) default void land() { System.out.println("准备降落,收起起落架..."); checkSafety(); // 调用私有方法 } // 4. Java 8 静态方法(工具方法) static void showRules() { System.out.println("飞行法则:安全第一!"); } // 5. Java 9 私有方法(辅助默认方法) private void checkSafety() { System.out.println("安全检查完毕。"); } } // 接口2:游泳能力说明书 interface Swimmable { void swim(); }创建实现类,并重写接口中所有的抽象方法
java// 实现类:鸭子。它继承自某个父类(如果是的话),并同时实现了飞行和游泳两种能力 class Duck implements Flyable, Swimmable { // 必须实现 Flyable 的抽象方法 @Override public void fly() { System.out.println("鸭子扑腾翅膀飞起来了!速度最高可达: " + MAX_SPEED); } // 必须实现 Swimmable 的抽象方法 @Override public void swim() { System.out.println("鸭子在水里愉快地游弋。"); } // 注意:默认方法 land() 已经被自动继承,可以选择不重写 }创建实现类实例对象(接口不能 new 对象),并调用重写的方法
javapublic class Main { public static void main(String[] args) { Duck donald = new Duck(); donald.fly(); // 鸭子扑腾翅膀飞起来了... donald.swim(); // 鸭子在水里愉快地游弋。 donald.land(); // 调用了接口中的默认方法:准备降落... 安全检查完毕。 // 调用接口的静态方法 Flyable.showRules(); // 飞行法则:安全第一! } }
设计目的
为什么需要接口(核心设计思想):
定义“能力” (Can-Do):抽象类定义了“你是什么(Is-A)”,而接口定义了“你能做什么”。例如,鸟(Bird)和飞机(Airplane)是完全不同的事物,不能继承同一个父类,但它们都有飞行的能力,所以它们都可以实现
Flyable接口。解耦与多态:这是接口最强大的地方。在大型项目中,我们通常面向接口编程。比如后端开发中,Controller 层只需要调用 Service 层的接口,而不需要关心具体的实现类是谁。如果未来需要更换数据库或底层逻辑,只需要换一个实现类即可,调用方的代码一行都不用改。
制定标准:就像 USB 接口标准一样。电脑主板(调用方)只认 USB 接口的规范,不管是鼠标、键盘还是 U 盘(实现类),只要符合 USB 接口规范,插上就能用。
接口成员
在 Java 中,接口(Interface)的成员结构随着 Java 版本的更迭经历了巨大的演变。在 Java 8 之前,接口非常纯粹,只能包含常量和抽象方法;但从 Java 8 和 Java 9 开始,为了解决向后兼容性和代码复用问题,接口中引入了默认方法、静态方法和私有方法。
目前,一个现代 Java 接口中可以包含以下 6 种核心成员。下面为您详细拆解并提供对应的代码示例:
常量
常量 (Constant Variables):
在接口中声明的任何变量,隐式地(自动)都是 public static final 的。这意味着它们是全局静态常量,必须在声明时进行初始化,且不能被修改。
适用场景:定义该接口及其实现类共享的配置或状态码。
注意:
- 被
static final修饰的成员变量一般当场就赋值。 - 习惯上将
static final修饰的成员变量名大写。
- 被
代码示例:
javapublic interface DatabaseConfig { // 1. 即使不写 public static final,编译器也会自动加上 String DB_URL = "jdbc:mysql://localhost:3306/mydb"; int MAX_CONNECTIONS = 100; } class Test { void printConfig() { // 2. 使用方式:直接通过接口名调用 System.out.println("数据库地址: " + DatabaseConfig.DB_URL); // 3. ❌ 编译报错!常量不可修改 // DatabaseConfig.MAX_CONNECTIONS = 200; } }
抽象方法
抽象方法 (Abstract Methods):
这是接口最核心的成员。接口中的普通方法隐式地都是 public abstract 的。它们只有方法签名,没有方法体(没有大括号),强制要求非抽象的实现类必须重写(Override)它们。
适用场景:定义具体的行为规范和契约。
代码示例:
javapublic interface PaymentProcessor { // 隐式为 public abstract boolean pay(double amount); boolean pay(double amount);void refund(String transactionId); } class AliPayProcessor implements PaymentProcessor { // 必须实现接口中的抽象方法 @Override public boolean pay(double amount) { System.out.println("支付宝支付了: " + amount + " 元"); return true; } @Override public void refund(String transactionId) { System.out.println("支付宝退款单号: " + transactionId); } }
默认方法
默认方法 (Default Methods) - Java 8 引入:
使用 default 关键字修饰,带有具体的方法体。实现类会自动继承这个方法,可以直接使用,也可以根据需要重写它。
适用场景:在不破坏现有实现类(向后兼容)的情况下,为接口添加新的功能。
注意:在接口中可以显式使用
default关键字修饰方法,但在普通类中不能显式使用default关键字。代码示例:
javapublic interface Vehicle { void drive(); // 抽象方法 // 默认方法:提供基础实现 default void turnOnRadio() { System.out.println("车载收音机已打开,正在播放音乐..."); } } class Car implements Vehicle { @Override public void drive() { System.out.println("汽车在公路上行驶。"); } // turnOnRadio() 已经被自动继承,无需强制重写 } class TestCar { public static void main(String[] args) { Car myCar = new Car(); myCar.drive(); myCar.turnOnRadio(); // ✅ 直接使用实现类的实例对象调用接口提供的默认方法 } }
静态方法
静态方法 (Static Methods) - Java 8 引入:
使用 static 关键字修饰,带有具体的方法体。与类中的静态方法类似,它属于接口本身,不能被实现类继承或重写。
适用场景:提供与该接口相关的工具方法(Utility methods)或辅助方法。只能通过
接口名.方法名()调用。代码示例:
javapublic interface MathUtils { // 静态工具方法 static int add(int a, int b) { return a + b; }static boolean isEven(int number) { return number % 2 == 0; } } class TestMath { public static void main(String[] args) { // 直接通过接口名调用,不需要(也不能)通过实现类对象调用 int sum = MathUtils.add(5, 10); System.out.println("Sum: " + sum); // 输出: Sum: 15 } }
私有方法
私有方法 (Private Methods) - Java 9 引入:
使用 private 关键字修饰,带有具体的方法体(可以是普通私有方法,也可以是 private static 方法)。它们对外部和实现类完全不可见。
适用场景:专门用来提取接口内部多个
default方法或static方法中重复的代码逻辑,提高代码复用性。代码示例:
javapublic interface Logger { default void logInfo(String message) { logToConsole("INFO", message); // 调用内部私有方法 } default void logError(String message) { logToConsole("ERROR", message); // 调用内部私有方法 } // 私有方法:不对外暴露,只服务于接口内部的默认/静态方法 private void logToConsole(String level, String message) { System.out.println(System.currentTimeMillis() + " [" + level + "] " + message); } }
嵌套类型
嵌套类型 (Nested Types):
接口中还可以定义嵌套的类、接口或枚举。这些嵌套类型隐式地都是 public static 的。
适用场景:定义与该接口紧密相关的辅助数据结构或状态字典。
代码示例:
javapublic interface Task { void execute(); // 嵌套枚举(隐式 public static) enum Status { PENDING, RUNNING, COMPLETED, FAILED } } class MyTask implements Task { @Override public void execute() { // 直接使用接口中的嵌套枚举 Task.Status currentStatus = Task.Status.RUNNING; System.out.println("任务状态: " + currentStatus); } }
总结
| 成员类型 | 引入版本 | 隐式修饰符 | 是否有方法体 | 能否被子类重写 | 调用方式 |
|---|---|---|---|---|---|
| 常量 | Java 1.0 | public static final | - | 否 | 接口名.常量名 |
| 抽象方法 | Java 1.0 | public abstract | 无 | 必须重写 | 实例对象.方法名() |
| 默认方法 | Java 8 | public | 有 | 可以选择重写 | 实例对象.方法名() |
| 静态方法 | Java 8 | public | 有 | 否 | 接口名.方法名() |
| 私有方法 | Java 9 | private | 有 | 否 | 仅接口内部调用 |
引入 default 默认方法后,Java 实际上在某种程度上拥有了“多继承”的特征。如果一个类实现了两个不同的接口,而这两个接口恰好包含同名的 default 方法,就会引发经典的**“菱形继承冲突(Diamond Problem)”**。
菱形继承冲突
接口名.super.方法名()
在 Java 8 引入了接口的 default(默认)方法之后,Java 实际上拥有了某种程度上的“多重继承”能力。这就带来了一个经典的面向对象难题——菱形继承冲突(Diamond Problem)。
接口名.super.方法名() 这个特殊语法,正是 Java 官方为了解决接口多重继承冲突,以及在实现类中调用父接口默认方法而专门设计的“解药”。它的作用是:
消除歧义:在多实现导致默认方法冲突时,明确指明要调用哪一个接口的方法。
代码复用:在重写接口方法时,能够像调用父类方法一样,继续使用接口已经写好的逻辑。
通过这种方式,Java 既拥抱了接口多实现的灵活性,又完美避开了传统 C++ 中多重继承带来的逻辑混乱陷阱。
多重继承的冲突
核心痛点:多重继承的“选择困难症”:
假设你有两个接口 A 和 B,它们碰巧都定义了一个同名且参数列表相同的 default 方法。此时,如果一个类 C 同时实现了这两个接口,编译器就“懵”了:当你调用这个方法时,到底该用 A 的实现,还是 B 的实现?
为了保证代码的严谨性,Java 编译器此时会直接报错,强制要求类 C 必须重写(Override)这个冲突的方法,以此来表明开发者的明确意图。
语法拆解
语法拆解:
当你被迫重写这个冲突的方法时,你可能并不想完全重写业务逻辑,而是想直接复用其中一个(或全部)接口现成的默认代码。这时就需要用到这个语法:
接口名:明确指定你要调用的是哪一个父接口里的方法(解决歧义)。.super:告诉编译器“我要调用的不是当前类的,而是父级(接口)的方法”。这和普通类中调用super.method()概念类似。.方法名():具体调用的默认方法。
应用1:解决多重继承冲突
我们用一个非常贴近生活的例子:“智能手机”同时具备“照相机”和“音乐播放器”的功能。
实现多接口会出现默认方法的冲突
java// 接口1:照相机 interface Camera { default void start() { System.out.println("【相机】正在启动摄像头,准备拍照..."); } } // 接口2:音乐播放器 interface MusicPlayer { default void start() { System.out.println("【播放器】正在加载音频文件,准备播放音乐..."); } }实现类时,通过
接口名.super.方法名()明确指定调用的默认方法名java// 实现类:智能手机同时实现两个接口 // 此时如果不重写 start(),编译器会报错: // class SmartPhone inherits unrelated defaults for start() from types Camera and MusicPlayer class SmartPhone implements Camera, MusicPlayer { // 强制必须重写冲突的方法,并且只能重写一遍! @Override public void start() { System.out.println("--- 智能手机开机中 ---"); // 关键语法登场:我想在这里复用父接口的默认实现 // 1. 明确调用 Camera 接口的默认方法 Camera.super.start(); // 2. 明确调用 MusicPlayer 接口的默认方法 MusicPlayer.super.start(); System.out.println("--- 智能手机启动完毕 ---"); } } // 测试类 public class Main { public static void main(String[] args) { SmartPhone myPhone = new SmartPhone(); myPhone.start(); /* 输出结果: --- 智能手机开机中 --- 【相机】正在启动摄像头,准备拍照... 【播放器】正在加载音频文件,准备播放音乐... --- 智能手机启动完毕 --- */ } }
应用2:单纯扩展与复用
哪怕没有发生继承冲突(比如你只实现了一个接口),如果你觉得接口提供的 default 方法写得挺好,但你需要在它的基础上增加一点额外的逻辑,你同样可以使用这个语法。
interface Vehicle {
default void drive() {
System.out.println("车辆引擎已启动,准备出发。");
}
}
class SportsCar implements Vehicle {
@Override
public void drive() {
System.out.println("开启赛道模式!"); // 子类增加的额外逻辑
// 调用父接口的默认实现
Vehicle.super.drive();
System.out.println("百公里加速只需 3 秒!"); // 子类增加的额外逻辑
}
}抽象类 vs 接口
这是 Java 面试和实际架构设计中最常面临的选择。简单对比:
| 特性 | 抽象类 (Abstract Class) | 接口 (Interface) |
|---|---|---|
| 定位 | 都位于继承的顶端,用于被其他类实现或继承 | |
| 能否实例化 | 都不能通过 new 创建实例对象 | |
| 抽象方法 | 都包含抽象方法,子类都必须实现这些抽象方法 | |
| 设计意图 | 表达 "Is-A" (是一个) 关系,属于事物本质的抽象。 | 表达 "Has-A" / "Can-Do" (具备某种能力/行为) 关系,是一种契约。 |
| 继承机制 | 单继承:一个类只能继承一个抽象类。 | 多实现:一个类可以实现多个接口。 |
| 成员变量 | 可以有各种类型的成员变量。 | 只能有 public static final 常量。 |
| 构造方法 | 有构造方法(供子类 super 调用)。 | 没有构造方法。 |
| 适用场景 | 当多个子类有共享的代码和状态(变量) 时使用。 | 当你想定义一组行为规范,且不同类之间没有直接继承关系时使用。 |
final
在 Java 中,final 关键字代表着**“最终的”、“不可改变的”**。它就像是代码里的“一把锁”,可以用来锁定变量、方法和类,防止它们被意外修改或继承。
final 设计初衷主要是为了架构安全、不可变性(Immutability)以及代码逻辑的严谨性。它主要有三种使用场景,我们来逐一拆解:
final 修饰变量
final 修饰变量 (Variables):锁定值或引用地址:
这是 final 最常见的用法。一旦一个 final 变量被初始化赋值,它就永远不能再被赋予新的值。
这里有一个非常核心的区分点(也是面试常考点):基本数据类型和引用数据类型在 final 下的表现截然不同。
基本数据类型 (Primitive Types):例如
int,double,boolean等。被final修饰后,数值本身被锁定,完全不能改变。引用数据类型 (Reference Types):例如数组、对象。被
final修饰后,引用地址被锁定(即它不能再指向另一个全新的对象),但是对象内部的属性(状态)是允许被修改的。javapublic class FinalVariableDemo { // 1. 修饰静态成员变量(这就是 Java 中的“常量”,通常全大写) public static final double PI = 3.14159; // 2. 空白 final (Blank Final):声明时不赋值,但必须在构造方法中完成赋值 private final int id;public FinalVariableDemo(int id) { this.id = id; // 构造器中赋值,一旦赋值,此后不可修改 } public void testFinal() { // 3. 修饰局部变量(基本类型) final int age = 18; // age = 19; // ❌ 编译错误!不能更改 final 基本类型的值 // 4. 修饰局部变量(引用类型) final StringBuilder sb = new StringBuilder("Hello"); // sb = new StringBuilder("World"); // ❌ 编译错误!不能改变引用地址,不能换新对象 sb.append(" Java"); // ✅ 允许!可以修改原有对象内部的状态 System.out.println(sb.toString()); // 输出: Hello Java } }
final 修饰方法
final 修饰方法 (Methods):禁止重写:
当 final 用来修饰一个方法时,它向所有的子类宣告:这个方法的实现已经是最终版本了,你们可以继承、可以直接用,但绝对不能重写(Override)它。
设计目的:如果你认为一个父类方法的内部逻辑非常核心,或者关系到类的内部稳定性和安全性,绝不希望子类通过重写去篡改它的行为,就可以加上
final。注意:
- 父类的
private方法隐式地包含了final的语义,因为子类根本看不见私有方法,自然也无从重写。 - 不能和
abstract配合使用,它们是互斥关系。
javaclass Parent { // final 修饰方法 public final void coreAlgorithm() { System.out.println("这是核心算法,子类严禁修改!"); } } class Child extends Parent { // @Override // public void coreAlgorithm() { // System.out.println("尝试修改..."); // ❌ 编译报错!无法覆盖 final 方法 // } }- 父类的
final 修饰类
final 修饰类 (Classes):禁止继承:
如果一个类被 final 修饰,就意味着这个类不能被任何其他类继承。它是一个完整的、不可扩展的“断子绝孙”类。
设计目的:通常是为了绝对的安全性。如果一个类的设计已经非常完美,或者它的内部状态极其敏感,绝不允许外界通过继承(可能导致多态滥用或内部逻辑被破坏)来改变它,就会设计为
final类。经典案例:Java 中的核心类
java.lang.String就是一个彻头彻尾的final类。你永远无法写出class MyString extends String这样的代码。这保证了字符串在 Java 系统底层的绝对安全和稳定。此外,所有的基本类型包装类(如Integer,Double)也都是final的。注意:
final类中的所有成员方法都会隐式地变成final方法(因为都没有子类了,自然没有重写一说)。java// final 修饰类 final class SecureData { public void printData() { System.out.println("绝对安全的底层数据类"); } } // class Hacker extends SecureData { } // ❌ 编译报错!无法继承 final 类