方法内部的匿名内部类访问局部变量与实例变量对比分析

方法内部的匿名内部类访问局部变量与实例变量对比分析

概念

局部变量:定义在方法内部的变量

实例变量:定义在类中方法之外的变量

匿名内部类

  • 定义与实例化:提供一种简洁的方式直接定义(实现接口或继承普通类)并实例化类的对象。无需先显式声明类再创建其实例。

    • 继承普通类:当匿名内部类基于一个普通类时,它实际上创建了该类的一个子类,并可以覆盖父类中的方法。通常会重写该类中的某些方法(特别是那些抽象方法)。

    • 实现接口:当匿名内部类基于一个接口时,它实际上实现了该接口,并提供了接口中所有抽象方法的具体实现。需要实现接口中的所有抽象方法,有default的抽象方法可以选择是否覆盖。

  • 多态性:通过继承或实现接口体现多态,方便重写父类方法或实现接口方法。

  • 适用场景:适用于仅需一次性使用的对象,减少代码量,提升简洁性。例如,在图形用户界面(GUI)编程中,按钮点击事件的处理程序往往只需要定义一次,使用匿名内部类可以非常方便地完成这一任务。

  • 特点

    • 没有名字,只能使用一次。
    • 可访问外部类的所有成员;在方法内定义时可访问该方法内的“effectively final”局部变量。
    • 由于没有名字,不能显式定义构造器,但可以通过其父类构造器传入参数。

结论

在AI梳理笔记时发现匿名内部类的特性,于是深究了一下匿名内部类对于自己外部的变量访问的规则。先说结论——

定义在方法内部的匿名内部类

  • 可访问外部类(如下是OuterClass)的所有成员;在方法内定义时可访问该方法内的“effectively final”(不可修改)局部变量。
    • 外部类的所有成员,没有限制关键字,可修改,但需要程序员对变量的同步和安全问题负责
    • 方法内的局部变量在逻辑上要符合final特性,可以省略final关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OuterClass {
private int outerField = 10;//外部的实例变量

public void method() {
outerField = 0;
int localVar = 20; // 成员变量,Java 8及以上版本此'final'关键字可省略,但需保证逻辑上不变
// localVar = 0; // 取消注释则修改localVar,静态检查报错,编译报错
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Outer field: " + outerField); // 访问外部类字段
System.out.println("Local variable: " + localVar); // 访问局部变量
}
}).start();
// localVar = 0; // 取消注释则修改localVar,静态检查报错,编译报错
}
}

原因

为什么局部变量被设计为逻辑上是final,才能被匿名内部类访问?简单来说就是安全简单。

  • 一致性与可预测性:通过限制匿名内部类只能访问“effectively final”的局部变量,可以确保行为的一致性和结果的可预测性。
  • 简化语言特性的实现:这种限制简化了编译器的实现,使得匿名内部类更加容易被正确地编译和优化。在这种情况下,编译器只需为匿名内部类创建局部变量的一个副本即可。如果允许局部变量可变,则需要额外的机制来跟踪变量的变化,并确保所有引用该变量的地方都能看到最新的值,这增加了编译器的复杂度。
  • 提高代码的安全性和可靠性:防止由于变量状态的意外变化而导致的潜在错误,提高了代码的安全性和可靠性。

为什么实例变量被修改可以被匿名内部类访问,而局部变量被修改就不行?

  • 作用域和生命周期:

    简单来说实例变量能确保匿名内部类在访问时实例变量存在,修改的同步比较容易;而局部变量无法确保生命周期内被匿名内部类修改同步。

    • 实例变量:属于对象的一部分,它们的生命周期与对象相同。每个对象都有自己的一份实例变量副本。因此,无论匿名内部类是否访问这些变量,它们的状态都可以随时被该对象的方法改变。
    • 局部变量:仅存在于声明它的方法或代码块内,一旦方法执行完毕,局部变量就会失效。如果允许匿名内部类访问可变的局部变量,可能会导致数据不一致的问题,因为局部变量的生命期通常比匿名内部类短。
  • 同步问题:

    简单来说是实例变量这边的问题交给程序员来负责了,而局部变量在设计上规避问题。

    • 对于实例变量,由于它们是对象状态的一部分,任何对该对象的并发访问都需要开发者自行管理同步(例如使用synchronized关键字或其他同步机制)。这意味着,当一个线程通过匿名内部类访问某个实例变量时,另一个线程也有可能同时修改这个变量。然而,这是由开发者负责处理的情况,Java语言本身并不强制要求实例变量必须是不可变的。
    • 对于局部变量,情况则不同。如果允许匿名内部类访问可变的局部变量,这将引入复杂性,尤其是在多线程环境下。由于局部变量的作用域限制,确保多个线程安全地访问同一个局部变量会变得非常困难。因此,Java规定匿名内部类只能访问那些在其定义时被视为“effectively final”的局部变量。

