方法内部的匿名内部类访问局部变量与实例变量对比分析
方法内部的匿名内部类访问局部变量与实例变量对比分析
概念
局部变量:定义在方法内部的变量
实例变量:定义在类中方法之外的变量
匿名内部类
定义与实例化:提供一种简洁的方式直接定义(实现接口或继承普通类)并实例化类的对象。无需先显式声明类再创建其实例。
继承普通类:当匿名内部类基于一个普通类时,它实际上创建了该类的一个子类,并可以覆盖父类中的方法。通常会重写该类中的某些方法(特别是那些抽象方法)。
实现接口:当匿名内部类基于一个接口时,它实际上实现了该接口,并提供了接口中所有抽象方法的具体实现。需要实现接口中的所有抽象方法,有default的抽象方法可以选择是否覆盖。
多态性:通过继承或实现接口体现多态,方便重写父类方法或实现接口方法。
适用场景:适用于仅需一次性使用的对象,减少代码量,提升简洁性。例如,在图形用户界面(GUI)编程中,按钮点击事件的处理程序往往只需要定义一次,使用匿名内部类可以非常方便地完成这一任务。
特点:
- 没有名字,只能使用一次。
- 可访问外部类的所有成员;在方法内定义时可访问该方法内的“effectively final”局部变量。
- 由于没有名字,不能显式定义构造器,但可以通过其父类构造器传入参数。
结论
在AI梳理笔记时发现匿名内部类的特性,于是深究了一下匿名内部类对于自己外部的变量访问的规则。先说结论——
定义在方法内部的匿名内部类
- 可访问外部类(如下是OuterClass)的所有成员;在方法内定义时可访问该方法内的“effectively final”(不可修改)局部变量。
- 外部类的所有成员,没有限制关键字,可修改,但需要程序员对变量的同步和安全问题负责
- 方法内的局部变量在逻辑上要符合final特性,可以省略final关键字
1 |
|
原因
为什么局部变量被设计为逻辑上是final,才能被匿名内部类访问?简单来说就是安全简单。
- 一致性与可预测性:通过限制匿名内部类只能访问“effectively final”的局部变量,可以确保行为的一致性和结果的可预测性。
- 简化语言特性的实现:这种限制简化了编译器的实现,使得匿名内部类更加容易被正确地编译和优化。在这种情况下,编译器只需为匿名内部类创建局部变量的一个副本即可。如果允许局部变量可变,则需要额外的机制来跟踪变量的变化,并确保所有引用该变量的地方都能看到最新的值,这增加了编译器的复杂度。
- 提高代码的安全性和可靠性:防止由于变量状态的意外变化而导致的潜在错误,提高了代码的安全性和可靠性。
为什么实例变量被修改可以被匿名内部类访问,而局部变量被修改就不行?
作用域和生命周期:
简单来说实例变量能确保匿名内部类在访问时实例变量存在,修改的同步比较容易;而局部变量无法确保生命周期内被匿名内部类修改同步。
- 实例变量:属于对象的一部分,它们的生命周期与对象相同。每个对象都有自己的一份实例变量副本。因此,无论匿名内部类是否访问这些变量,它们的状态都可以随时被该对象的方法改变。
- 局部变量:仅存在于声明它的方法或代码块内,一旦方法执行完毕,局部变量就会失效。如果允许匿名内部类访问可变的局部变量,可能会导致数据不一致的问题,因为局部变量的生命期通常比匿名内部类短。
同步问题:
简单来说是实例变量这边的问题交给程序员来负责了,而局部变量在设计上规避问题。
- 对于实例变量,由于它们是对象状态的一部分,任何对该对象的并发访问都需要开发者自行管理同步(例如使用synchronized关键字或其他同步机制)。这意味着,当一个线程通过匿名内部类访问某个实例变量时,另一个线程也有可能同时修改这个变量。然而,这是由开发者负责处理的情况,Java语言本身并不强制要求实例变量必须是不可变的。
- 对于局部变量,情况则不同。如果允许匿名内部类访问可变的局部变量,这将引入复杂性,尤其是在多线程环境下。由于局部变量的作用域限制,确保多个线程安全地访问同一个局部变量会变得非常困难。因此,Java规定匿名内部类只能访问那些在其定义时被视为“effectively final”的局部变量。
实例变量同步反例
经典多线程例子
扯到多线程和并发了。。。可以选择性忽略。。。
这个例子乍一看其实是线程同步的经典例子,只是没在Java中试验过,刚好结合了匿名内部类访问实例变量。从结构上来理解,类一层一层往下剖析,一个一个的附属关系,确实是能想通为什么能访问到这个变量的问题,感觉有点稀奇古怪。。。
1 |
|
多开点线程,结果显然是不同的。
1 |
|
ConcurrentModificationException
基本数据类型好像对并发修改有保护机制,所以不会报错。如果是List进行了结构修改会报错ConcurrentModificationException
。扯到多线程和并发了。。。AI一下。。。
1 |
|
可能的问题
ConcurrentModificationException
:- 在
removeElements
方法中,我们试图在一个增强型for循环(即foreach循环)中直接修改集合list
。这种做法在单线程环境中就会导致ConcurrentModificationException
,因为Java集合框架并不支持在迭代过程中直接修改集合。 - 如果在多线程环境下执行此操作,则问题更加复杂。即使只有一个线程在迭代并修改集合,另一个线程也在添加元素,这也可能导致不可预期的行为,包括但不限于
ConcurrentModificationException
。
- 在
数据不一致:
- 即使没有抛出异常,由于缺乏适当的同步机制,线程
t1
和t2
可能会相互干扰,导致最终的集合状态不符合预期。例如,某些元素可能被重复删除或未被正确删除。
- 即使没有抛出异常,由于缺乏适当的同步机制,线程
解决方案
为了避免上述问题,可以采取以下几种解决方案:
使用同步块或
synchronized
方法:
对所有访问共享资源的方法加锁,确保任意时刻只有一个线程能够修改集合。使用并发集合:
使用专门设计用于并发环境下的集合类,如CopyOnWriteArrayList
或Collections.synchronizedList()
等。
修改后的示例代码
使用Collections.synchronizedList
来包装原始的ArrayList
,从而避免ConcurrentModificationException
并保证线程安全:
1 |
|
通过这种方式,我们可以有效地防止并发修改带来的问题,并确保程序在多线程环境下的稳定性和正确性。