04接口-lambda-内部类

6872 字
34 分钟
04接口-lambda-内部类

第 6 章 接口、lambda 表达式与内部类#

6.1 接口#

6.1.1 接口的概念#

想给一个 Employee[] 排序,用 Arrays.sort 就行。但通用排序代码怎么知道两个 Employee 谁在前?它是 JDK 里的通用代码,不可能认识 Employee。

办法是约定一个方法名:想被我排序的类,必须提供一个叫 compareTo 的方法。这个约定写成类型,就是接口:

public interface Comparable<T> {
int compareTo(T other); // 只有签名,没有实现
}

尖括号里的 <T> 是泛型(Ch8 才正式讲)。这里只记一句:Comparable<Employee> 就是「能和 Employee 比」。<T> 具体怎么回事,现在不用管。

类用 implements 兑现这个约定,把方法真正写出来:

public class Employee implements Comparable<Employee> {
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
}

Arrays.sort 只认「你能被比较」这件事,不关心你比的是薪水还是姓名——接口规定「有什么能力」,不规定「怎么做」。

为什么非得是个独立接口,不能就在类里写个 compareTo? 因为 Arrays.sort 是 JDK 写的——它写的时候,你的 Employee 还没出生。它没法「看」你的类,只能检查你是什么类型:

Arrays.sort(Comparable[] a) // 想进来,得是 Comparable 这个类型

你在类里写个 compareTo,只有你自己调得到;Arrays.sort 不认识你的类,也不会去翻你有哪些方法——真要它去找,只能靠反射去猜方法名(6.5 代理那套),又慢又脆。

换成接口,约定就从「口头说好」变成一条编译期就查得出的规则:没实现?编译不过。这也是它比「方法名叫 compareTo」那句口头约定强的地方。

说得再土一点:接口是互不相识的双方共用的插座。Arrays.sort(JDK,早写完了)和你的 Employee(还没写)互相不认识,但都认 Comparable 这个标准。所以 Integer、String、Employee 彼此毫无血缘,照样能被同一个 Arrays.sort 排。

vs C:C 没有这个机制。想让通用排序函数知道你「能被比较」,只能塞一个函数指针进去,每个调用点自己传「怎么比」。关于回调,6.1.7 会再讲。

6.1.2 接口的属性#

几条硬性规定:

  • 接口不是类,不能 new。new Comparable() 编译不过。
  • 接口里的方法默认 public abstract(公开 + 只有签名);字段默认 public static final(常量)。
public interface Comparable<T> {
int compareTo(T other); // 等价于 public abstract int compareTo(T other);
double PI = 3.14; // 等价于 public static final double PI = 3.14;
}
  • 接口类型的变量可以声明,指向实现类对象。这就是 5.1 的那个多态——只是「父类」换成了「接口」:接口变量只能调接口里有的方法;真正执行的则是实现类写的那个方法——毕竟接口里那个只有签名,没实现可跑。
Comparable<Employee> e = new Employee("张三", 50000);
Employee boss = new Employee("李四", 80000);
e.compareTo(boss); // ✅ 能调:compareTo 在 Comparable 里
e.getName(); // ❌ 编译不过:getName 不在 Comparable 里
  • 一个类可以 implements 多个接口,逗号隔开:
public class Employee implements Cloneable, Comparable<Employee> { ... }

6.1.3 接口与抽象类#

5.6 的抽象类也能「只给方法签名、不给实现」,看着和接口是一回事。区别只有两条。

第一条:一个类只能有一个父类,接口能有多个。

extends 后面只能写一个类:

class Employee extends Person { } // ✅
class Employee extends Person, Comparable { } // ❌ 编译错误

想再「多一样能力」,只能放到 implements 后面。那里可以写一串,用逗号隔开:

class Employee extends Person implements Comparable<Employee>, Cloneable { }

所以一个类要同时具备好几样能力,只能靠接口——这就是它存在的头号理由。

第二条:抽象类里有「现成的东西」,接口里基本没有。

  • 抽象类能有字段,用来存数据;接口的字段只能是常量(public static final)。
  • 抽象类能有普通方法(已经写好的实现);接口里的方法默认全是空签名(能带实现的 default 方法是 6.1.5 才加的)。

