Skip to content

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 弥补单继承缺陷的主要方式。

  • 接口之间支持多继承:接口没有实例变量,方法也没有或只有默认实现,不会产生像类多继承那样的严重冲突,即“菱形继承问题”在接口中可通过覆盖默认方法解决。

    java
    interface A { void methodA(); }
    interface B { void methodB(); }
    // 接口继承接口,使用 extends,且可以同时继承多个
    interface C extends A, B {  
        void methodC();
    }
    // 此时如果一个类 implements C,它必须实现 methodA, methodB, methodC 三个方法
  • 类继承一个父类的同时可以实现一个或多个接口

    java
    class 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(实现)关键字来遵守这份契约。

  1. 使用 interface 定义接口

    java
    [修饰符] interface 接口名 {
      // 1. 常量
      [public static final] 类型 常量名 = 常量值
    
      // 2. 抽象方法
      [public abstract] 类型 抽象方法名 ([参数]); // 没有方法体
    
      // 3. 默认方法
      [public] default 类型 默认方法名 ([参数]) { // 有方法体 }
    
      // 4. 静态方法
      [public] static 类型 静态方法名 ([参数]) { // 有方法体 }
    
      // 5. 私有方法
      private 类型 私有方法名 ([参数]) { // 有方法体 }
    }
  2. 实现类 implements 接口

    java
    class 实现类 implements 接口名,接口名2... {
      // 1. 必须实现接口中的抽象方法,其他方法可实现可不实现
      [public abstract] 类型 抽象方法名 ([参数]) { // 实现方法体}
    
      // 2. 多实现接口时,如果遇到接口中的默认方法冲突,必须重写冲突的默认方法
    }

代码示例:基本示例

image-20260303144104198


代码示例:多重能力与现代接口特性:

下面用一个例子展示接口如何赋予对象“能力”(Can-Do),以及 Java 8 的新特性:

  1. 创建接口

    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();
    }
  2. 创建实现类,并重写接口中所有的抽象方法

    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() 已经被自动继承,可以选择不重写
    }
  3. 创建实现类实例对象(接口不能 new 对象),并调用重写的方法

    java
    public class Main {
        public static void main(String[] args) {
            Duck donald = new Duck();
    
            donald.fly();   // 鸭子扑腾翅膀飞起来了...
            donald.swim();  // 鸭子在水里愉快地游弋。
            donald.land();  // 调用了接口中的默认方法:准备降落... 安全检查完毕。
    
            // 调用接口的静态方法
            Flyable.showRules(); // 飞行法则:安全第一!
        }
    }

设计目的

为什么需要接口(核心设计思想):

  1. 定义“能力” (Can-Do):抽象类定义了“你是什么(Is-A)”,而接口定义了“你能做什么”。例如,鸟(Bird)和飞机(Airplane)是完全不同的事物,不能继承同一个父类,但它们都有飞行的能力,所以它们都可以实现 Flyable 接口。

  2. 解耦与多态:这是接口最强大的地方。在大型项目中,我们通常面向接口编程。比如后端开发中,Controller 层只需要调用 Service 层的接口,而不需要关心具体的实现类是谁。如果未来需要更换数据库或底层逻辑,只需要换一个实现类即可,调用方的代码一行都不用改。

  3. 制定标准:就像 USB 接口标准一样。电脑主板(调用方)只认 USB 接口的规范,不管是鼠标、键盘还是 U 盘(实现类),只要符合 USB 接口规范,插上就能用。

接口成员

在 Java 中,接口(Interface)的成员结构随着 Java 版本的更迭经历了巨大的演变。在 Java 8 之前,接口非常纯粹,只能包含常量和抽象方法;但从 Java 8 和 Java 9 开始,为了解决向后兼容性和代码复用问题,接口中引入了默认方法、静态方法和私有方法。

目前,一个现代 Java 接口中可以包含以下 6 种核心成员。下面为您详细拆解并提供对应的代码示例:

常量

常量 (Constant Variables):

在接口中声明的任何变量,隐式地(自动)都是 public static final。这意味着它们是全局静态常量,必须在声明时进行初始化,且不能被修改

  • 适用场景:定义该接口及其实现类共享的配置或状态码。

  • 注意

    • static final 修饰的成员变量一般当场就赋值
    • 习惯上将 static final 修饰的成员变量名大写
  • 代码示例

    java
    public 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)它们

  • 适用场景:定义具体的行为规范和契约。

  • 代码示例

    java
    public 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 关键字。

  • 代码示例

    java
    public 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)或辅助方法。只能通过 接口名.方法名() 调用。

  • 代码示例

    java
    public 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 方法中重复的代码逻辑,提高代码复用性。

  • 代码示例

    java
    public 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

  • 适用场景:定义与该接口紧密相关的辅助数据结构或状态字典。

  • 代码示例

    java
    public 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.0public static final-接口名.常量名
