final的那些事儿
final作为java中基础常用的关键字,相信大家都很熟悉,大家都晓得final可以修饰类,方法和变量且具有以下特性:
- final修饰类时,该类不可被继承
- final修饰方法时,该方法不可被重写
- final修饰变量时,如果该变量是基础数据类型,则只能赋值一次,如果该变量是对象类型,则其指向的地址只能赋值一次
那么除了这些就没有了吗?看以下问题:
- 当匿名内部类引用函数中的局部变量时,局部变量是基础数据类型会怎样?对象类型会怎样?为什么需要final修饰?
- 当匿名内部类引用函数中的局部变量时,这个变量在堆上还是栈上?
- 当对象类型局部变量用final修饰后,如果被子线程持有,在子线程的属性值修改能被主线程感知吗?
如果这些问题你都晓得,那么恭喜你,不用继续往下看了。
当匿名内部类引用函数局部变量时,为什么需要final修饰?
内部类中不修改函数局部变量值
要验证该问题,我们先来简单编写一个内部类引用函数中局部变量的示例,代码如下:
public interface Callback {
void doWork();
}
public class FinalTest {
public FinalTest() {
}
public void execute() {
int variable = 10;
setCallback(new Callback() {
@Override
public void doWork() {
System.out.println("method variable is:" + variable);
}
});
}
public void setCallback(Callback callback) {
}
}
public class Main {
public static void main(String[] args) {
FinalTest finalTest = new FinalTest();
finalTest.execute();
}
}
可以看到在上述代码中,我们在FinalTest的execute方法中,通过Callback这个匿名内部类引用了execute函数中的variable变量,此时虽然variable没有被final修饰,但是代码仍然是可以运行的(基于JDK 1.8)。
有同学要说了,你又乱说,看看你的问题,问的是当内部类引用函数局部变量时,为什么需要final修饰?这没有final不照样跑的好好的?别急,我们来看下该程序的字节码,FinalTest类对应的字节码如下所示:
可以看到编译后自动为variable变量添加了final关键词修饰,那么为什么需要使用final关键词来修饰呢?主要有以下几点原因:
- 生命周期不同: 从运行时内存分区一文中可知,函数中的局部变量作为线程的私有数据,被存储在虚拟机栈对应的栈帧中,当函数结束执行后出栈,而Callback类的doWork方法其调用时机与execute函数的执行结束时机明显不一致,故如果不使用final修饰,不能保证doWork方法执行时variable变量仍然存在。
- 数据不同步: 从变量数据同步的角度来看,局部变量在传递到内部类时,是以备份的形式拷贝到自己的构造函数中,加以存储利用,如果不添加final修饰,则内外部修改互不同步,造成脏数据。
那么为什么使用fina就能解决以上问题呢?(仅针对基础类型讨论,对象类型见下一部分)
生命周期不同
继续查看FinalTest类字节码,可以看到当variable变量使用final修饰后,其被存储于常量池中,而常量池存储于方法区中,进而生命周期必然是大于Callback类的,variable变量相关字节码如下图:
数据不同步
查看通过new Callback创建的FinalTest匿名内部类的字节码,代码如下所示:
可以看到编译生成的 FinalTest$1
这个匿名内部类继承自Callback接口,其通过构造函数持有了外部类FinalTest的引用以及int类型的var2,当variable被final修饰时,由于其值不可修改,故外部的variable变量和 FinalTest$1
构造函数中传入的var2值始终保持一致,故而不存在数据不同步的问题。
从这里我们明显可以看出匿名内部类会持有外部类的引用,Handler导致的Activity内部泄漏的场景就是这样引发的
同理也可以看出,为什么匿名内部类访问外部类的成员变量不需要final修饰,主要是这种访问关系都可以转化为通过外部类引用间接访问
内部类中修改函数局部变量值
仍以上文中代码为例,我们在doWork中修改variable变量的值,来看下会怎么样?
从上图可以看出编译器提示我们将variable转化成一个单元素的数组,按照编译器提示修改,果然可以正常运行了
那么这是为什么呢?不是说final修饰的变量值不能变吗?编译器bug了?
当然不是,这里我们需要明白单元素数组它不是一个基础类型变量,它指向的是一块内存地址,当其被final修饰时,说的是该变量不能重新指向一块新的内存地址,而不是内存地址处存储的内容不可以变化,也就是说我们不能再次执行variable = {20}这种赋值操作(PS:类对象同理,变量不可以重新赋值成新的对象,但是对象的成员属性取值可以发生变化)。
为对对象的指向地址修改和成员属性修改做区分,下文中将成员属性修改简称为内容修改,将地址修改简称为值修改
同时我们也可以从这里了解到当匿名内部类持有函数的局部变量时,是通过符号引用获取的(类结构中常量池中字段说明可以参考<<深入理解Java虚拟机>>), FinalTest$1
类中val$variable
变量声明及在常量池中引用如下图所示:
子线程修改final修饰局部变量内容,是否可同步?
仍以上文代码为例,修改FinalTest类如下所示:
public class FinalTest {
public FinalTest() {
}
public void execute() {
int variable = 10;
setCallback(new Callback() {
@Override
public void doWork() {
System.out.println("method variable is:" + variable);
}
});
}
public void execute2() {
final int[] variable = {10};
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("variable[0] is:" + variable[0]);
variable[0] = 100;
System.out.println("change variable[0] is:" + variable[0]);
}
});
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("after change variable[0] is:" + variable[0]);
}
public void setCallback(Callback callback) {
}
}
在Main中运行execute2,执行结果如下图所示:
可以看到,数组元素的值确定发生了改变,这也就意味着被final修饰的变量,其内容修改在多线程环境下具有可见性。
final在多线程环境下具有可见性
总结
final,static与synchronized
关键词 | 修饰类型 | 作用 |
---|---|---|
final | 类,方法,变量 | 修饰类,则类不可继承; 修饰方法,则方法不可被子类重写; 修饰变量,则变量只能初始化一次 |
static | 内部类,方法,变量,代码段 | 修饰内部类,则该类只能访问外部类的静态成员变量和方法,在Handler内存泄漏的修复方案中就有静态内部类的方式; 修饰方法,则该方法可以直接通过类名访问 修饰变量,则该变量可以直接通过类名访问,在类的实例中,静态变量共享同一份内存空间,故其具有全局性质 修饰代码段,则该代码段在类加载的时候就会执行,由于类加载是多线程安全的,所以可以通过静态代码段实现一些初始化操作而不用担心多线程问题 |
synchronized | 方法,代码段 | 修饰方法时,则该方法为同步方法,使用该方法所在的类对象做为锁对象,多线程环境下,排队执行 修饰代码段时,一般会指定所使用的锁对象,多线程环境下,该代码段排队执行 |
链接:https://juejin.cn/post/7201665010199003197
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。