两条合起来,选法就一句:

  • 想给类多挂几样能力 → 用接口;
  • 想给子类留一份现成的状态和代码 → 用抽象类。
抽象类接口
一个类能挂几个1 个任意多个
字段能存数据只能是常量
方法空签名和现成实现都有默认全是空签名

6.1.4 静态和私有方法#

Java 8 起,接口可以写 static 方法。 它就是挂在接口名下的普通工具方法:

public interface Greeting {
static String hello() { return "你好"; } // 普通静态方法,直接给实现
}

调用时写接口名:Greeting.hello()。Comparator 上那批 comparing / naturalOrder 也是这样挂上去的——6.2.8 会用到。

Java 9 起,接口还能写 private 方法:

public interface Greeting {
default void bye() { log(); } // 默认方法,6.1.5 讲
private void log() { System.out.println("called"); } // 只给接口内部用
}

理由很实际:接口里几个方法想共用一小段代码,又不想把这段代码变成接口的 API——实现类不该看到它。私有方法只服务于接口里其他方法。

6.1.5 默认方法#

问题在演进:接口已经发布,有几十个实现类。现在想给它加一个方法——直接加抽象方法,那几十个类都没实现,全部编译失败。加不了。

default 解决它:直接在接口里给出实现,实现类可重写、也可不管:

public interface Named {
String getName(); // 老规矩:抽象方法,必须实现
default void greet() { // 新加:给了实现
System.out.println("你好," + getName());
}
}

老实现类只写了 getName,greet 白得一个默认实现,一行不用改就编译通过。

现实里的例子是集合:Java 8 给 Collection 加 stream() 时,用的就是默认方法。

6.1.6 解决默认方法冲突#

默认方法带来一个新麻烦:一个类实现两个接口,两个接口的默认方法同名同参数,用谁的?

public interface A { default void f() { System.out.println("A"); } }
public interface B { default void f() { System.out.println("B"); } }
public class C implements A, B { } // ❌ 编译错误:默认方法冲突

编译器不给猜,要求必须自己重写,用 接口名.super 明确挑一个:

public class C implements A, B {
@Override
public void f() { A.super.f(); } // 显式选 A 那一版
}

接口名.super——这是唯一能写 super 的接口场景。

还有一条优先级要记:父类里的方法永远赢。如果 C 的父类里有一个具体的 f(),那两个接口的默认方法都会被忽略,直接用父类那版。规则连起来就是:

  1. 父类里的方法 > 接口的默认方法;
  2. 接口之间冲突 → 编译器报错,必须手写,要么改名。

6.1.7 接口与回调#

回调 = 把「要执行的代码」交给别人,时机到了再调回来。 注意它和接口没有必然联系。

JDK 的 javax.swing.Timer 是干净的例子:它每隔一段时间触发一次。触发时干什么,它不知道——它只要求你交一个实现了 ActionListener 的对象:

public interface ActionListener {
void actionPerformed(ActionEvent event); // 只有一个方法
}

你交的代码是被「叫回来」执行的一方,控制权在 Timer 手上:它决定什么时候调、调多少次。

class MyListener implements ActionListener { // ① 先写个实现类
public void actionPerformed(ActionEvent e) {
System.out.println("到点了");
}
}
ActionListener listener = new MyListener(); // ② 接口变量 → 实现类对象
var timer = new Timer(1000, listener); // ③ Timer 是类,能 new
timer.start(); // 之后每秒它调一次 actionPerformed

注意这里「接口」和「类」的分工:new 的永远是类(Timer、MyListener);ActionListener 只当类型用——变量、参数的位置都能写它,但它自己不能 new。所以上面既没有、也没法 new 一个接口。

注意 ActionListener 只有一个方法。下一节的 lambda 就是为缩写这种接口准备的。

vs C:C 用函数指针做回调,把函数地址传出去即可。Java 早年没有函数指针,只能拿「只有一个方法的接口」顶替:先把函数包进一个对象,再把这个对象传出去。

6.1.8 Comparator#

