接口默认方法、静态方法与私有方法 | JavaSE

接口默认方法、静态方法与私有方法

一、学习目标

学完本章,你应该能够:

  1. 能够解释为什么 Java 8 开始需要增强接口的实现能力。
  2. 能够正确声明、调用和重写接口默认方法(Default Method)。
  3. 能够正确定义和调用接口静态方法(Static Interface Method)。
  4. 能够使用 Java 9 引入的接口私有方法复用接口内部公共代码。
  5. 能够区分抽象方法、默认方法、静态方法和私有方法的调用方式与继承规则。
  6. 能够处理多个接口出现同名默认方法时的冲突问题。

二、核心知识

2.1 为什么现代 Java 要增强接口

早期 Java 接口的主要职责非常纯粹:

定义规范
        ↓
实现类负责实现

例如:

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

这种设计很好,但会遇到一个非常现实的问题:

已经发布并被大量类实现的接口,如果后来需要新增一个方法怎么办?

假设最初:

public interface Player {

    void play();

}

已经存在:

MusicPlayer
VideoPlayer
NetworkPlayer
LocalPlayer
...

大量实现类。

现在接口增加:

void pause();

那么所有普通实现类都必须立即实现:

pause()

否则无法通过编译。

对于大型 API 和公共框架而言,这会严重影响接口演化。

因此 Java 8 引入了默认方法(Default Method)

接口可以直接提供一个默认实现:

default void pause() {
    System.out.println("暂停");
}

原来的实现类即使没有重写它,也仍然能够继续工作。

这使接口拥有了更好的:

  • 向后兼容能力;
  • API 演化能力;
  • 公共行为复用能力。

2.2 接口方法的现代结构

在 JDK 21 中,接口中常见的方法可以分成:

| 方法类型 | 典型关键字 | 是否有方法体 | 主要调用方式 | | ------------ | ---------------- | -----------: | ------------ | | 抽象方法 | 无 / abstract | 否 | 实现类对象 | | 默认方法 | default | 是 | 实现类对象 | | 静态方法 | static | 是 | 接口名 | | 私有实例方法 | private | 是 | 接口内部 | | 私有静态方法 | private static | 是 | 接口内部 |

本章重点掌握前三种公开能力以及接口私有辅助方法。


2.3 默认方法

默认方法使用:

default

关键字。

语法:

public interface 接口名 {

    default 返回值类型 方法名(参数列表) {
        方法体;
    }

}

例如:

public interface Player {

    void play();

    default void pause() {
        System.out.println("默认暂停");
    }
}

实现类:

public class MusicPlayer implements Player {

    @Override
    public void play() {
        System.out.println("播放音乐");
    }
}

虽然 MusicPlayer 没有实现:

pause()

仍然可以:

MusicPlayer player = new MusicPlayer();

player.play();
player.pause();

因为接口已经提供了默认实现。


2.4 default 方法本质上是实例方法

默认方法不是静态方法。

例如:

public interface Device {

    default void start() {
        System.out.println("设备启动");
    }

}

实现:

public class Computer implements Device {
}

调用:

Computer computer = new Computer();

computer.start();

也可以使用接口引用:

Device device = new Computer();

device.start();

不能简单写:

Device.start();

因为它是实例方法。


2.5 实现类可以重写默认方法

默认实现只意味着:

实现类“不重写也有一个可用版本”。

并不意味着不能修改。

例如:

public interface Payment {

    default void printReceipt() {
        System.out.println("生成默认支付凭证");
    }

}

实现类:

public class AlipayPayment implements Payment {

    @Override
    public void printReceipt() {
        System.out.println("生成支付宝电子凭证");
    }
}

调用:

Payment payment = new AlipayPayment();

payment.printReceipt();

最终执行实现类自己的版本。

因此默认方法仍然符合多态和方法重写机制。


2.6 接口静态方法

接口可以定义:

static

方法。

例如:

public interface PaymentUtils {

