Skip to content

内部类与枚举基础 ​

学习目标 ​

  • 区分静态嵌套类、成员内部类、局部类和匿名类。
  • 理解成员内部类对外部对象的隐式引用及捕获变量的限制。
  • 用枚举表达有限状态,并使用 EnumSet、EnumMap 管理相关数据。

核心知识点 ​

专业术语 ​

  • 静态嵌套类(static nested class):声明在外部类中的静态类型,不自动持有外部实例引用。
  • 成员内部类(inner class):非静态成员类,每个实例隐式关联一个外部类实例。
  • 局部类与匿名类:声明在方法内部或表达式中的一次性类型,可捕获有效 final 的局部变量。
  • 枚举(enum):实例集合在编译期固定的特殊类,可拥有字段、构造器、方法和抽象行为。
  • EnumSet/EnumMap:针对枚举优化的集合和映射,类型安全且通常比通用集合更紧凑。

白话解释与边界 ​

静态嵌套类像外部类的命名空间成员,创建它不需要外部对象;成员内部类像“带着外部对象一起走”的对象,可能延长大对象或请求上下文的生命周期。局部类和匿名类适合短小策略,复杂逻辑应提取为有名字的类型。被捕获的局部变量必须是 final 或 effectively final,字段则通过外部对象访问。

枚举常量不是字符串,也不应依赖 ordinal() 作为持久化编号,因为新增或重排常量会改变序号。有限状态可以把合法行为写进枚举方法,再用 EnumMap 配置描述;状态转换仍要由领域规则校验,不能因为类型安全就默认所有转换都合法。

常用用法 ​

static class:创建静态嵌套类 ​

静态嵌套类不自动持有外部实例引用,适合只在外部类命名空间中组织、又有独立生命周期的类型。

java
// 说明:Report.Header 可直接 new,无需先构造 Report 实例;text() 返回 "header"。
class Report {
// 作用:静态嵌套类不自动持有外部实例引用,适合只在外部类命名空间中组织、又有独立生命周期的类型。
    static class Header {
        String text() {
            return "header";
        }
    }
}

System.out.println(new Report.Header().text());
// 输出:header

成员内部类:访问外部实例状态 ​

成员内部类实例隐式关联外部对象,能直接访问外部实例成员,但也可能延长外部对象的生命周期。

java
// 语义:成员内部类实例隐式关联外部对象,能直接访问外部实例成员,但也可能延长外部对象的生命周期。
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

局部类:在方法内部命名一次性类型 ​

局部类适合短小且只服务一个方法的逻辑;复杂行为应提取成顶层类或静态嵌套类。

java
// 语义:局部类适合短小且只服务一个方法的逻辑。
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。

java
// 语义:匿名类适合只使用一次的简单策略。
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"));
// 输出:JAVA

enum:声明有限状态集合 ​

枚举常量是类型安全的固定实例,可以用 == 比较同一枚举类型;它不是任意字符串。

java
enum Status {
// 作用:枚举常量是类型安全的固定实例,可以用 == 比较同一枚举类型。
    NEW, PAID, DONE
}

Status status = Status.PAID;
// 初始状态:status 当前为 Status.PAID。
System.out.println(status.name() + ", " + (status == Status.PAID));
// 输出:PAID, true

values:遍历全部枚举常量 ​

需要按声明顺序遍历枚举类型的全部常量时使用编译器生成的 values 方法。

java
// 语义:需要按声明顺序遍历枚举类型的全部常量时使用编译器生成的 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。

java
enum Status { NEW, PAID, DONE }

Status parsed = Status.valueOf("PAID");
// 初始状态:parsed 当前为 Status.valueOf("PAID")。
// 作用:需要按精确名称解析枚举常量时使用 valueOf,外部输入应处理大小写差异和 IllegalArgumentException。
System.out.println(parsed);
// 输出:PAID

EnumSet:高效保存枚举集合 ​

EnumSet 只接受枚举元素,表达“某些状态的集合”时通常比通用 HashSet 更紧凑。

java
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));
// 输出:true

EnumMap:用枚举作为 Map 键 ​

创建时要给出枚举类型;它适合状态到标签、处理器或配置的映射。

java
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 只表示当前源码声明顺序,新增或重排常量会改变数字;数据库和接口应显式保存稳定代码字段。

java
enum Status { NEW, PAID, DONE }

// 作用:通过 ordinal 读取声明序号但不要持久化。
System.out.println(Status.PAID.ordinal());
// 输出:1

getDeclaringClass:读取枚举声明类型 ​

反射或通用框架偶尔需要它,普通业务判断直接使用枚举类型和方法更清晰。

java
enum Status { NEW, PAID }

// 作用:通过 getDeclaringClass 读取枚举声明类型。
System.out.println(Status.PAID.getDeclaringClass().getSimpleName());
// 输出:Status

枚举字段与构造器:附带稳定业务代码 ​

枚举构造器不能由业务代码直接调用;稳定的 code 更适合持久化和外部协议,避免用 ordinal 猜业务含义。

java
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;如果逻辑需要持续变化的状态,应使用字段或专门的状态对象。

java
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。

继续阅读 ​

简单案例 ​

java
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 才适合任意对象键。

课后小问 ​

  1. 为什么成员内部类可能造成外部对象无法及时回收? 答案:内部类实例隐式保存外部类实例引用,只要内部类仍可达,外部对象也可能保持可达。 解析:监听器、缓存或异步任务若长期保存内部类实例,就可能把请求上下文或大对象一起留住;静态嵌套类可避免这条引用。

  2. 为什么不能把 OrderState.ordinal() 直接存进数据库? 答案:枚举常量的 ordinal 随声明顺序变化,新增或重排常量会让历史数字指向别的状态。 解析:应为每个常量定义稳定的字符串或整数代码并显式转换,这样源码排序不会改变持久化含义。

本节小结 ​

  • 静态嵌套类、成员内部类、局部类和匿名类的外部引用与生命周期不同。
  • 捕获的局部变量必须有效 final,复杂匿名逻辑应提取为可命名、可测试的类型。
  • 枚举用固定实例表达有限集合,可以承载字段、方法和状态行为。
  • EnumSet 与 EnumMap 适合枚举集合,持久化状态要使用稳定代码而不是 ordinal。

快速回顾 ​

  • 能判断一个嵌套类是否需要外部实例引用。
  • 能解释成员内部类的隐式引用和生命周期风险。
  • 能用枚举字段表达状态标签而不是散落字符串常量。
  • 能为枚举状态选择 EnumSet、EnumMap 和稳定持久化代码。