Comparator<T> 是外部比较器:compare(a, b) 返回值小于 0 / 等于 0 / 大于 0,分别表示 a < b / a == b / a > b。

public interface Comparator<T> {
int compare(T a, T b);
}

6.1.1 的 Arrays.sort 要求元素自己能比(Comparable)。Comparator 反过来:比法由外部给。Arrays.sort 和 List.sort 都有收 Comparator 的重载:

Arrays.sort(people, new LengthComparator()); // 按名字长度排
class LengthComparator implements Comparator<String> {
public int compare(String a, String b) {
return a.length() - b.length();
}
}

好处是把「怎么比」和「数据」分开。同一个 String 数组:想要字典序就传字典序比较器,想要长度序就传长度序,想要倒序再换一个——数据本身一个字不用动,排法穷举不完。

ComparableComparator
写在哪类内部,compareTo类外部,compare
一个类能有几种排法1 种(写死)想要几种有几几种
用处这是「我的自然顺序」这是一次性临时排法

6.1.9 对象克隆#

clone() 定义在 Object 上,是 protected 的。要用它,类得先声明 implements Cloneable:

public class Employee implements Cloneable {
@Override
public Employee clone() throws CloneNotSupportedException {
return (Employee) super.clone();
}
}

Cloneable 是个标记接口——里面一个方法都没有,只用来「打标记」,告诉 JVM「这个类允许克隆」。没打这个标记调 clone(),抛 CloneNotSupportedException。

关键在 super.clone() 给的是什么:

浅拷贝——字段逐个复制,但引用字段只复制引用:副本和原对象指向同一个内部对象。谁改了它,两边一起变(4.2 讲过,对象变量里存的是地址,不是对象本身)。

要真正独立,得深拷贝——把可变的引用字段也克隆一份,接进新对象:

public class Employee implements Cloneable {
private String name; // 不可变:共享无所谓
private LocalDate hireDay; // 不可变:共享无所谓
private Date birthDay; // 可变:共享会出问题,必须深拷贝
@Override
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone(); // 浅:birthDay 只复制了引用
cloned.birthDay = (Date) birthDay.clone(); // 深:这句才是关键
return cloned;
}
}

一句话版clone:clone() 只复制字段的值;引用字段的值是地址,所以副本和原对象指向同一个内层对象——要让副本彻底独立,就得把内层对象也各自复制一份,然后把默认的Object类强转成具体类。

vs C:这和 memcpy 是同一个坑——memcpy 也逐字节浅拷贝,指针字段复制过去的还是同一个地址。想复制指向的内容,只能自己手工再 malloc + memcpy 一遍,和这里的深拷贝是一个意思。

关于 <T>(Ch8 正式讲):定义时随便起个名当占位,使用时必须填真类型。 填进去之后,接口里所有用这个名字的地方一起替换成那个类型。

6.2 lambda 表达式#

先记一句话:lambda 必须和接口搭配使用。 它自己没有类型,必须由一个函数式接口(只有一个抽象方法的接口)接住——下面每一处都会用到这一点。

注意这里说的是函数式接口、函数式接口、函数式接口!!! 可以用4种方式赋值(独立实现类对象、匿名内部类、Lambda、方法引用),普通接口只能2种方式(前两种),详见04案例篇文章。

6.2.1 为什么引入 lambda 表达式#

回到 6.1.7 的回调。给数组按名字长度排序,得先造一个比较器对象:

Arrays.sort(words, new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});

六行,真正的逻辑只有 a.length() - b.length() 这一行。其余全是「起个类、写个方法签名、new 出来」的样板——这一类接口(只有一个抽象方法)实在太像函数了,Java 8 干脆给了个函数字面量,lambda:

Arrays.sort(words, (a, b) -> a.length() - b.length());

这一行看不见接口名——它藏在方法签名里:Arrays.sort 的第二个参数要的就是 Comparator。lambda 的目标类型就是从这儿来的。

lambda 就是「方法的字面量」:一段代码,没有名字、没有所属的类,直接当成值传出去。

6.2.2 lambda 表达式的语法#

