匿名类

1️⃣ 本质:一次性定义的匿名子类对象

匿名类在语法上表现为 new 接口/类(){...},但实际上做了两件事:

  • 隐式定义了一个子类:继承指定类或实现指定接口,类名由编译器自动生成(如 Outer$1)。
  • 立即创建该子类的一个实例,并返回引用。
Runnable r = new Runnable() {
    @Override
    public void run() { System.out.println("匿名子类对象"); }
};
// r 的实际类型是编译器生成的匿名子类(如 Demo$1)

2️⃣ 对象的编译时类型 vs 运行时类型

  • 编译时类型:声明时使用的类型(父类或接口),例如 Runnable
  • 运行时类型:实际创建的匿名子类,例如 class Demo$1
System.out.println(r.getClass().getName()); // 输出类似 Demo$1

这种多态性意味着:匿名类对象只能通过父类/接口引用访问其方法,不能使用匿名类内部新增的成员(除非强制转型,但很不安全且不推荐)。

3️⃣ 匿名类作为“子类”的特点

  • 继承父类/实现接口:匿名类一定是某个已有类或接口的子类。
  • 没有类名:无法在其他地方重复使用该子类定义,也无法 extends 或 implements 它。
  • 可以重写父类方法:就像普通子类一样,可重写父类的非 final 方法或接口的抽象方法。
  • 不能定义静态成员(除 static final 常量外),因为匿名类本质上是局部内部类。
  • 不能有构造器:但可以用实例初始化块模拟构造功能。 匿名类实例: new 父类名(){类体}(是一种子类对象)
new Object() {
    { // 实例初始化块,在构造时执行
        System.out.println("匿名子类初始化");
    }
};

4️⃣ 对象的创建与生命周期

  • 创建方式new 接口/父类() { ... } 一步完成定义和实例化。
  • 唯一实例:每次 new 都会创建一个新的匿名子类对象(但类定义只编译生成一次 .class 文件)。
  • 引用持有:可以赋值给变量,也可以直接作为方法参数传递(一次性使用)。
  • 生命周期:与普通对象相同,由 GC 管理。如果匿名类对象持有外部类引用(非静态匿名类),可能阻止外部类实例被回收。

5️⃣ 匿名类对象的特殊行为

  • this 指向匿名类对象:在匿名类内部,this 表示当前匿名子类的实例。
  • 外部类.this:如果需要引用外部类对象,使用 外部类名.this
  • 局部变量捕获:只能访问 final 或 effectively final 的局部变量(Java 8+),这些变量会被复制到匿名类对象中(类似闭包)。
class Outer {
    void method() {
        int x = 10; // effectively final
        Runnable r = new Runnable() {
            public void run() {
                System.out.println(x); // 合法,x 被捕获到匿名对象中
                // x = 20; // 编译错误
            }
        };
    }
}

6️⃣ 匿名类 vs 普通子类对象对比

对比项匿名类对象普通子类对象
类名无(编译器生成 $1有显式名称
复用性无法复用此类定义可多次实例化
构造器无(可用实例初始化块)有构造器
定义位置表达式内部独立文件或类内部
使用场景一次性实现接口/继承类需要反复使用的类

7️⃣ 示例:当作参数传递的子类对象

// 方法签名:需要 Runnable 子类对象
public void execute(Runnable task) { task.run(); }
 
// 调用时直接创建匿名子类对象
execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("任务执行");
    }
});

这里匿名类对象在 execute 方法内部被使用,方法结束后如果没有其他引用,对象即成为 GC 候选,体现了一次性子类的特点。

topic: 编程 tags:

  • Java
  • 概念