内部类与枚举基础
学习目标
- 区分静态嵌套类、成员内部类、局部类和匿名类。
- 理解成员内部类对外部对象的隐式引用及捕获变量的限制。
- 用枚举表达有限状态,并使用
EnumSet、EnumMap管理相关数据。
核心知识点
专业术语
- 静态嵌套类(static nested class):声明在外部类中的静态类型,不自动持有外部实例引用。
- 成员内部类(inner class):非静态成员类,每个实例隐式关联一个外部类实例。
- 局部类与匿名类:声明在方法内部或表达式中的一次性类型,可捕获有效 final 的局部变量。
- 枚举(enum):实例集合在编译期固定的特殊类,可拥有字段、构造器、方法和抽象行为。
EnumSet/EnumMap:针对枚举优化的集合和映射,类型安全且通常比通用集合更紧凑。
白话解释与边界
静态嵌套类像外部类的命名空间成员,创建它不需要外部对象;成员内部类像“带着外部对象一起走”的对象,可能延长大对象或请求上下文的生命周期。局部类和匿名类适合短小策略,复杂逻辑应提取为有名字的类型。被捕获的局部变量必须是 final 或 effectively final,字段则通过外部对象访问。
枚举常量不是字符串,也不应依赖 ordinal() 作为持久化编号,因为新增或重排常量会改变序号。有限状态可以把合法行为写进枚举方法,再用 EnumMap 配置描述;状态转换仍要由领域规则校验,不能因为类型安全就默认所有转换都合法。
常用用法
static class:创建静态嵌套类
静态嵌套类不自动持有外部实例引用,适合只在外部类命名空间中组织、又有独立生命周期的类型。
// 说明:Report.Header 可直接 new,无需先构造 Report 实例;text() 返回 "header"。
class Report {
// 作用:静态嵌套类不自动持有外部实例引用,适合只在外部类命名空间中组织、又有独立生命周期的类型。
static class Header {
String text() {
return "header";
}
}
}
System.out.println(new Report.Header().text());
// 输出:header成员内部类:访问外部实例状态
成员内部类实例隐式关联外部对象,能直接访问外部实例成员,但也可能延长外部对象的生命周期。
// 语义:成员内部类实例隐式关联外部对象,能直接访问外部实例成员,但也可能延长外部对象的生命周期。
class Counter {
private final int start;
Counter(int start) {
this.start = start;
}
class View {
int value() {
return start;
}
}
}
System.out.println(new Counter(7).new View().value());
// 输出:7局部类:在方法内部命名一次性类型
局部类适合短小且只服务一个方法的逻辑;复杂行为应提取成顶层类或静态嵌套类。
// 语义:局部类适合短小且只服务一个方法的逻辑。
class Labels {
static String makeLabel(String value) {
class Label {
String text() {
return "label:" + value;
}
}
return new Label().text();
// 返回:new Label().text() 把传入值拼接为标签;调用 makeLabel("Java") 时返回 "label:Java"。
}
}
System.out.println(Labels.makeLabel("Java"));
// 输出:label:Java匿名类:临时实现接口
匿名类适合只使用一次的简单策略;需要复用、测试或保存较多状态时优先使用有名字的类型或 Lambda。
// 语义:匿名类适合只使用一次的简单策略。
interface Formatter {
String format(String value);
}
Formatter formatter = new Formatter() {
// 关键变化:formatter 接收表达式 new Formatter() { 的计算结果。
// 初始状态:formatter 的初始值为 new Formatter()。
@Override
public String format(String value) {
return value.toUpperCase();
}
};
System.out.println(formatter.format("java"));
// 输出:JAVAenum:声明有限状态集合
枚举常量是类型安全的固定实例,可以用 == 比较同一枚举类型;它不是任意字符串。
enum Status {
// 作用:枚举常量是类型安全的固定实例,可以用 == 比较同一枚举类型。
NEW, PAID, DONE
}
Status status = Status.PAID;
// 初始状态:status 当前为 Status.PAID。
System.out.println(status.name() + ", " + (status == Status.PAID));
// 输出:PAID, truevalues:遍历全部枚举常量
需要按声明顺序遍历枚举类型的全部常量时使用编译器生成的 values 方法。
// 语义:需要按声明顺序遍历枚举类型的全部常量时使用编译器生成的 values 方法。
// 具体调用:Status.values() 返回按 NEW、PAID、DONE 声明顺序排列的新数组。
enum Status { NEW, PAID, DONE }
System.out.println(java.util.Arrays.toString(Status.values()));
// 输出:[NEW, PAID, DONE]
// 作用:通过 values 遍历全部枚举常量。valueOf:按名称解析枚举常量
需要按精确名称解析枚举常量时使用 valueOf,外部输入应处理大小写差异和 IllegalArgumentException。
enum Status { NEW, PAID, DONE }
Status parsed = Status.valueOf("PAID");
// 初始状态:parsed 当前为 Status.valueOf("PAID")。
// 作用:需要按精确名称解析枚举常量时使用 valueOf,外部输入应处理大小写差异和 IllegalArgumentException。
System.out.println(parsed);
// 输出:PAIDEnumSet:高效保存枚举集合
EnumSet 只接受枚举元素,表达“某些状态的集合”时通常比通用 HashSet 更紧凑。
import java.util.EnumSet;
enum Status { NEW, PAID, DONE }
// 作用:EnumSet 只接受枚举元素,表达“某些状态的集合”时通常比通用 HashSet 更紧凑。
EnumSet<Status> terminal = EnumSet.of(Status.DONE);
// 初始状态:terminal 当前为 EnumSet.of(Status.DONE)。
// 返回:EnumSet.of(Status.DONE) 创建只含 DONE 的枚举集合。
// 作用:EnumSet 只接受枚举元素,表达“某些状态的集合”时通常比通用 HashSet 更紧凑。
terminal.add(Status.PAID);
// 关键变化:terminal.add(Status.PAID) 将 PAID 加入已有 DONE 集合,terminal 现在含 [PAID, DONE]。
System.out.println(terminal.contains(Status.PAID));
// 输出:trueEnumMap:用枚举作为 Map 键
创建时要给出枚举类型;它适合状态到标签、处理器或配置的映射。
import java.util.EnumMap;
enum Status { NEW, PAID, DONE }
EnumMap<Status, String> labels = new EnumMap<>(Status.class);
// 初始状态:labels 当前为 new EnumMap<>(Status.class)。
// 返回:new EnumMap<>(Status.class) 创建以 Status 为键类型的空映射。
// 作用:创建时要给出枚举类型。
labels.put(Status.PAID, "已支付");
// 关键变化:labels.put(Status.PAID, "已支付") 建立 PAID -> 已支付 映射。
System.out.println(labels.get(Status.PAID));
// 输出:已支付不常用但需要知道
ordinal:读取声明序号但不要持久化
ordinal 只表示当前源码声明顺序,新增或重排常量会改变数字;数据库和接口应显式保存稳定代码字段。
enum Status { NEW, PAID, DONE }
// 作用:通过 ordinal 读取声明序号但不要持久化。
System.out.println(Status.PAID.ordinal());
// 输出:1getDeclaringClass:读取枚举声明类型
反射或通用框架偶尔需要它,普通业务判断直接使用枚举类型和方法更清晰。
enum Status { NEW, PAID }
// 作用:通过 getDeclaringClass 读取枚举声明类型。
System.out.println(Status.PAID.getDeclaringClass().getSimpleName());
// 输出:Status枚举字段与构造器:附带稳定业务代码
枚举构造器不能由业务代码直接调用;稳定的 code 更适合持久化和外部协议,避免用 ordinal 猜业务含义。
enum Status {
NEW("N"), PAID("P");
private final String code;
Status(String code) {
this.code = code;
}
String code() {
return code;
}
}
System.out.println(Status.PAID.code());
// 输出:P捕获有效 final 局部变量:局部类和匿名类边界
被捕获的局部变量必须是 final 或 effectively final;如果逻辑需要持续变化的状态,应使用字段或专门的状态对象。
String prefix = "id=";
// 关键变化:prefix 接收表达式 "id=" 的计算结果。
// 初始状态:prefix 的初始值为 "id="。
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(prefix + 42);
// 输出:id=42
}
};
task.run();
// 关键变化:task.run() 执行匿名 Runnable,把捕获的 prefix="id=" 与 42 拼成 id=42。继续阅读
- 类与对象:引用、实例成员和对象生命周期。
- 构造器与初始化顺序:嵌套类和枚举初始化时机。
static、final与代码组织:静态嵌套类与常量的组织边界。
简单案例
import java.util.EnumMap;
import java.util.EnumSet;
enum OrderState {
CREATED("待支付"), PAID("已支付"), SHIPPED("已发货"), DONE("已完成"), CANCELED("已取消");
private final String label;
OrderState(String label) {
this.label = label;
}
String label() {
return label;
}
}
public class NestedEnumDemo {
static class Labels {
static String display(OrderState state) {
return state.name() + "=" + state.label();
}
}
private final int prefix;
NestedEnumDemo(int prefix) {
this.prefix = prefix;
}
class Member {
int value() {
return prefix; // 隐式使用外部实例
}
}
public static void main(String[] args) {
EnumMap<OrderState, String> labels = new EnumMap<>(OrderState.class);
// 初始状态:labels 是空的 EnumMap<OrderState, String>。
for (OrderState state : OrderState.values()) {
labels.put(state, state.label());
// 关键变化:labels.put(state, state.label()) 依次写入 CREATED=待支付、PAID=已支付、SHIPPED=已发货、DONE=已完成、CANCELED=已取消。
}
EnumSet<OrderState> terminal = EnumSet.of(OrderState.DONE, OrderState.CANCELED);
// 初始状态:terminal 包含 DONE 和 CANCELED 两个终态。
System.out.println(Labels.display(OrderState.PAID));
// 输出:PAID=已支付
System.out.println("terminal=" + terminal + ", label=" + labels.get(OrderState.SHIPPED));
// 输出:terminal=[DONE, CANCELED], label=已发货
System.out.println("member=" + new NestedEnumDemo(7).new Member().value());
// 输出:member=7
}
}输出会列出支付状态、终态集合和 member=7。Labels 是不持有外部实例的静态嵌套类,Member 则通过隐式引用访问 prefix;这就是两者生命周期边界的可观察差异。
易混点
- 静态嵌套类不自动持有外部对象,成员内部类会持有;需要独立生命周期时优先使用前者。
- 枚举是带固定实例的类,不是可随意拼写的字符串;比较同一枚举类型时可使用
==。 ordinal()是声明顺序,不是稳定业务 ID;持久化应显式保存代码字段。EnumSet/EnumMap只适合枚举键,普通Set/Map才适合任意对象键。
课后小问
为什么成员内部类可能造成外部对象无法及时回收? 答案:内部类实例隐式保存外部类实例引用,只要内部类仍可达,外部对象也可能保持可达。 解析:监听器、缓存或异步任务若长期保存内部类实例,就可能把请求上下文或大对象一起留住;静态嵌套类可避免这条引用。
为什么不能把
OrderState.ordinal()直接存进数据库? 答案:枚举常量的 ordinal 随声明顺序变化,新增或重排常量会让历史数字指向别的状态。 解析:应为每个常量定义稳定的字符串或整数代码并显式转换,这样源码排序不会改变持久化含义。
本节小结
- 静态嵌套类、成员内部类、局部类和匿名类的外部引用与生命周期不同。
- 捕获的局部变量必须有效 final,复杂匿名逻辑应提取为可命名、可测试的类型。
- 枚举用固定实例表达有限集合,可以承载字段、方法和状态行为。
EnumSet与EnumMap适合枚举集合,持久化状态要使用稳定代码而不是 ordinal。
快速回顾
- 能判断一个嵌套类是否需要外部实例引用。
- 能解释成员内部类的隐式引用和生命周期风险。
- 能用枚举字段表达状态标签而不是散落字符串常量。
- 能为枚举状态选择
EnumSet、EnumMap和稳定持久化代码。
阿源的知识库