(参数) -> 表达式
(参数) -> { 语句; ... }

lambda 本身没有类型,必须由左边某个函数式接口接住。所以下面都是完整写法:

Comparator<String> c1 = (String a, String b) -> a.length() - b.length();
// 完整写法
Comparator<String> c2 = (a, b) -> a.length() - b.length();
// 参数类型可省
Function<Integer, Integer> f = x -> x + 1;
// 单个参数,括号也能省
Supplier<Integer> s1 = () -> 42;
// 没有参数
Supplier<Integer> s2 = () -> {
// 多条语句要花括号 + return
System.out.println("go");
return 42;
};

Function / Supplier 是 JDK 自带的函数式接口(java.util.function),6.2.7 会列全。这里先当它们是「收一个参数」和「不收参数」的接口。

参数类型能省掉,是因为编译器能从上下文推断出你要实现哪个方法——6.2.3 说清这件事。

6.2.3 函数式接口#

lambda 只能装进函数式接口:抽象方法恰好只有一个的接口。

注意别读成「接口只能有一个方法」。 接口可以有任意多个方法,函数式接口只数抽象方法——default / static / private 方法都有实现,不算数。Comparator 就是现成的例子:

@FunctionalInterface
public interface Comparator<T> {
int compare(T a, T b); // ← 唯一的抽象方法,算数
default Comparator<T> reversed() { ... } // 有实现,不算
}

除了 compare,它还有 reversed(6.2.8 会用到)这类有实现的方法——都不影响函数式接口的身份。

为什么必须「只有一个」:lambda 只写了一段代码,编译器得知道它实现的是哪一个方法——接口里只有一个抽象方法,才没有歧义。

@FunctionalInterface 可写可不写,作用同 5.1 的 @Override:写了编译器替你检查「是不是真的只有一个抽象方法」,不满足就报错。不加也不影响运行。

6.2.4 方法引用#

如果 lambda 写出来是「把参数原样转交给某个方法」,还能再简写,用 :::

x -> System.out.println(x) // 等价于下面这个
System.out::println

System.out::println 字面就是「println 这个方法本身」。三种形式:

形式例子等价于
对象::实例方法System.out::printlnx -> System.out.println(x)
类::静态方法Math::sinx -> Math.sin(x)
类::实例方法String::compareToIgnoreCase(a, b) -> a.compareToIgnoreCase(b)

第三行要解释:String::compareToIgnoreCase 是实例方法,但它没有调用者——省略的调用者从参数里补:第一个参数 a 当接收者,剩下的 b 当实参。

三种写法,干的是同一件事。 从最啰嗦到最简洁:

① 写一个类(最啰嗦)

interface Printer { void print(String s); }
class ConsolePrinter implements Printer { // 专门造一个类
public void print(String s) {
System.out.println(s);
}
}
public class Demo1 {
public static void main(String[] args) {
Printer p = new ConsolePrinter();
p.print("hello"); // hello
}
}

② lambda(类不要了)

interface Printer { void print(String s); }
public class Demo2 {
public static void main(String[] args) {
Printer p = s -> System.out.println(s); // 只剩参数 + 方法体
p.print("hello"); // hello
}
}

③ 方法引用(参数也省了)

interface Printer { void print(String s); }
public class Demo3 {
public static void main(String[] args) {
Printer p = System.out::println; // 连参数名都不用写
p.print("hello"); // hello
}
}

三个版本输出都是 hello。方法引用能成立的前提只有一条:lambda 里除了「把参数原样转交给某个方法」,什么都没干。

6.2.5 构造器引用#

类名::new,把「造对象」这件事本身交出去。同样得有个接口接着:

interface PointFactory { Point create(int x, int y); } // 收两个参数,返回 Point
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
public class Demo {
public static void main(String[] args) {
PointFactory f1 = (x, y) -> new Point(x, y); // lambda 写法
PointFactory f2 = Point::new; // 方法引用,效果完全一样
Point p = f2.create(3, 4);
System.out.println(p.x + ", " + p.y); // 3, 4
}
}

一个类可能有多个构造器,Point::new 具体挑哪个,由目标接口的方法签名决定——接口方法收两个参数,就挑两参的那个构造器。

