我们在一个方法中定义匿名内部类访问方法的本地变量时常常会发现编译时出错,被告知“需要被声明为最终类型",甚是疑惑,于是在网上搜索其原因, 在此作一总结。
局部内部类(在方法内部定义的类)中无法直接访问方法中的局部变量,须修饰其为final
1:在方法内声明的本地变量的生命周期与局部内部类对象的生命周期不一致从而导致了这个问题。前者在一个方法运行结束后就随之被销毁。而后者生命周期的终点却并不在此处,只有当该对象不再被引用时,它才会被GC回收。倘若匿名内部类可以直接访问不被final修饰的本地变量,那么就有可能出现一个奇怪的现象:对象在访问一个已经不存在的变量。
2:我们假设局部内部类中可以直接访问方法中的局部变量,且不需要其为final型。我们知道,在内部类中访问变量实际上是在访问该变量的复制品,如果上述条件成立,无论是基本类型还是引用类型,那么一旦局部变量实体或者复制品任何一方发生改变,都不能相互同步,从而造成变量的实体与复制品不一致,想象一下你看着是在访问一个变量,然而你得到的值却与实际的值不同,因此这样就毫无意义可言。
为了更加清晰深刻的了解Java在处理匿名内部类访问final修饰的本地变量时的处理过程,先看看下面这一段代码
代码清单FinalKeywordTest.java
public class FinalKeywordTest {
public static void main(String[] args) {
final int i = 10;
final Person person = new Person("penny", 26);
new Thread() {
@Override
public void run() {
System.out.println(i + 1);
System.out.println(person);
}
};
}
}
class Person{
String name;
int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
首先通过JDK的工具javap查看一下这个Java文件被编译成什么样子的
javap -v FinalKeywordTest.class > 1.txt
javap -v FinalKeywordTest$1.class > 2.txt
这里得到的结果分别打印到1.txt和2.txt中了,由于我们研究的是匿名内部类如何访问final修饰的本地变量,所以我们主要看2.txt即可
Classfile /D:/Workspace/eclipse-for-temp/test/bin/com/kmter/test/FinalKeywordTest$1.class
Last modified 2014-12-21; size 772 bytes
MD5 checksum b2d663f92b7895759e9a26f6124ef489
Compiled from "FinalKeywordTest.java"
class com.kmter.test.FinalKeywordTest$1 extends java.lang.Thread
SourceFile: "FinalKeywordTest.java"
EnclosingMethod: #38.#40 // com.kmter.test.FinalKeywordTest.main
InnerClasses:
#1; //class com/kmter/test/FinalKeywordTest$1
minor version: 0
major version: 51
flags: ACC_SUPER
Constant pool:
#1 = Class #2 // com/kmter/test/FinalKeywordTest$1
#2 = Utf8 com/kmter/test/FinalKeywordTest$1
#3 = Class #4 // java/lang/Thread
#4 = Utf8 java/lang/Thread
#5 = Utf8 val$person
#6 = Utf8 Lcom/kmter/test/Person;
#7 = Utf8 <init>
#8 = Utf8 (Lcom/kmter/test/Person;)V
#9 = Utf8 Code
#10 = Fieldref #1.#11 // com/kmter/test/FinalKeywordTest$1.val$person:Lcom/kmter/test/Person;
//这里可以看到被final修饰的Person对象在匿名内部类中直接体现为常量池的一个对象引用,这个对象的引用是从FinalKeywordTest类的main方法中复制过来的
#11 = NameAndType #5:#6 // val$person:Lcom/kmter/test/Person;
#12 = Methodref #3.#13 // java/lang/Thread."<init>":()V
#13 = NameAndType #7:#14 // "<init>":()V
#14 = Utf8 ()V
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/kmter/test/FinalKeywordTest$1;
#19 = Utf8 run
#20 = Fieldref #21.#23 // java/lang/System.out:Ljava/io/PrintStream;
#21 = Class #22 // java/lang/System
#22 = Utf8 java/lang/System
#23 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Methodref #27.#29 // java/io/PrintStream.println:(I)V
#27 = Class #28 // java/io/PrintStream
#28 = Utf8 java/io/PrintStream
#29 = NameAndType #30:#31 // println:(I)V
#30 = Utf8 println
#31 = Utf8 (I)V
#32 = Methodref #27.#33 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#33 = NameAndType #30:#34 // println:(Ljava/lang/Object;)V
#34 = Utf8 (Ljava/lang/Object;)V
#35 = Utf8 SourceFile
#36 = Utf8 FinalKeywordTest.java
#37 = Utf8 EnclosingMethod
#38 = Class #39 // com/kmter/test/FinalKeywordTest
#39 = Utf8 com/kmter/test/FinalKeywordTest
#40 = NameAndType #41:#42 // main:([Ljava/lang/String;)V
#41 = Utf8 main
#42 = Utf8 ([Ljava/lang/String;)V
#43 = Utf8 InnerClasses
{
com.kmter.test.FinalKeywordTest$1(com.kmter.test.Person);
flags:
//默认构造函数字节码
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #10 // Field val$person:Lcom/kmter/test/Person;
//将实例域person赋值为常量池中的#10(这里是关键,将main方法中的person对象在本类中认为是一个实例变量)
5: aload_0
6: invokespecial #12 // Method java/lang/Thread."<init>":()V
9: return
LineNumberTable:
line 1: 0
line 7: 5
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/kmter/test/FinalKeywordTest$1;
//run方法的字节码
public void run();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
3: bipush 11 //将单字节常量11压入栈顶
5: invokevirtual #26 // Method java/io/PrintStream.println:(I)V
//执行println(int)方法
8: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_0 //将this指针压入栈顶
12: getfield #10 // Field val$person:Lcom/kmter/test/Person;
//拿到person对象并将其压入栈顶
15: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
//执行println(Object)方法
18: return
LineNumberTable:
line 11: 0
line 12: 8
line 13: 18
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 this Lcom/kmter/test/FinalKeywordTest$1;
}
通过上面的字节码分析我们可以发现匿名内部类访问本地变量的时候是直接将本地对象的引用当作了该类中的一个实例变量来处理的,因为是由final修饰,所以不用担心指针发生更改