    static boolean validAmount(double amount) {
        return amount > 0;
    }

}

调用:

boolean result = PaymentUtils.validAmount(100);

注意:

接口静态方法应该通过接口名调用。

例如:

PaymentUtils.validAmount(100);

而不是把它理解成普通对象方法。


2.7 静态方法属于接口本身

假设:

public interface A {

    static void show() {
        System.out.println("A.show");
    }

}

实现:

public class B implements A {
}

调用:

A.show();

接口的 static 方法不会因为:

B implements A

就变成 B 的实例行为。

因此应该牢记:

default → 跟着对象走
static  → 跟着接口类型走

2.8 接口私有方法

Java 9 开始,接口允许声明私有方法。

例如:

public interface Logger {

    default void info(String message) {
        print("INFO", message);
    }

    default void error(String message) {
        print("ERROR", message);
    }

    private void print(String level, String message) {
        System.out.println("[" + level + "] " + message);
    }
}

这里:

print()

只是:

info()
error()

共同使用的内部辅助逻辑。

外部不能直接调用:

logger.print(...);

实现类也不能直接调用。

它属于接口自身的内部实现细节。


2.9 为什么接口需要 private 方法

假设没有私有方法:

public interface Logger {

    default void info(String message) {
        System.out.println("==========");
        System.out.println("[INFO] " + message);
        System.out.println("==========");
    }

    default void error(String message) {
        System.out.println("==========");
        System.out.println("[ERROR] " + message);
        System.out.println("==========");
    }
}

出现大量重复。

如果把公共逻辑定义成:

default

方法,又意味着实现类也会看到这一行为。

但它明明只是接口内部细节。

所以可以抽取:

private void print(String level, String message) {
    System.out.println("==========");
    System.out.println("[" + level + "] " + message);
    System.out.println("==========");
}

这样接口自身也能做到:

公共 API
    ↓
内部私有实现

更接近普通类的封装能力。


2.10 private 接口方法不是 public

必须特别区分:

default

方法如果没有显式访问修饰符,对外属于 public 实例方法。

普通接口抽象方法同样隐式 public

接口公开静态方法没有显式访问修饰符时也是 public

但是:

private

方法就是私有方法。

它:

  • 不对实现类公开;
  • 不对调用者公开;
  • 不被子接口继承;
  • 不能被实现类重写。

它只服务接口自己的内部实现。


三、使用方法

3.1 完整示例

定义:

public interface Loggable {

    default void info(String message) {
        print("INFO", message);
    }

    default void error(String message) {
        print("ERROR", message);
    }

    private void print(String level, String message) {
        System.out.println("[" + level + "] " + message);
    }

    static String version() {
        return "1.0";
    }
}

实现:

public class Service implements Loggable {
}

调用:

public class InterfaceMethodDemo {

    public static void main(String[] args) {
        Service service = new Service();

        service.info("启动成功");
        service.error("参数非法");

        System.out.println(Loggable.version());
    }
}

其中:

service.info(...)
service.error(...)

调用的是默认方法。

默认方法内部:

print(...)

调用接口私有方法。

而:

Loggable.version()

调用接口静态方法。


3.2 四种方法放在一起比较

考虑:

public interface Demo {

    void method1();

    default void method2() {
    }

    static void method3() {
    }

    private void method4() {
    }
}

可以这样建立知识模型:

method1
抽象方法
→ 实现类必须关注

method2
默认方法
→ 实现类直接继承行为,也可以重写

method3
静态方法
→ 接口自己使用,接口名调用

method4
私有方法
→ 接口内部封装

3.3 默认方法最重要的用途:接口演进

第一版:

public interface DataExporter {

    void export();

}

实现类:

PdfExporter
ExcelExporter
CsvExporter

后来希望增加:

preview()

如果直接:

void preview();

所有实现类都必须修改。

可以改为:

public interface DataExporter {

    void export();

    default void preview() {
        System.out.println("当前导出器暂不提供特殊预览");
    }
}

原实现类不受影响。