实例变量同步反例

经典多线程例子

扯到多线程和并发了。。。可以选择性忽略。。。

这个例子乍一看其实是线程同步的经典例子,只是没在Java中试验过,刚好结合了匿名内部类访问实例变量。从结构上来理解,类一层一层往下剖析,一个一个的附属关系,确实是能想通为什么能访问到这个变量的问题,感觉有点稀奇古怪。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Counter {
private int instanceVar = 0; // 实例变量

public void increment() {
instanceVar++;
}

public int getInstanceVar() {
return instanceVar;
}

public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();

// 创建并启动100个线程,每个线程都会调用increment方法
Thread[] threads = new Thread[10000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
counter.increment(); // 增加实例变量的值
}
});
threads[i].start();
}

// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}

// 打印最终的结果
System.out.println("Expected: " + threads.length);
System.out.println("Actual: " + counter.getInstanceVar());
}
}

多开点线程,结果显然是不同的。

1
2
Expected: 10000
Actual: 9997

ConcurrentModificationException

基本数据类型好像对并发修改有保护机制,所以不会报错。如果是List进行了结构修改会报错ConcurrentModificationException。扯到多线程和并发了。。。AI一下。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package org.example;

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExample {
private List<Integer> list = new ArrayList<>();

public void addElement(Integer element) {
list.add(element);
}

public void removeElements() {
for (Integer value : list) { // 迭代过程中尝试修改list
if (value % 2 == 0) {
list.remove(value); // 直接修改集合,可能导致ConcurrentModificationException
}
}
}

public static void main(String[] args) {
final ConcurrentModificationExample example = new ConcurrentModificationExample();

// 初始化列表
for (int i = 0; i < 10; i++) {
example.addElement(i);
}

// 创建两个线程同时操作list
Thread t1 = new Thread(() -> example.removeElements());
Thread t2 = new Thread(() -> example.addElement(10));

t1.start();
t2.start();

try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Final list size: " + example.list.size());
}
}

可能的问题

  1. ConcurrentModificationException:

    • removeElements方法中,我们试图在一个增强型for循环(即foreach循环)中直接修改集合list。这种做法在单线程环境中就会导致ConcurrentModificationException,因为Java集合框架并不支持在迭代过程中直接修改集合。
    • 如果在多线程环境下执行此操作,则问题更加复杂。即使只有一个线程在迭代并修改集合,另一个线程也在添加元素,这也可能导致不可预期的行为,包括但不限于ConcurrentModificationException
  2. 数据不一致:

    • 即使没有抛出异常,由于缺乏适当的同步机制,线程t1t2可能会相互干扰,导致最终的集合状态不符合预期。例如,某些元素可能被重复删除或未被正确删除。

解决方案

为了避免上述问题,可以采取以下几种解决方案:

  • 使用同步块或synchronized方法
    对所有访问共享资源的方法加锁,确保任意时刻只有一个线程能够修改集合。

  • 使用并发集合
    使用专门设计用于并发环境下的集合类,如CopyOnWriteArrayListCollections.synchronizedList()等。

修改后的示例代码

使用Collections.synchronizedList来包装原始的ArrayList,从而避免ConcurrentModificationException并保证线程安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class SafeConcurrentModificationExample {
// 使用Collections.synchronizedList包装ArrayList
private List<Integer> list = Collections.synchronizedList(new ArrayList<>());

public void addElement(Integer element) {
synchronized (list) {
list.add(element);
}
}

public void removeElements() {
synchronized (list) {
for (Integer value : new ArrayList<>(list)) { // 创建副本进行迭代
if (value % 2 == 0) {
list.remove(value);
}
}
}
}

public static void main(String[] args) {
final SafeConcurrentModificationExample example = new SafeConcurrentModificationExample();

// 初始化列表
for (int i = 0; i < 10; i++) {
example.addElement(i);
}

// 创建两个线程同时操作list
Thread t1 = new Thread(example::removeElements);
Thread t2 = new Thread(() -> example.addElement(10));

t1.start();
t2.start();

try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Final list size: " + example.list.size());
}
}

通过这种方式,我们可以有效地防止并发修改带来的问题,并确保程序在多线程环境下的稳定性和正确性。


方法内部的匿名内部类访问局部变量与实例变量对比分析
http://willxu0313.github.io/2025/04/19/Java/方法内部的匿名内部类访问局部变量与实例变量对比分析/
作者
Will
发布于
2025年4月19日
许可协议