6.2.6 变量作用域#

lambda 能读外层局部变量:

int count = 10;
Runnable r = () -> System.out.println(count); // ✅ 读,合法

但那个变量必须「初始化后不再改」(事实最终,effectively final)。一旦改过就编译不过:

int count = 0;
Runnable r = () -> count++; // ❌ 编译错误:count 必须是事实最终

为什么禁止:lambda 捕获的是那一刻的副本,不是外层变量本身。副本里 count++ 改的是副本,外层的 count 纹丝不动——既然改了也没用,语言索性禁止,免得你以为改了。

所以 lambda 里不能改外层变量。要累计结果,得换一个可变的载体(比如对象的某个字段、数组元素)。

顺带:lambda 里的 this 指的是外围对象本身,不是这个 lambda。

6.2.7 处理 lambda 表达式#

这节讲一件事:你自己写的方法,怎么接收 lambda。

lambda 没有自己的类型,只能落在函数式接口上。所以「接收 lambda 的方法」,参数就写成函数式接口:

public class Demo {
static void repeat(int times, Runnable task) { // 参数是函数式接口
for (int i = 0; i < times; i++) task.run();
}
public static void main(String[] args) {
repeat(3, () -> System.out.println("hi")); // 传 lambda 进去:输出 3 行 hi
}
}

JDK 已经备好一批常用的(java.util.function),不用每次自己造:

接口方法含义
Predicate<T>boolean test(T)判断
Consumer<T>void accept(T)吃一个,不返回
Function<T,R>R apply(T)转换
Supplier<T>T get()生产一个
Runnablevoid run()无参无返回

(这些接口卷 II 讲 Stream API 时会大量出现,那时再细看。)

6.2.8 再谈 Comparator#

Comparator 上有一批静态工厂方法,配方法引用把排序规则压成一行:

Arrays.sort(people, Comparator.comparing(Employee::getName)); // 按名字排

而且能接着链下去:

people.sort(Comparator.comparing(Employee::getName) // 先按名字
.reversed() // 倒序
.thenComparing(Employee::getSalary)); // 再按薪水

.reversed() 和 .thenComparing() 都是 Comparator 上的方法,返回的还是 Comparator,所以能一环扣一环——这就是链式调用成立的前提:每个方法都返回同类型的对象。

(6.1.4 留的问题,现在看懂不亏)签名里那个 <T> Comparator.comparing 的签名是 static <T> Comparator<T> comparing(...)。static 和返回类型中间多出来的 <T> 叫泛型方法:方法自己声明一个类型参数,作用范围只在这个方法里。

它跟接口头上 interface Comparator<T> 那个 T 毫无关系,只是恰好重名——接口那个跟着对象走,而 static 方法没有对象,够不着它,只能自己再造一个。验证办法:把它改名叫 U(static <U> Comparator<U> comparing(...)),一模一样能用;真是承接的话,改名早报错了。

还是看不懂就跳过——Ch8 会正式讲,不耽误往下读。

6.3 内部类#

6.3.1 使用内部类访问对象状态#

内部类最直接的价值:能访问外围类的私有字段。

public class TalkingClock {
private int interval;
private class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println(interval); // 直接读外围类的 private 字段
}
}
}

TimePrinter 能读 interval,别的类读不了。不违反封装——private 对「写在外围类代码里的成员」全都放行,内部类就在外围类的代码里。

(ActionListener + Timer 就是 6.1.7 的回调:外围类把内部类对象交给定时器,到点了回调 actionPerformed。)

6.3.2 内部类的特殊语法规则#

内部类对象里隐含着一个指向外围对象的引用——这就是它能读 interval 的物理原因。

public class Outer {
private int interval = 1000;
class Inner {
void demo() {
Outer outer = Outer.this; // ① 外围对象:就是个 Outer 对象,能存进变量
System.out.println(outer.interval);
}
}
}
public class Demo {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
// ② 外部造内部类;③ 类型名要从外面写全
inner.demo();
}
}