特殊实现类仍可重写:

public class PdfExporter implements DataExporter {

    @Override
    public void export() {
        System.out.println("导出 PDF");
    }

    @Override
    public void preview() {
        System.out.println("预览 PDF");
    }
}

这正是默认方法设计的重要动机。


四、原理与进阶

4.1 多个接口拥有同名默认方法怎么办

考虑:

public interface A {

    default void show() {
        System.out.println("A");
    }
}
public interface B {

    default void show() {
        System.out.println("B");
    }
}

现在:

public class C implements A, B {
}

编译器无法确定:

new C().show();

究竟应该执行:

A.show()

还是:

B.show()

因此实现类必须主动解决冲突:

public class C implements A, B {

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

这相当于告诉编译器:

不需要替我选择,我自己提供最终实现。


4.2 如何显式调用指定接口的默认实现

有时我们希望在重写之后继续复用某个父接口版本。

语法:

接口名.super.方法名();

例如:

public class C implements A, B {

    @Override
    public void show() {
        A.super.show();
        B.super.show();

        System.out.println("C");
    }
}

执行:

new C().show();

逻辑相当于:

A.show
B.show
C.show

4.3 为什么 Java 要强制解决这种冲突

如果 Java 自动任选一个:

A.show()

那么结果可能与开发者业务意图完全不同。

如果根据接口声明顺序:

implements A, B

决定优先级,又会导致:

implements B, A

出现不同语义。

这会让代码非常难以维护。

所以 Java 的策略是:

当同等有效的默认实现发生冲突时,让实现类明确做出选择。

这种设计强调显式优于隐式


4.4 类方法与接口默认方法重名

考虑:

class Parent {

    public void show() {
        System.out.println("Parent");
    }
}

接口:

interface A {

    default void show() {
        System.out.println("A");
    }
}

子类:

class Child extends Parent implements A {
}

调用:

new Child().show();

类继承体系中的方法会占据优先地位。

可以建立一个基础记忆:

class wins —— 类中的实例方法优先于接口默认方法。

原因也很实际:

Java 在加入默认方法时,需要尽可能避免改变原来基于类继承的既有行为。


4.5 更具体的子接口默认方法优先

例如:

interface Animal {

    default void move() {
        System.out.println("Animal move");
    }
}
interface Bird extends Animal {

    @Override
    default void move() {
        System.out.println("Bird fly");
    }
}

实现:

class Sparrow implements Bird {
}

调用:

new Sparrow().move();

使用的是更加具体的:

Bird.move()

而不是更上层的:

Animal.move()

可以理解成:

更具体的接口声明
        ↓
覆盖更一般的接口声明

4.6 静态接口方法不会被实现类继承

假设:

interface Tool {

    static void help() {
        System.out.println("help");
    }
}
class ToolImpl implements Tool {
}

正确:

Tool.help();

不要把它理解成:

new ToolImpl().help();

也不要认为:

ToolImpl.help();

就是实现类继承来的接口静态方法。

接口静态方法属于声明它的接口自身。


4.7 private 方法不会被继承和重写

例如:

interface A {

    private void helper() {
    }
}

实现类:

class B implements A {
}

B 并没有获得一个可以直接访问的:

helper()

方法。

因此不能:

new B().helper();

也谈不上通过实现类:

@Override

去重写接口的私有方法。

这与普通类中的私有实现思想类似:

私有成员属于内部实现细节,而不是子类型契约。


五、实践应用

5.1 default:接口版本升级

非常适合:

已经有大量实现类
        +
接口需要新增一个带合理默认行为的新能力

例如:

interface NotificationSender {

    void send(String message);

    default boolean supportsBatch() {
        return false;
    }
}

原实现类继续运行。

支持批量发送的实现可以:

@Override
public boolean supportsBatch() {
    return true;
}

5.2 static:与接口语义强相关的工具行为

例如:

public interface Payment {

    void pay(double amount);

