04接口-lambda-内部类
- 100概述
- 201Java背景和基本结构
- 302对象与类
- 403继承与反射
- 504接口-lambda-内部类本文
- 605异常-断言-日志

第 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(),那两个接口的默认方法都会被忽略,直接用父类那版。规则连起来就是:
- 父类里的方法 > 接口的默认方法;
- 接口之间冲突 → 编译器报错,必须手写,要么改名。
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 是类,能 newtimer.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 数组:想要字典序就传字典序比较器,想要长度序就传长度序,想要倒序再换一个——数据本身一个字不用动,排法穷举不完。
Comparable | Comparator | |
|---|---|---|
| 写在哪 | 类内部,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 就是现成的例子:
@FunctionalInterfacepublic 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::printlnSystem.out::println 字面就是「println 这个方法本身」。三种形式:
| 形式 | 例子 | 等价于 |
|---|---|---|
| 对象::实例方法 | System.out::println | x -> System.out.println(x) |
| 类::静态方法 | Math::sin | x -> 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() | 生产一个 |
Runnable | void 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 的办法很简单:让实现类自己报名。
- 定接口。JDBC 定的是
java.sql.Driver。 - 实现方自己登记。每个驱动 jar 里放一个文本文件,路径是固定的:
META-INF/services/java.sql.Driver文件内容就一行,写自己的类名:
com.mysql.cj.jdbc.Driver- 调用方去读名单:
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 阶段会正面碰到。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