三样不是一类东西:Outer.this 是对象引用(① 能赋给 Outer 变量);outer.new Inner() 是创建对象(② 先有外围对象才能造);Outer.Inner 是类型名(③ 从类外面引用必须写全,类内部写 Inner 就够)。

容易混的是 Outer.class——那个才代表「这个类」,是个 Class 对象(5.9 讲过)。Outer.this 跟它完全不是一回事。

编译产物:内部类被编译成独立的 class 文件,文件名是 Outer$Inner.class。

6.3.3 内部类是否有用、必要和安全#

有用:能访问外围私有状态(6.3.1);能把一个类藏起来——只服务外围那一个类,不占用类的成员列表,外界根本看不到它。

必要:不是非它不可。同样的事用「普通类 + 构造器传引用」也能做,只是啰嗦。

安全:编译器会生成 access$0 这类合成方法,让内部类绕过 private 检查去读外围字段。这说明 private 是编译期检查,不是运行时铁幕——5.9 的反射能开同一道门。

今天内部类的用途已经大幅收窄:匿名内部类那部分基本被 lambda 取代了(6.2.1),剩下的主要就是「需要直接访问外围私有状态」这一个场景。

6.3.4 局部内部类#

类也能定义在方法体内,作用域只在这个方法里:

public void start() {
class TimePrinter implements ActionListener { // 只在本方法内可见
public void actionPerformed(ActionEvent e) { ... }
}
var t = new TimePrinter();
}

出了方法没人认识它。

6.3.5 由外部方法访问变量#

局部内部类能捕获外层方法的局部变量,规则和 lambda 完全一样:读可以,改不行,且必须是事实最终(6.2.6)。

public void start(int interval) { // 方法参数也是「外层局部变量」
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println(interval); // ✅ 读,合法
}
}
}

原因也一样:捕获的是副本,改了也没用。

6.3.6 匿名内部类#

new 接口名() { ... }:没有名字、当场创建、只用一次。

var t = new ActionListener() { // 不写类名,直接给实现
public void actionPerformed(ActionEvent e) {
System.out.println("到点了");
}
};

省掉了「先起个名字、再 new」两步,代价是这个实现只能在创建它的地方用这一次。

lambda 出现前,这是回调的标准写法(6.2.1 那段六行代码就是匿名内部类)。现在单方法接口的匿名内部类基本都能换成 lambda;接口里有多个方法要一起实现时,还得用匿名内部类。

6.3.7 静态内部类#

加了 static 的内部类不绑定外围类实例,等于一个普通类、只是嵌套放在别人里面:

public class Outer {
private static int counter;
private static class Helper { // 没有外围对象引用
void bump() { counter++; } // 只能碰 static 成员
}
}

两种内部类的区别只有一条:非静态内部类带着「外围对象引用」(6.3.2 那个隐含引用),静态内部类没有。由此派生两点:静态内部类创建时不需要外围对象,也访问不到外围类的实例字段。

public class Outer {
class A { } // 非静态内部类
static class B { } // 静态内部类
}
Outer.B b = new Outer.B(); // 静态:直接 new,不需要外围对象
Outer o = new Outer(); // 非静态:必须先有一个 Outer 对象
Outer.A a = o.new A();

static 只管它自己那一层,不往下传。 外层是静态、内层不是,内层照样要实例:

public class Top {
static class Mid { // 静态:new 它不需要 Top 实例
class Inner { } // 非静态:仍然需要 Mid 实例
}
}
Top.Mid m = new Top.Mid(); // 直接 new,到此为止
Top.Mid.Inner i = m.new Inner(); // Inner 不是静态的,还得要 m

所以准确的说法是:每个非静态内部类,都要依附「它紧邻的那一层」的实例——不是最外层。

需要「按功能打包、又不想单独开文件」时,首选静态内部类——少一个隐式引用,语义最简单。

6.4 服务加载器#

场景:你定了一个接口,别人写了很多实现,散在各处。你想把它们全部找出来用,但你不知道它们叫什么名字。