抽象方法Java 1.0public abstract必须重写实例对象.方法名()
默认方法Java 8public可以选择重写实例对象.方法名()
静态方法Java 8public接口名.方法名()
私有方法Java 9private仅接口内部调用

引入 default 默认方法后,Java 实际上在某种程度上拥有了“多继承”的特征。如果一个类实现了两个不同的接口,而这两个接口恰好包含同名的 default 方法,就会引发经典的**“菱形继承冲突(Diamond Problem)”**。

菱形继承冲突

接口名.super.方法名()

在 Java 8 引入了接口的 default(默认)方法之后,Java 实际上拥有了某种程度上的“多重继承”能力。这就带来了一个经典的面向对象难题——菱形继承冲突(Diamond Problem)

接口名.super.方法名() 这个特殊语法,正是 Java 官方为了解决接口多重继承冲突,以及在实现类中调用父接口默认方法而专门设计的“解药”。它的作用是:

  1. 消除歧义:在多实现导致默认方法冲突时,明确指明要调用哪一个接口的方法。

  2. 代码复用:在重写接口方法时,能够像调用父类方法一样,继续使用接口已经写好的逻辑。

通过这种方式,Java 既拥抱了接口多实现的灵活性,又完美避开了传统 C++ 中多重继承带来的逻辑混乱陷阱。

多重继承的冲突

核心痛点:多重继承的“选择困难症”:

假设你有两个接口 AB,它们碰巧都定义了一个同名且参数列表相同default 方法。此时,如果一个类 C 同时实现了这两个接口,编译器就“懵”了:当你调用这个方法时,到底该用 A 的实现,还是 B 的实现?

为了保证代码的严谨性,Java 编译器此时会直接报错,强制要求类 C 必须重写(Override)这个冲突的方法,以此来表明开发者的明确意图。

语法拆解

语法拆解:

当你被迫重写这个冲突的方法时,你可能并不想完全重写业务逻辑,而是想直接复用其中一个(或全部)接口现成的默认代码。这时就需要用到这个语法:

  • 接口名:明确指定你要调用的是哪一个父接口里的方法(解决歧义)。
  • .super:告诉编译器“我要调用的不是当前类的,而是父级(接口)的方法”。这和普通类中调用 super.method() 概念类似。
  • .方法名():具体调用的默认方法。

应用1:解决多重继承冲突

我们用一个非常贴近生活的例子:“智能手机”同时具备“照相机”和“音乐播放器”的功能。

  1. 实现多接口会出现默认方法的冲突

    java
    // 接口1:照相机
    interface Camera {
      default void start() {
        System.out.println("【相机】正在启动摄像头,准备拍照...");
      }
    }
    
    // 接口2:音乐播放器
    interface MusicPlayer {
      default void start() {
        System.out.println("【播放器】正在加载音频文件,准备播放音乐...");
      }
    }
  2. 实现类时,通过 接口名.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 方法写得挺好,但你需要在它的基础上增加一点额外的逻辑,你同样可以使用这个语法。

java
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 修饰后,引用地址被锁定(即它不能再指向另一个全新的对象),但是对象内部的属性(状态)是允许被修改的

    java
    public 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 配合使用,它们是互斥关系。
    java
    class 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 类