java内部类final语义实现
final int x = 10; new Runnable() { @Override public void run() { System.out.println(x); } }.run();
当输出内部类字节码(javap -p -s -c -v)时,如下所示:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: bipush 10 5: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 8: return
可以看出,此常量值直接被写在内部类的临时变量中,即相当于进行了一次变量copy。
本地临时变量 引用类型
final T t = new T(); new Runnable() { @Override public void run() { System.out.println(t); } }.run();
字节码变为如下所示:
final T val$t; flags: ACC_FINAL, ACC_SYNTHETIC T$1(T); Signature: (LT;)V //构建函数的字节码 0: aload_0 1: aload_1 2: putfield #1 // Field val$t:LT; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."":()V 9: return //main函数字节码 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field val$t:LT; 7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 10: return
可以看出,这时自动生成了一个带有1个参数的构造函数,并且将相应的t值作为参数传递到内部类当中,同时设定final语义,即不能被内部类修改。
上面的是无参构造函数,如果是一个有参数的内部类呢,如下所示:
Thread thread = new Thread("thread-1") { @Override public void run() { System.out.println(t); } };
生成的字节码如下:
T$1(java.lang.String, T); Signature: (Ljava/lang/String;LT;)V
可以看出,编译器将自动对原来调用的构造函数进行了修改,将原来只需要1个参数的构造函数 修改为传2个参数,并且同时将相应的t传递进去。
引用字段,基本类型
int t = 3; private void xx() { new Runnable() { @Override public void run() { System.out.println(t); } }.run(); }
生成的字节码如下:
T$1(T); Signature: (LT;)V flags: Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LT; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."":()V 9: return
这里并没有如临时变量那样,直接在内部类中进行常量定义。为什么?因为这里的t对象随时可能被修改。
引用字段,引用类型
final String t = new String("abc"); private void xx() { new Runnable() { @Override public void run() { System.out.println(t); } }.run(); }
生成字节码如下:
final T this$0; Signature: LT; T$1(T); //内部类构造函数 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LT; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."":()V 9: return
这里,在内部类的构造函数中,直接将外部类的this传递进来了,因此在内部类的run方法中,对于t,将直接两层getField进行调用,即可以拿到相应的信息。如下所示:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:LT; 7: getfield #4 // Field T.t:Ljava/lang/String; 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: return
引用类型,引用类型,static字段
static String t = new String("abc"); private void xx() { new Runnable() { @Override public void run() { System.out.println(t); } }.run(); }
字节码如下:
final T this$0; Signature: LT; flags: ACC_FINAL, ACC_SYNTHETIC T$1(T); Signature: (LT;)V //构造函数字节码 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LT; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."":()V 9: return //run方法字节码 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: getstatic #4 // Field T.t:Ljava/lang/String; 6: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 9: return
可以看出,即使是引用static字段,在内部类中仍然会保留外部类的引用,即达到引用目的。同时,在run方法内部,因为是static字段,因此将不再使用getField,而是使用getStatic来进行相应字段的引用。
总结
在整个内部类字节码的生成规则中,主要采用了修改构造函数的方式来将需要在整个内部类中引用的变量进行参数传递。并且,因为是内部类,构造函数是已知的,可以随意的修改。针对特定的场景,可以进行一定的优化,如常量化(临时变量基本类型)。
因为在整个JVM层,并没有针对内部类作特殊的处理,因此这些处理手法都是在编译层进行处理的。同时,在语言层,针对这些生成的信息进行指定的说明。如SYNTHETIC语义。
在反射字段Member层,定义了如下方法:
/** * Returns {@code true} if this member was introduced by * the compiler; returns {@code false} otherwise. * * @return true if and only if this member was introduced by * the compiler. * @jls 13.1 The Form of a Binary * @since 1.5 */ public boolean isSynthetic();
即此信息是由编译器引入的。
了解这些对于整个语言层有一定的理解意义,但并不代表将来这些不会会改变,了解一些实现细节有助于自己在代码实现层有进一步的思考空间,并不局限于之前所了解的信息。
推荐阅读
-
13_Java面向对象_第13天(static、final、匿名对象、内部类、包、修饰符、代码块)_讲义
-
JAVA-this关键字JAVAsuper关键字JAVA-static修饰符和final以代码块内部类和枚举
-
Java利用文件输入输出流实现文件夹内所有文件拷贝到另一个文件夹
-
Java—final关键字、权限修饰符、内部类
-
Java基础06-final、访问修饰符、内部类、引用类型用法总结
-
java基础第九篇之final和内部类等
-
对JAVA中内部类(匿名内部类)访问的局部变量为何要用final修饰的讨论
-
Java学习笔记23---内部类之局部内部类只能访问final的局部变量
-
夯实Java基础系列18:深入理解Java内部类及其实现原理
-
如何用匿名内部类实现 Java 同步回调