    static boolean isValidAmount(double amount) {
        return amount > 0;
    }
}

调用:

Payment.isValidAmount(100);

这种方法:

  • Payment 概念高度相关;
  • 又不依赖某一个 Payment 对象;

适合作为接口静态行为。


5.3 private:接口内部代码复用

例如:

public interface Validator {

    default boolean validUsername(String username) {
        return notBlank(username) && username.length() >= 4;
    }

    default boolean validPassword(String password) {
        return notBlank(password) && password.length() >= 8;
    }

    private boolean notBlank(String value) {
        return value != null && !value.isBlank();
    }
}

这里:

notBlank()

属于接口内部实现细节。

对使用者真正有意义的只有:

validUsername()
validPassword()

这体现了接口自身的封装。


六、常见问题

6.1 default 方法是抽象方法吗?

不是。

默认方法具有完整方法体:

default void test() {
    System.out.println("test");
}

而抽象方法:

void test();

没有方法体。


6.2 default 方法必须由实现类重写吗?

不必须。

实现类可以直接继承默认实现。

只有业务需要自定义行为时才重写。


6.3 default 是不是访问权限修饰符?

不是。

default 在这里是用于声明:

接口默认实例方法。

不要把它与:

default/package-private 包访问权限

混为一谈。


6.4 接口 static 方法使用对象调用吗?

应该使用接口名:

InterfaceName.method();

例如:

Loggable.version();

6.5 private 接口方法是 Java 8 就有的吗?

不是。

应该区分:

Java 8
├── default 方法
└── static 方法

Java 9
└── private 接口方法

在 JDK 21 学习环境下三者都可以正常使用。


6.6 接口所有方法是不是都会默认 public?

不是。

这是一个非常重要的易错点。

普通接口抽象方法如果没有访问修饰符:

void test();

隐式为:

public abstract

默认方法:

default void test() {
}

对外也是公开实例方法。

公开静态方法:

static void test() {
}

没有显式访问修饰符时属于公开静态接口方法。

但是:

private void helper() {
}

明确就是 private

因此不能概括成:

“接口里的所有方法都默认 public。”


6.7 两个接口出现相同 default 方法怎么办?

实现类必须主动解决冲突,例如:

@Override
public void show() {
}

需要调用指定父接口默认实现时:

A.super.show();

6.8 父类方法和接口 default 方法冲突怎么办?

正常情况下,继承自类体系的实例方法优先。

例如:

Parent.show()
A.default show()

子类继承 Parent 并实现 A 时,会使用类体系中的方法。


6.9 static 方法可以使用 this 吗?

不能。

静态接口方法属于静态上下文,并不存在当前对象。

因此不能在其中直接使用:

this

这和普通类静态方法中的基本规则一致。


6.10 接口有了 default 方法以后是不是就等于抽象类?

不是。

两者依然存在明显区别。

接口主要强调:

契约 + 能力 + 多实现

抽象类仍然适合:

共同状态 + 类继承体系 + 部分公共实现

默认方法只是增强了接口演进和代码复用能力,并没有把接口变成普通父类。


七、练习与验收

7.1 知识问答

  1. Java 为什么要为接口增加默认方法?
  2. 默认方法使用什么关键字声明?
  3. 默认方法是实例方法还是静态方法?
  4. 默认方法通常通过谁调用?
  5. 实现类是否必须重写默认方法?
  6. 接口静态方法如何调用?
  7. 接口静态方法会不会作为实例方法继承到实现类?
  8. 接口私有方法从哪个 Java 版本开始支持?
  9. 接口私有方法主要解决什么问题?
  10. 接口私有方法能否被实现类直接调用?
  11. 接口私有方法能否被实现类重写?
  12. default 方法与抽象方法最大的区别是什么?
  13. 两个接口出现同名默认方法时,实现类应该怎么办?
  14. 如何显式调用指定父接口的默认方法?
  15. 父类实例方法和接口默认方法同名时,通常哪个优先?
  16. 为什么不能简单认为“接口中所有方法都是 public”?

7.2 代码阅读

阅读:

interface A {

