ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java内部类全解析:分类、原理与实战

Java内部类全解析:分类、原理与实战 内部类这个话题凡是写过 Java 的几乎没有绕开过。但我做技术面试这几年能真正把内部类讲清楚的候选人真的不多——多数人知道有大体分类能背出语法可真到用的时候照样懵什么时候该选静态内部类成员内部类凭什么能直接访问外部类的私有字段局部变量被内部类引用一下为什么就非得加 final答案背后全是设计逻辑靠死记硬背撑不过去。所以这篇我用一线写代码的视角把内部类的分类、底层原理、实战场景和坑一次性理清楚。适合正在学 Java 的初学者也适合准备跳槽面试或者想硬啃 JDK 源码的进阶开发者。看完之后你再去翻 HashMap 或 ArrayList 的源码会发现曾经让你头大的内部类其实全是套路。1. 内部类到底在解决什么问题1.1 从一次真实改需求说起我之前做一个订单系统的价格计算模块需求是这样的一个订单可能会有多件商品每件商品有自己的优惠策略比如满减、折扣、会员价。这些优惠策略只服务于价格计算这个场景不提供给订单、库存、用户等其他模块使用。最初我把优惠策略定义成了独立的顶层类结果包结构变成了这样PriceCalculator、DiscountStrategy、FullReductionStrategy、MemberPriceStrategy……每个类一个文件看起来挺规范但用起来很别扭。别的同事在 IDE 里搜索类名的时候总会把这些策略类搜出来还以为是通用能力结果引用完才发现耦合特别重。后来我重构了一下把优惠策略统统收敛到PriceCalculator内部作为它的私有内部类。外界的感知立刻变得清晰你只看到一个PriceCalculator优惠逻辑被牢牢关在门里外部根本不需要知道它内部还分了哪几种策略。这个例子很朴素但它说明了内部类的第一个价值表达“从属关系”。一个类只服务于另一个类时塞进对方肚子里比扔在包外面更符合人的直觉。类似的情况还有很多。比如说一个通用的EventManager它向外抛出的Event对象只在事件流里传递没有任何业务类会主动 new 一个裸Event。把Event定义为EventManager的内部类从语义上就锁死了“事件属于事件管理器”这个关系代码自解释能力立刻提升。读这种代码的人不需要再去包结构里找Event到底是干什么的看一眼EventManager.Event就全明白了。1.2 内部类的四大优势站在工程角度我一般会把内部类的价值归纳成四句话面试和写代码时心里都默念这一套。第一是封装性。内部类加上 private 修饰之后外部完全不可见只通过外部类这个门面来交互。JDK 里的ThreadLocal.ThreadLocalMap就是典型ThreadLocalMap是ThreadLocal的私有静态内部类外面根本不需要知道每个线程的变量副本到底用什么数据结构存这层细节被完整地藏在了ThreadLocal肚子里。第二是紧密协作。非静态内部类天然持有外部类对象的引用可以直接访问外部类的私有字段和方法。这个东西用“组合”或者“传参”也能模拟但要写一堆 getter、setter 和样板代码内部类直接把协作成本降到了零。它表达的关系比普通组合更强内部类不仅属于外部类而且在运行时就是外部对象的一部分。第三是代码组织。相关类放在一起比撒在包目录里更容易维护。尤其是一些辅助实现类、迭代器类、节点类它们通常不独立存在作为内部类能明显减少顶层类的数量也让代码浏览路径变短。比如你要实现一个链表Node节点类到底是放在外部还是放在链表类里面放在里面管理链表头尾的逻辑和节点本身挨在一起读起来流畅得多。第四是回调和事件处理。在 Java 8 之前匿名内部类是 Java 实现回调的最主流武器即使现在只要回调需要重写多个方法、或者需要持有自己的状态匿名内部类依然比 Lambda 更合适。后面实战部分我会用一个具体的例子展示为什么 Lambda 不能完全替代匿名内部类。2. 内部类分类全解析五种形态一次说清网络上搜“内部类分类”标准答案通常是一张图加四类成员内部类、静态内部类、局部内部类、匿名内部类。我把这四类都拆开讲最后再补一个容易被忽略的冷门变体凑成完整的五种形态。2.1 成员内部类绑死外部实例的“贴身伙计”成员内部类是非静态的定义在外部类的成员位置与字段、方法平级。它最大的特征是必须依附于一个外部类实例存在。在外部类外面想创建它语法是这样的Outer outer new Outer(); Outer.Inner inner outer.new Inner();注意那个outer.new Inner()这个写法很多人刚接触时会觉得奇怪但它恰恰把“内部类属于哪个外部对象”表达得清清楚楚。因为Inner会在创建时把outer的引用保存下来之后Inner里的代码可以无条件访问outer的任何成员包括私有成员。这里有个很容易忽略的细节成员内部类里其实藏着一个指向外部对象的字段名字叫this$0你在 IDE 的调试窗口里能看到它。成员内部类里访问外部类成员还有一个细节如果外部类和内部类有同名字段内部类内用Outer.this.field显式访问外部类字段。这个语法我在面试里会问能答上来说明作者确实写过而不只是背过概念。另外成员内部类不能有静态成员原因是它在概念上绑定的是“一个具体对象”而不是“一个类”所以它没有独立的类级状态。JDK 16 之后这条限制有所放开但你在生产代码里依然不应该依赖这个特性。2.2 静态内部类不需要外部实例的“独立部件”静态内部类用static修饰语法上看起来只是“把类放在另一个类里面”实际效果是它不持有外部类对象的引用行为上更接近普通类只是命名空间被收拢到了外部类里。创建方式是new Outer.StaticInner()不需要先创建Outer实例。静态内部类适合什么场景第一作为数据载体典型的就是 Builder 模式里的Builder类第二作为数据结构节点比如HashMap.Node第三作为复杂组件的私有实现比如ThreadLocal.ThreadLocalMap。这些类虽然从语义上属于外部类但逻辑上并不需要访问外部类的实例字段用 static 就是为了把这种独立性明确说出来。有一个常被搞混的点静态内部类虽然不依赖外部实例但依然可以访问外部类的private static成员。它和外部类之间的关系是“命名空间上的从属 静态层面的互相可见”而不是“对象层面的绑定”。正因为这种独立性静态内部类在内存占用和生命周期管理上要安全得多不会因为持有外部引用而导致外部对象无法回收。2.3 局部内部类只在方法里有效的“临时工”局部内部类定义在方法或者代码块内部作用域只在所在的花括号范围内。它更像一个临时工具类用完了就消失外面连类名都看不到。它同样可以访问外部类的所有成员但访问方法中的局部变量时要求该变量必须是final或者在后续代码中从未被修改过这个规则叫 effectively final。为什么局部内部类很少用但因为“内部类分类”这个问题又一定会被提到因为它是理解变量捕获机制的最好入口。面试里问“为什么局部变量需要 final”描述的对象往往就是局部内部类。你写一个方法内部的临时类本质上是把一个小的状态机封装在局部作用域里这在复杂算法的方法中偶尔能派上用场。一个容易被忽略的约束是局部内部类不能用public、private、protected或static修饰它的访问权限就是所在代码块。如果你发现某个局部内部类写得比较复杂说明它已经膨胀到应该升格成成员内部类或者独立顶层类了。判断标准很简单超过二三十行代码的类就不应该再缩在方法里。2.4 匿名内部类一次性的“影子选手”匿名内部类是最特殊的一种它没有类名定义的同时就创建对象。通常用来实现只有一个方法的接口或抽象类或者需要快速覆写某个方法。最经典的写法ExecutorService executor Executors.newSingleThreadExecutor(); executor.execute(new Runnable() { Override public void run() { System.out.println(任务执行); } });这个new Runnable() {...}就是一个匿名内部类。它实现了Runnable接口并立刻创建一个实例。匿名内部类有两点必须记住第一它只能继承一个父类或者实现一个接口想同时做两件事不行第二它没有构造器如果想在创建时传参数只能通过父类/接口的构造参数或实例初始化块{}来模拟。匿名内部类和 Lambda 的关系我在第 3 章会专门对比。在 Java 8 之后很多单方法接口的回调都可以用 Lambda 替代但匿名内部类仍然有用武之地。比如回调里要维护多个临时字段、要重载多个方法时Lambda 就无能为力必须回到匿名内部类。我写批量导入工具时就遇到过回调里需要记录处理数量、失败列表、开始时间这些状态放在匿名内部类的字段里非常顺手。2.5 接口与枚举里的嵌套类型以及和 Lambda 的关系标准教科书里会说“内部类有四种”但严格抠javadoc的定义接口里也可以定义嵌套类型枚举里也可以定义嵌套类型。接口里的嵌套类型默认是public static的枚举里的嵌套类型也遵循静态嵌套的规则。这些属于“静态内部类”的变体很多人写了很多年代码没注意过但读源码时偶尔会撞见。真正需要认真理解的是匿名内部类与 Lambda 的差异。两者表面相似底层完全不同匿名内部类编译后会生成一个独立的类文件比如Outer$1.class它运行时是真正多了一个类Lambda 则借助invokedynamic指令多数情况下不会生成额外的类文件运行时由 JVM 决定如何调用。还有一个本质区别匿名内部类里的this指向匿名类自身而 Lambda 里的this指向外部类实例。这意味着在回调里想引用当前对象自身状态时这两个的语义完全不同选错了就会出现隐蔽的 bug。3. 内部类核心机制编译产物、引用捕获与作用域3.1 编译成两个 class内部类不是语法糖那么简单虽然写代码的时候内部类看起来只是“括号里的一个小类”但编译之后每个内部类都会生成独立的.class文件。我写了一个简单例子编译完看一眼目录就明白了javac Outer.java ls # Outer.class # Outer$Inner.class # Outer$1.classOuter$Inner.class对应成员内部类InnerOuter$1.class对应匿名内部类。局部内部类则是Outer$1Inner.class这种命名数字表示“第几个局部类”。这说明内部类在 JVM 层面并不是什么神秘结构它就是两个独立的类只是命名上带上了$符号。那为什么成员内部类能访问外部私有字段这是编译器在中间做了手脚。JDK 11 之前编译器会在外部类里生成合成的access$000之类的桥接方法内部类通过调用这些合成方法来获取外部私有字段JDK 11 引入 nestmates 机制之后JVM 直接允许同一嵌套组内的类互访私有成员编译器不再需要生成这些桥接方法。理解了这一点你会明白为什么老代码里通过反射能看到access$000这类奇怪方法名。3.2 成员内部类访问外部私有成员access 方法到 nestmates这里补充一个细节。在早期 JDK 里当你写出outer.privateField这种访问时编译器不会直接让你用getField而是生成一个像static int access$000(Outer)的合成方法。这带来两个问题类的字节码里出现一堆额外方法方法数量膨胀而且反射时你会看到这些不该出现的桥接方法给框架代码带来不少困扰。JDK 11 之后JVM 底层引入了嵌套类型nestmates支持。编译器会在class文件里标注NestHost和NestMembers属性JVM 在权限检查时如果两个类属于同一个 nest就直接放行私有成员访问不需要中间人。所以现在的成员内部类编译产物比老版本干净很多。知道这个细节的人不多面试时讲出来是很加分的加分项。3.3 局部变量为什么必须 final 或 effectively final这是被问得最多的一个点。用一个生活类比来解释你在家里拍了一张合影然后把照片送给了朋友。后来你又整容了但朋友手里那张照片不会变。局部内部类/匿名内部类访问局部变量本质上是把变量的“值副本”传给了内部类对象而不是传了一个引用。如果在方法里后续修改了原变量内部类看到的副本还是旧值两边就出现不一致程序行为会变得非常诡异。为了避免这种不一致编译器规定内部类只能访问final或 effectively final 的局部变量。JDK 8 之前必须显式写finalJDK 8 开始只要变量赋值之后就没再改过编译器就自动放行这个叫 effectively final。所以你现在写回调代码时不写final也没报错不是编译器变宽松了而是它替你检查了“确实没有被改过”。我见过有人试图在 Lambda 里给局部变量赋新值编译直接报错这就是同一个机制在起作用。3.4 类加载与初始化顺序上的细节内部类在执行顺序上也有几个容易踩的点。静态内部类的静态成员只在你真正使用它时才初始化也就是说静态内部类的加载与外部类解耦。如果外部类加载时没有触发内部类的加载你的工具类启动速度可能因此更快一点。成员内部类和外部类的初始化顺序则是外部类对象先构建完才能创建成员内部类对象。因此在外部类的构造方法里直接 new 一个成员内部类、并且内部类还访问了尚未初始化完成的字段可能读到默认值。这种边界场景在单例、缓存类里出现过排查起来很隐蔽。而静态内部类因为不依赖外部实例它的static块在外部类首次主动访问它时执行往外踩坑的概率小得多。4. 真实项目中的内部类经典用法4.1 JDK 源码里的内部类HashMap、ArrayList、ThreadLocal与其满网络找内部类的“最佳实践”不如直接读 JDK 源码里的真实用法这才是最权威的教材。先说HashMap。HashMap.NodeK,V是static class它实现了Map.Entry每个键值对在哈希桶链表里的节点就是它。后面为了解决哈希冲突的极端情况JDK 8 里又加了HashMap.TreeNode也是静态内部类是红黑树节点。KeySet、EntrySet、Values这几个视图类反而是非静态成员内部类因为它们需要回看HashMap实例的方法来执行遍历、删除等操作。同一个类里静态内部类和非静态内部类搭配使用各司其职这是很好的学习样本。再说ArrayList。ArrayList.Itr是私有成员内部类实现了Iterator它在遍历时需要随时访问外层ArrayList的elementData数组和modCount字段所以被设计成非静态内部类。ListItr继承Itr并增加了向前遍历能力。可以看出非静态内部类的“隐式外部引用”在这里变成了核心优势迭代器就是“数组对象自己派出来的游标”。最后是ThreadLocal。它的ThreadLocalMap是static final class直接作为每个线程私有存储的容器ThreadLocalMap.Entry也是静态内部类继承WeakReferenceThreadLocal?。这个场景里内部类不需要访问ThreadLocal的实例状态所以用静态同时类的命名空间被收拢进ThreadLocal不会和java.util.WeakHashMap等类混淆。4.2 事件回调与监听器匿名内部类的天下做 UI 或异步任务时事件回调是最常见的场景。Swing 里的ActionListener、Android 里的OnClickListener在 Lambda 出来之前全是匿名内部类的天下button.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent e) { // 处理点击 } });这种写法的好处是回调逻辑写在使用处代码上下文连贯不用为了一个一次性监听器单独建类。缺点也明显占字符、缩进深匿名内部类如果有成员状态管理代码会很快变重。所以 Java 8 之后单方法接口通常用 Lambdabutton.addActionListener(e - handleClick(e));但如果你要实现的接口有多个抽象方法比如旧式的MouseListener重写mousePressed、mouseReleased等一堆方法Lambda 就没法用了匿名内部类依然是正解。我在实际项目里写批量导入的进度回调时也常用匿名内部类回调里要临时记录已处理的数量和失败列表我直接在匿名类内部加字段就行Lambda 做不到这种状态持有。4.3 Builder 模式与不可变对象静态内部类的典型舞台Effective Java 里推荐的 Builder 模式几乎清一色用静态内部类实现。为什么必须是静态因为 Builder 是在外部类之外的地方使用的调用方通常这样写new User.Builder(张三).age(30).build()如果 Builder 是非静态成员内部类你得先创建一个User实例才能写outer.new Builder()这在逻辑上根本说不通。静态内部类 Builder 的另一个好处是它和外部类在同一个文件里可以访问外部类的私有构造器而不需要额外暴露。外部类的字段干脆做成 final保证不可变性。这是非常整洁的组合public class User { private final String name; private final int age; private User(Builder builder) { this.name builder.name; this.age builder.age; } public static class Builder { private String name; private int age; public Builder name(String name) { this.name name; return this; } public Builder age(int age) { this.age age; return this; } public User build() { return new User(this); } } }这段代码里User的构造器是 private 的外部只能通过Builder.build()创建对象同时User的字段全是 final创建之后不可变。这不只是语法演示而是把一个真实需求彻底落实了对象的构建过程可以分步、校验但一旦构建完成就锁定。5. 完整实战用多种内部类写一个任务队列5.1 设计思路与类结构前面讲了这么多概念现在用一个能跑起来的例子把知识串起来。我设计一个极简的任务队列名字叫TaskQueue支持按优先级插入任务、遍历任务。这个例子特意把几种内部类用进去方便相互对照。类结构如下TaskQueue外部类管理任务数组和大小TaskQueue.Task静态内部类表示一个任务持有任务名和优先级TaskQueue.TaskIterator成员内部类实现IteratorTask遍历任务TaskQueue.Builder静态内部类用于配置队列容量并构造TaskQueueaddTask方法里的局部内部类Validator校验任务名sortTasks方法里的匿名内部类ComparatorTask按优先级排序。通过这个例子你会发现内部类在不同位置承担了不同职责作为数据结构的任务用静态内部类作为遍历器的游标用成员内部类作为临时校验逻辑用局部内部类作为一次性排序策略用匿名内部类。5.2 代码实现与逐段解读下面是完整代码我直接贴出来关键行都写了注释。import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Comparator; public class TaskQueue { private static final int DEFAULT_CAPACITY 10; private Task[] tasks; private int size; public TaskQueue(int capacity) { tasks new Task[capacity]; size 0; } public void addTask(String name, int priority) { // 局部内部类只在方法内有效 class Validator { boolean check(String taskName) { return taskName ! null !taskName.trim().isEmpty(); } } Validator validator new Validator(); if (!validator.check(name)) { throw new IllegalArgumentException(任务名不能为空); } tasks[size] new Task(name, priority); sortTasks(); } private void sortTasks() { // 匿名内部类一次性排序策略 ComparatorTask comparator new ComparatorTask() { Override public int compare(Task a, Task b) { return Integer.compare(b.getPriority(), a.getPriority()); } }; for (int i 1; i size; i) { Task key tasks[i]; int j i - 1; while (j 0 comparator.compare(tasks[j], key) 0) { tasks[j 1] tasks[j]; j--; } tasks[j 1] key; } } public IteratorTask iterator() { return new TaskIterator(); } // 静态内部类任务数据载体 public static class Task { private final String name; private final int priority; public Task(String name, int priority) { this.name name; this.priority priority; } public String getName() { return name; } public int getPriority() { return priority; } Override public String toString() { return name ( priority ); } } // 成员内部类隐式持有 TaskQueue 引用遍历数组 private class TaskIterator implements IteratorTask { private int cursor 0; Override public boolean hasNext() { return cursor size; } Override public Task next() { if (cursor size) { throw new NoSuchElementException(); } return tasks[cursor]; } } // 静态内部类Builder public static class Builder { private int capacity DEFAULT_CAPACITY; public Builder capacity(int capacity) { this.capacity capacity; return this; } public TaskQueue build() { return new TaskQueue(capacity); } } public static void main(String[] args) { TaskQueue queue new TaskQueue.Builder() .capacity(5) .build(); queue.addTask(读取文件, 1); queue.addTask(渲染界面, 3); queue.addTask(网络请求, 2); for (TaskQueue.Task task : queue) { System.out.println(task); } } }这段代码里每个内部类都值得单独说几句。Task是静态内部类它不依赖TaskQueue实例main方法里用TaskQueue.Task引用它说明它具备独立生命周期。TaskIterator是非静态成员内部类它直接访问外层TaskQueue的tasks数组和size字段不需要任何访问器这就是成员内部类“隐式引用”的价值。Validator是局部内部类如果是更复杂的校验逻辑它能把一堆临时校验方法收拢在一起但又不污染外部类。Comparator匿名内部类则负责一次性排序。5.3 运行结果与这个例子说明的问题运行main方法输出结果是渲染界面(3) 网络请求(2) 读取文件(1)任务按优先级从高到低排列符合预期。这个结果本身不重要重要的是这段代码完整展示了“同一个场景里为什么不同类型的内部类各有各的位置”如果Task不用静态内部类而是普通顶层类TaskQueue.Task的命名空间优势就没了任务对象的生命周期也变得含混如果TaskIterator不用成员内部类那么迭代器需要额外拿到tasks数组和size的引用代码要多好几个字段还不利于封装局部内部类和匿名内部类则负责方法级别的临时逻辑它们的存在让TaskQueue的公共方法保持精简。当然这个示例为了教学把局部内部类放进了一个非常小的校验场景实际业务里我更倾向把这种一两个方法的校验抽成私有静态方法。局部内部类最适合的是当临时状态多、逻辑需要聚合的场景比如在某个方法里临时封装一个带回调的处理过程。6. 高频面试题与避坑指南6.1 成员内部类和静态内部类的区别面试必问这是“内部类分类”之外的天然延伸题。核心区别一句话就能说清成员内部类持有外部类实例引用静态内部类不持有。展开讲有三层。第一层是创建方式成员内部类要在外部实例上写outer.new Inner()静态内部类直接写new Outer.Inner()。第二层是访问能力成员内部类可以访问外部类所有实例成员静态内部类只能访问外部类的静态成员。第三层是生命周期成员内部类作为外部对象的一部分存在外部对象回收时它不能再被单独持有静态内部类是一个独立的类生命周期与外部实例无关。面试官往往会追加一句话“如果成员内部类对象长期被外部持有会发生什么”这就引出了内存泄漏的话题。非静态内部类隐式引用外部对象当内部类对象被某个长期存活的容器引住时外部类对象就没办法被垃圾回收。Android 的 Handler 泄漏就是最经典的案例。6.2 内部类为何容易引发内存泄漏具体说下 Android 场景Activity 里经常写new Handler()如果这个 Handler 是匿名内部类或非静态成员内部类它就持有外部 Activity 的引用。Handler 把消息投递到 MessageQueueMessageQueue 又被 Looper 持有Looper 在应用进程里几乎永久存活。结果就是一个延时消息没被处理整个 Activity 即使 onDestroy 了也收不回来内存越积越多。解决方式并不复杂把 Handler 定义成静态内部类再用WeakReference持有 Activity 的引用保证不阻断 Activity 的回收。这个套路很多移动端团队都在用服务端也可能遇到类似的“缓存里存了回调对象”问题。核心原则是凡是生命周期可能超过外部类的内部类优先考虑静态内部类并仔细检查它有没有隐式引用外部实例。6.3 反射、序列化与内部类的“诡异”行为内部类在反射和序列化上有不少反直觉的点。首先是反射下的getDeclaringClass和getEnclosingClass。成员内部类的getDeclaringClass返回外部类局部内部类和匿名内部类的getDeclaringClass是null因为它们不是“成员”但它们的getEnclosingClass都能拿到外部类getEnclosingMethod还能拿到定义所在的方法。写动态代理或 class 分析工具时这组 API 一定要分清。其次是序列化。非静态内部类实现Serializable是有隐患的内部类持有一个指向外部实例的this$0合成字段反序列化时如果没有正确的构造函数很容易抛出InvalidClassException或者把整个外部对象图都一起序列化数据量和安全性都不可控。Effective Java 里也专门建议不要序列化非静态内部类实例。如果确实要序列化请用静态内部类并自行设计 DTO 结构。最后提一个字节码层面的现象早期 JDK 编译成员内部类时生成的合成access$方法看起来像是“外部类给内部类开后门”。到了 JDK 11 配合 nestmates 之后这个后门改由 JVM 直接管理。你如果维护的是老项目可能在反编译工具里见过这些方法名不要把它们当成普通方法处理它们只是编译器产物。6.4 我整理的内部类使用速查表写到这把内部类的最常用结论收在一张表里方便收藏备查。场景推荐类型理由数据载体 / DTO / 节点静态内部类无外部依赖命名空间收拢可独立创建迭代器 / 游标 / 视图非静态成员内部类需要访问外部实例的状态和私有字段Builder 模式静态内部类从外部静态上下文调用且能访问私有构造器一次性接口回调匿名内部类或 Lambda单方法用 Lambda多方法/持状态用匿名类方法内临时工具局部内部类逻辑仅限当前方法状态较多时比裸代码清晰不希望被感知的实现细节private 静态内部类强封装且不拖累外部实例生命周期这张表不是死规则但按这个方向选绝大多数场景不会出错。真正需要警惕的只有一条不确定生命周期关系时默认选静态内部类。它不会隐式引用外部对象踩内存泄漏的坑概率低很多。我自己在真实项目里用得最多的是静态内部类和匿名内部类。静态内部类负责各种 DTO、节点、Builder结构清晰又不泄漏匿名内部类在处理多方法回调、带状态回调时依然比 Lambda 顺手。成员内部类要谨慎因为那个隐式的外部引用既是便利也是包袱。局部内部类说实话用得最少但它是我写复杂方法时的“收拢思维”工具——当临时类状态一多我就先写一个局部类把状态封装起来再想是否要升格。内部类本身不难难的是分清楚每种形态背后的生命周期与访问规则。你只要把“谁被谁持有、谁需要访问谁”这两个问题想明白所有分类和选择都顺理成章了。
返回列表