JDBC 就是这种情况。连数据库要靠「驱动」(一个 jar 包,里面是某个数据库「怎么连」的 Java 代码),而驱动有 MySQL 的、Oracle 的、PostgreSQL 的……你的代码不可能把每种驱动的类名都写一遍——写了 MySQL,换 Oracle 就得改代码、重新编译。

ServiceLoader 的办法很简单:让实现类自己报名。

  1. 定接口。JDBC 定的是 java.sql.Driver。
  2. 实现方自己登记。每个驱动 jar 里放一个文本文件,路径是固定的:
META-INF/services/java.sql.Driver

文件内容就一行,写自己的类名:

com.mysql.cj.jdbc.Driver
  1. 调用方去读名单:
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
for (Driver d : loader) {
// 找到几个就遍历几个,一个实现类的名字都不用写
}

就这么回事:一个接口 + 一个固定路径的文本文件,运行时就能把所有实现找出来。

这个机制叫 SPI(Service Provider Interface)。JDBC 靠它工作,所以换数据库只需要换 jar,Java 代码一行不用改。

6.5 代理#

代理 = 你以为在调真的,其实中间隔了一层。

找人办事,对方门口有个秘书:你说「我要办 X」,秘书先记下来(可能还加一句「已登记」),再进去转达给本人。——秘书就是代理。

代码里也一样:你拿到一个对象,调它的方法;实际上它是个假对象,方法调用先被转交给你自己写的一段代码,那段代码干完自己的活,再去调真对象。

6.5.1 什么时候用#

日志、监控、权限检查这类事,每个方法都要有,但又不该混进业务代码里。

比如一个接口有 20 个方法,你想每个方法被调用时都打一行日志——总不能 20 个方法里各写一遍吧?代理能统一插在中间:20 个方法全走同一个入口,你在那个入口写一次就够了。

6.5.2 造一个代理#

先有接口和真实现:

interface Greeter {
String greet(String name);
}
class RealGreeter implements Greeter {
public String greet(String name) { return "你好," + name; }
}

Proxy.newProxyInstance(...) 替你把这个假类现造出来,你要写的只有「方法被调到时干什么」:

class LogHandler implements InvocationHandler {
private final Greeter target; // 包着真实现
LogHandler(Greeter target) { this.target = target; }
//构造器
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用:" + method.getName());
// ① 截住,先干自己的活
return method.invoke(target, args);
// ② 再转给真实现
}
}

然后用它造代理:

Greeter real = new RealGreeter();
Greeter proxy = (Greeter) Proxy.newProxyInstance(
Greeter.class.getClassLoader(), // 固定写法
new Class[]{ Greeter.class }, // 代理要实现的接口
new LogHandler(real) // 方法被调到时跑它
);
System.out.println(proxy.greet("张三"));

输出:

调用:greet
你好,张三

这段里三样东西现在不用懂,各有去处:

看不懂的在哪讲
Method、method.invoke(...)5.9 反射——认得即可
Greeter.class.getClassLoader()JVM(类加载那一篇)
Proxy、InvocationHandler就是本节

看不懂不影响往下读。记住一句就够:代理是「中间站一个人,替你转达」。

6.5.3 代理类的特性#

  • 代理类是运行时生成的,不是编译出来的,硬盘上没有对应源码或 class 文件;
  • 它继承 Proxy,类名是 $Proxy0 这类;
  • 只能代理接口,不能代理类。原因就是它已经 extends Proxy 了——Java 单继承,没法再成为你那个类的子类;而「实现接口」不受数量限制,所以代理接口没问题;
  • 代理类里每个接口方法都转给 handler.invoke。

这就是 Spring AOP 的底层机制之一:接口用 JDK 动态代理,类用 CGLIB 生成子类。SSM 阶段会正面碰到。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
04接口-lambda-内部类
https://hecloud.top/posts/tech/java-se/04接口-lambda-内部类/
作者
贺小云
发布于
2026-10-16
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
贺小云
热爱技术与折腾的博客 Dancing with uncertainty. 与不确定性共舞
公告
出于成本考虑,贺云已全面转型Serverless架构
分类
标签
碎碎念
站点统计
文章
91
分类
5
标签
44
总字数
103,192
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.16.5
文章许可
CC BY-NC-SA 4.0