问题

什么是匿名内部类?如何访问其外部定义的变量?

答案

核心概念

匿名内部类(Anonymous Inner Class)是一种没有名字的内部类,在定义的同时直接创建实例。它通常用于简化代码,特别是在需要实现接口或继承类但只使用一次的场景。

基本语法

// 实现接口的匿名内部类
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("匿名内部类实现 Runnable");
    }
};

// 继承类的匿名内部类
Thread thread = new Thread() {
    @Override
    public void run() {
        System.out.println("匿名内部类继承 Thread");
    }
};

语法结构

new 父类构造器(参数)  接口() {
    // 匿名内部类的类体
    // 可以重写方法、定义新方法和字段
};

典型使用场景

1. 事件监听器(GUI 编程)

button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("按钮被点击");
    }
});

2. 线程创建

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("执行异步任务");
    }
}).start();

3. 集合排序

List<String> list = Arrays.asList("banana", "apple", "cherry");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();  // 按长度排序
    }
});

访问外部变量的规则

匿名内部类可以访问外部的变量,但有严格的限制:

1. 访问外部类的成员变量(无限制)

public class Outer {
    private int outerField = 100;
    private static int staticField = 200;

    public void test() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(outerField);      // ✅ 可以访问实例变量
                System.out.println(staticField);     // ✅ 可以访问静态变量
                outerField = 999;                    // ✅ 可以修改
            }
        };
        runnable.run();
    }
}

2. 访问局部变量(必须是 final 或 effectively final)

关键限制:匿名内部类访问的局部变量必须是 final事实上的 final(effectively final,即初始化后未被修改)。

public void test() {
    int localVar = 10;           // 事实上的 final(未被修改)
    final int finalVar = 20;     // 显式声明 final

    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println(localVar);   // ✅ 可以访问
            System.out.println(finalVar);   // ✅ 可以访问
            // localVar = 100;              // ❌ 编译错误:无法修改
        }
    };

    // localVar = 30;  // ❌ 如果在这里修改,上面的访问会编译错误
}

为什么局部变量必须是 final?

本质原因变量捕获机制(Variable Capture)

  1. 局部变量的生命周期:方法执行完毕后,局部变量在栈上被销毁
  2. 匿名内部类的生命周期:对象可能在方法返回后继续存在
  3. 实现机制:编译器会将局部变量的值复制到匿名内部类的字段中
// 源代码
public void test() {
    int x = 10;
    Runnable r = new Runnable() {
        public void run() {
            System.out.println(x);
        }
    };
}

// 编译器生成的等价代码(简化)
class Outer$1 implements Runnable {
    final int val$x;  // 编译器生成的字段,存储 x 的副本

    Outer$1(int x) {
        this.val$x = x;
    }

    public void run() {
        System.out.println(val$x);  // 访问的是副本
    }
}

如果允许修改

  • 外部的 x 和内部类的 val$x 是两个独立的变量
  • 修改一个不会影响另一个,导致数据不一致
  • 因此 Java 强制要求 final,确保值不可变

完整示例

public class AnonymousClassDemo {
    private int outerField = 100;

    public void demonstrate() {
        final int finalLocal = 10;
        int effectivelyFinal = 20;

        // 匿名内部类实现接口
        Runnable runnable = new Runnable() {
            private int innerField = 5;  // 匿名内部类自己的字段

            @Override
            public void run() {
                // 访问外部类的成员变量
                System.out.println("外部类字段: " + outerField);
                outerField = 200;  // 可以修改

                // 访问局部变量(必须是 final 或 effectively final)
                System.out.println("final 局部变量: " + finalLocal);
                System.out.println("effectively final 局部变量: " + effectivelyFinal);

                // 访问自己的字段
                System.out.println("内部类字段: " + innerField);

                // ❌ 不能修改局部变量
                // effectivelyFinal = 30;  // 编译错误
            }
        };

        runnable.run();
    }

    public static void main(String[] args) {
        new AnonymousClassDemo().demonstrate();
    }
}

匿名内部类 vs Lambda 表达式

Java 8 引入 Lambda 表达式后,很多匿名内部类可以简化:

// 匿名内部类
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// Lambda 表达式(更简洁)
Runnable r2 = () -> System.out.println("Hello");

区别

  1. this 指向
    • 匿名内部类的 this 指向内部类实例
    • Lambda 的 this 指向外部类实例
  2. 编译产物
    • 匿名内部类生成独立的 .class 文件(如 Outer$1.class
    • Lambda 使用 invokedynamic 指令,不生成额外类文件
  3. 使用限制
    • Lambda 只能用于函数式接口(只有一个抽象方法的接口)
    • 匿名内部类可以实现任意接口或继承类

匿名内部类的限制

  1. 不能定义构造器:因为没有类名,无法声明构造器(但可以使用实例初始化块)
  2. 不能是静态的:匿名内部类不能声明为 static
  3. 只能实现一个接口或继承一个类:不能同时实现多个接口
// ✅ 使用实例初始化块
Runnable r = new Runnable() {
    {
        // 实例初始化块,相当于构造器
        System.out.println("初始化匿名内部类");
    }

    @Override
    public void run() {
        System.out.println("执行任务");
    }
};

实际开发建议

  1. 优先使用 Lambda:对于函数式接口,Lambda 更简洁
  2. 复杂逻辑用命名类:如果逻辑复杂或需要复用,定义独立的类
  3. 注意变量捕获:理解 effectively final 的限制,避免编译错误
  4. 避免过度嵌套:匿名内部类嵌套会降低代码可读性

面试答题要点

  1. 定义:没有名字的内部类,定义时直接创建实例
  2. 使用场景:事件监听、线程创建、一次性接口实现
  3. 访问规则
    • 可以访问外部类的所有成员(包括 private)
    • 访问局部变量必须是 final 或 effectively final
  4. 原理:编译器通过变量捕获(值复制)实现局部变量访问
  5. 与 Lambda 对比:Lambda 更简洁,但只适用于函数式接口

这道题考察对 Java 内部类机制的理解,以及闭包(Closure)概念的掌握。