    default void test() {
        System.out.println("A");
    }
}

class B implements A {
}

public class Test {
    public static void main(String[] args) {
        B b = new B();
        b.test();
    }
}

回答:

  1. B 没有重写 test(),代码能否通过编译?
  2. 调用 b.test() 时执行哪个方法?
  3. test() 属于实例方法还是静态方法?

阅读:

interface Tool {

    static void print() {
        System.out.println("Tool");
    }
}

class ToolImpl implements Tool {
}

判断:

Tool.print();
ToolImpl.print();
new ToolImpl().print();

分别应该如何理解?


阅读:

interface A {

    default void show() {
        System.out.println("A");
    }
}

interface B {

    default void show() {
        System.out.println("B");
    }
}

class C implements A, B {

    @Override
    public void show() {
        A.super.show();
    }
}

回答:

  1. 为什么 C 必须重写 show()
  2. A.super.show() 表示什么?
  3. 最终输出什么?
  4. 如果删除整个重写方法会发生什么?

7.3 手写代码

  1. 定义一个接口,包含一个抽象方法和一个默认方法。
  2. 创建实现类,只实现抽象方法,并调用接口默认方法。
  3. 重写默认方法,观察调用结果变化。
  4. 定义一个接口静态方法,并使用正确方式调用。
  5. 定义两个默认方法,把共同代码抽取到一个接口私有方法。
  6. 定义两个接口,它们拥有相同签名的默认方法,再由同一个类同时实现,并正确解决冲突。
  7. 使用 A.super.method() 调用其中某一个接口的默认实现。

7.4 Debug

下面代码存在问题:

interface A {

    default void show();

}

要求:

  1. 判断错误原因。
  2. 说明默认方法的方法体规则。
  3. 修复代码。

下面代码存在问题:

interface A {

    private void helper() {
        System.out.println("helper");
    }
}

class B implements A {

    public void test() {
        helper();
    }
}

要求:

  1. 判断 B 为什么不能直接调用 helper()
  2. 说明接口 private 方法的可见范围。
  3. 重新设计代码,使公共行为能够间接使用该私有逻辑。

下面代码:

interface A {

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

interface B {

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

class C implements A, B {
}

要求:

  1. 判断为什么发生编译冲突。
  2. 给出正确解决方案。
  3. 尝试分别调用 AB 的默认实现。

7.5 综合训练

设计一个日志接口 Logger

要求:

  1. 定义 info(String message) 默认方法。
  2. 定义 error(String message) 默认方法。
  3. 两个默认方法必须复用一个接口私有方法完成统一格式化。
  4. 定义一个接口静态方法返回 Logger 版本号。
  5. 编写 ConsoleLogger 实现接口。
  6. ConsoleLogger 不需要重新实现已有默认逻辑。
  7. 再创建一个 CustomLogger,重写其中一个默认方法。
  8. 编写测试程序比较两个实现的行为。
  9. 解释 defaultprivatestatic 在本设计中分别承担什么职责。

7.6 本章验收

不查看资料,完成以下内容:

  • [ ] 口述 Java 8 为什么引入默认方法。
  • [ ] 手写一个 default 方法。
  • [ ] 说明默认方法如何调用。
  • [ ] 重写一个默认方法。
  • [ ] 手写一个接口 static 方法。
  • [ ] 使用接口名调用静态方法。
  • [ ] 说明 Java 9 接口私有方法的作用。
  • [ ] 手写接口 private 方法。
  • [ ] 解释为什么接口私有方法不能被实现类直接调用。
  • [ ] 解决两个接口默认方法冲突。
  • [ ] 手写 A.super.method()
  • [ ] 解释类方法与接口默认方法冲突时的基本优先规则。
  • [ ] 区分抽象方法、默认方法、静态方法和私有方法。

能够做到“闭卷解释 + 闭卷手写 + 能处理冲突”,才算真正掌握本章。