内部类、Lambda 与枚举:外部类引用隐患、函数式接口与枚举单例
拆解三种「长得不一样但本质一样」的语法:非静态内部类隐式持有外部类引用(埋下内存泄漏),Lambda 必须依附函数式接口而存在,枚举用
private构造器把对象数量钉死从而实现单例。顺带厘清接口、抽象类与代理模式这几个常和它们混在一起的概念。
一、内部类:语法特殊,本质仍是类
内部类写在另一个类体内部,写法比常规类陌生,但编译后就是普通类文件(Outer$Inner.class)。关键是分两类:
-
非静态内部类(成员内部类):依赖外部类实例存在,能直接访问外部类的私有字段;
-
静态内部类(
static嵌套类):不依赖外部实例,不能访问外部实例字段。
1.1 非静态内部类持有外部类引用,会内存泄漏
非静态内部类的构造器,第一个参数就是外部类实例的引用------这是编译器偷偷塞进去的。正因如此,只要内部类对象还活着,外部类对象就绝不会被 GC 回收。
内存泄漏场景:把非静态内部类实例传给一个长生命周期的对象(如丢进线程池、放进静态集合或缓存),而外部类本应被释放。此时外部类被内部类「拽住」,占着的大对象(比如一个 10MB 的缓冲区)迟迟回收不了。
public class Outer {
private byte[] bigBuffer = new byte[1024 * 1024 * 10]; // 大对象
private String tag = "外部类实例";
public class Inner {
public void whoIsMyOuter() { System.out.println(tag); } // 直接访问外部字段
}
public static class StaticNested {
public void say() { /* 不持有外部引用,无法访问 tag */ }
}
}
修复办法:能用静态内部类就用静态内部类;若必须访问外部实例字段,考虑把内部类改成 static 嵌套类,或用 WeakReference 持有外部引用。
二、Lambda:离开函数式接口就活不了
Lambda 不是凭空存在的语法,它本质只能是某个「函数式接口」的实例 。函数式接口 = 只有一个抽象方法的接口(可用 @FunctionalInterface 强制约束,多写一个抽象方法编译就报错)。
@FunctionalInterface
interface Calculator { int calc(int a, int b); }
Calculator add = (a, b) -> a + b; // Lambda 就是 Calculator 的实例
Calculator mul = (a, b) -> a * b;
如果背后没有函数式接口,Lambda 根本无法独立存在。Java 集合 API 大量依赖它:比如 forEach 接收的 Consumer、排序的 Comparator 都是函数式接口。Lambda 只是「匿名内部类实现接口」的语法糖,让代码更短。
方法引用是另一种写法,同样要求目标是个函数式接口:list.forEach(System.out::println) 等价于 s -> System.out.println(s)。
2.1 Lambda 的变量捕获:effectively final
Lambda 体里访问外部局部变量时,该变量必须是事实不可变(effectively final) 的------即声明后没有被重新赋值。Java 8 不再强制写 final,但只要在 Lambda 内外任一处改了它,编译直接报错。这是因为 Lambda 可能在另一个线程、另一个时刻才执行,若变量可变会导致难以排查的并发问题。
int base = 10; // 没写 final,但没被重新赋值 → effectively final
Calculator addBase = (a, b) -> a + b + base; // 合法
// base = 20; // 一旦重新赋值,上面这行编译失败
这也解释了为什么匿名内部类、Lambda 常和「内部类」放在一起讲:它们都涉及对外层作用域的捕获,规则一脉相承。
三、枚举:把对象数量钉死的类
枚举用 enum 声明,和 class/interface 平级,但有个铁律:实例在定义时就全部列死,外部不能用 new 创建,只能使用已定义的常量 。比如 Cat 只允许 A、B、C 三个对象。
public enum Cat {
A("黑色", 3), B("白色", 5), C("橘色", 2);
private final String color;
private final int age;
Cat(String color, int age) { this.color = color; this.age = age; }
public void run() { /* 普通方法,照常写 */ }
public static Cat findByColor(String color) { /* 静态方法也行 */ }
}
枚举除了「对象个数受限」,其他几乎和普通类一样:能定义字段、普通方法、静态方法。唯一硬性区别是构造器默认 private ------这是为了防止外部 new。日常里星期(固定 7 个)、状态码、单例模式都特别适合用枚举。
3.1 枚举的单例本质
枚举常量是单例 :无论多少次取到 Cat.A,都是同一个内存对象(Cat.A == Cat.A 为 true)。这正是枚举实现单例模式的优雅之处------enum 自己就把构造器锁死成 private,JVM 还保证不会被反射随意创建。
Constructor<?>[] ctors = Cat.class.getDeclaredConstructors();
// 构造器签名: private Cat(String,int,String,int),确为 private
ctors[0].setAccessible(true);
ctors[0].newInstance("黑色", 99);
// 抛 IllegalArgumentException: Cannot reflectively create enum objects
可见枚举构造器是 private,且即便用反射强行 newInstance 也会被 JVM 拒绝------单例安全性比手写单例(还要防反射、防反序列化)强得多。
3.2 枚举自带工具方法与 switch
每个枚举都白送几个方法:values() 返回所有常量的数组,name() 返回常量名,ordinal() 返回声明顺序(从 0 开始)。还能用 valueOf("A") 按名字反查常量。枚举天然适合做 switch 的分支条件,比用数字常量更安全、可读:
for (Cat c : Cat.values()) {
switch (c) {
case A: System.out.println("黑猫"); break;
case B: System.out.println("白猫"); break;
default: System.out.println("其他");
}
}
枚举之间还能用 compareTo 按 ordinal 比较大小,因此状态机、策略分发、错误码枚举都是它的主场。
四、补充:接口、抽象类与代理(常和上面混在一起)
这几块在源码里常和内部类/Lambda 一起出现,一并理清。
4.1 接口为什么存在:解开单继承的锁
Java 类只能单继承 (一个类只有一个父类),否则两个父类若有同名方法(如都叫 run),调用时无法抉择。接口只声明方法、不写实现,是「纯规范」,因此一个类可以实现多个接口,获得多重能力(同时是「画家」和「歌手」)。
接口比抽象类更纯粹:当一类里全是抽象方法时,本质上就是接口。接口方法默认 public abstract(不写也成立);接口里的变量默认 public static final(即常量,必须声明时赋值,因为接口本身不能 new、没有对象级存储)。Java 8 起接口还能用 default 关键字写带实现的方法,但实战用得不多。
抽象类则更灵活:可同时含抽象方法和具体方法;非抽象子类必须实现所有抽象方法,否则自己也得声明为 abstract 把锅往下传。
接口 vs 抽象类 速记:接口支持多继承(interface C extends A, B)和多实现(一个类 implements 多个接口),抽象类只能单继承;接口变量是常量,抽象类字段和普通类一样;两者都能实现多态(接口引用 / 父类引用指向实现类对象)。
4.2 代理模式:给目标对象「加戏」
代理模式用一个代理对象包裹目标对象,在调用目标方法前后插入额外逻辑(日志、权限、事务),实现功能增强与解耦。静态代理是手动写死代理类(编译期就定好代理谁),动态代理在运行期生成(更灵活)。
JDK 动态代理要求被代理类必须实现接口 ,核心三步:实现 InvocationHandler 在 invoke() 里写增强逻辑;用 Proxy.newProxyInstance(类加载器, 接口数组, handler) 生成代理对象;invoke(proxy, method, args) 里通过反射 method.invoke(真实对象, args) 执行原方法。注意代理类自身已继承了 Proxy,Java 单继承限制让它不能再继承别的类,所以动态代理必须基于接口而非类------这也解释了为什么接口在框架里无处不在。
五、本文实测代码与输出
工程位置:csdn-blog/demos/java-advanced/,包 cn.ts.adv.inner、cn.ts.adv.lambda、cn.ts.adv.enums,JDK 8 编译运行。
Outer 用反射证明内部类持有外部引用:
Outer outer = new Outer();
Inner inner = outer.new Inner();
inner.whoIsMyOuter();
// 反射查看构造器参数
for (Constructor<?> c : Inner.class.getDeclaredConstructors())
System.out.println(Arrays.toString(c.getParameterTypes()));
for (Constructor<?> c : StaticNested.class.getDeclaredConstructors())
System.out.println(Arrays.toString(c.getParameterTypes()));
LambdaDemo:
Calculator add = (a, b) -> a + b;
list.forEach(s -> System.out.println("forEach 元素: " + s));
list.sort((x, y) -> x.compareTo(y));
list.forEach(System.out::println);
EnumDemo:
Cat c1 = Cat.A, c2 = Cat.A;
System.out.println(c1 == c2); // true,单例
System.out.println(Arrays.toString(Cat.values()));
Constructor<?>[] ctors = Cat.class.getDeclaredConstructors();
System.out.println(Modifier.isPrivate(ctors[0].getModifiers())); // true
实测输出:
内部类能直接访问外部类字段 tag=「外部类实例」
---- 反射查看构造器参数,证明内部类持有外部类引用 ----
Inner 构造器参数: [class cn.ts.adv.inner.Outer]
StaticNested 构造器参数: []
Lambda 加法 3+5 = 8
Lambda 乘法 3*5 = 15
forEach 元素: banana
forEach 元素: apple
forEach 元素: cherry
排序后: [apple, banana, cherry]
方法引用输出:
apple
banana
cherry
Cat.A == Cat.A(单例): true
Cat.A 的 ordinal: 0
Cat.A 的 name: A
所有枚举值: [A, B, C]
小猫「A」颜色「黑色」年龄「3」正在跑
通过字段查找白色猫: B
Cat 枚举构造器签名: private cn.ts.adv.enums.Cat(java.lang.String,int,java.lang.String,int),是 private: true
通过反射 newInstance 枚举会被拒绝: java.lang.IllegalArgumentException: Cannot reflectively create enum objects
Inner 的构造器参数列表里清清楚楚带着 Outer------这就是它隐式持有外部类引用的铁证;而 StaticNested 的参数列表为空,证明静态嵌套类不带这层引用。枚举侧则验证了单例与 private 构造器,以及反射也无法突破枚举的单例防线。
附:关键语法 / API 速查
| 名称 | 作用 | 备注 |
|---|---|---|
| 非静态内部类 | 写在类体内部,依赖外部实例 | 构造器首个参数是外部类引用,易致内存泄漏 |
static 嵌套类 |
不依赖外部实例 | 不能访问外部实例字段 |
@FunctionalInterface |
约束接口只有一个抽象方法 | Lambda 必须依附它 |
接口::方法 |
方法引用 | 同样是函数式接口的实例 |
enum |
声明枚举 | 实例列死,外部不能 new |
枚举构造器 private |
钉死对象数量 | JVM 拒绝反射创建枚举 |
values() / name() / ordinal() |
枚举自带方法 | 遍历常量、取名字、取下标 |
| 接口多实现 / 多继承 | 解开单继承限制 | 类 implements 多接口,接口 extends 多接口 |
InvocationHandler.invoke |
动态代理增强逻辑 | Proxy.newProxyInstance 配合 |
| 动态代理基于接口 | JDK 代理要求实现接口 | 代理类已继承 Proxy,不能再继承别的类 |