本文描述在java内部类中,经常会引用外部类的变量信息。但是这些变量信息是如何传递给内部类的,在表面上并没有相应的线索。本文从字节码层描述在内部类中是如何实现这些语义的。

本地临时变量 基本类型

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."<init>":()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."<init>":()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."<init>":()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."<init>":()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();

即此信息是由编译器引入的。

了解这些对于整个语言层有一定的理解意义,但并不代表将来这些不会会改变,了解一些实现细节有助于自己在代码实现层有进一步的思考空间,并不局限于之前所了解的信息。

学习Java的同学注意了!!! 
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:454297367 我们一起学Java!

Java内部类final语义实现的更多相关文章

  1. 为什么java内部类访问局部变量必须声明为final?

    https://blog.csdn.net/z55887/article/details/49229491 先抛出让我疑惑了很久的一个问题 编程时,在线程中使用局部变量时候经常编译器会提示:局部变量必 ...

  2. Java内部类详解

    Java内部类详解 说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉.原因是平时编写代码时可能用到的场景不多,用得最多的是在有事件监听的情况下,并且即使用到也很少去总结内部类的用法.今天我们就 ...

  3. 黑马----JAVA内部类

    黑马程序员:Java培训.Android培训.iOS培训..Net培训 黑马程序员--JAVA内部类 一.内部类分为显式内部类和匿名内部类. 二.显式内部类 1.即显式声明的内部类,它有类名. 2.显 ...

  4. java 内部类 *** 最爱那水货

    注: 转载于http://blog.csdn.net/jiangxinyu/article/details/8177326 Java语言允许在类中再定义类,这种在其它类内部定义的类就叫内部类.内部类又 ...

  5. java内部类和匿名内部类

    内部类即是包含在类里面的又一个类. java内部类分为: 成员内部类.静态嵌套类.方法内部类.匿名内部类 . 内部类的共性 (1).内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.clas ...

  6. Java内部类小程序(成员内部类,静态内部类,匿名内部类)

    /** * 测试java内部类(成员内部类,静态内部类,匿名内部类) * 局部内部类不常用,就不写了. * @package :java05 * @author shaobn * @Describe ...

  7. [转] Java内部类详解

    作者:海子 出处:http://www.cnblogs.com/dolphin0520/ 本博客中未标明转载的文章归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置 ...

  8. java内部类的作用分析

    提起Java内部类(Inner Class)可能很多人不太熟悉,实际上类似的概念在C++里也有,那就是嵌套类(Nested Class),关于这两者的区别与联系,在下文中会有对比.内部类从表面上看,就 ...

  9. JAVA内部类(转)

    源出处:JAVA内部类 在java语言中,有一种类叫做内部类(inner class),也称为嵌入类(nested class),它是定义在其他类的内部.内部类作为其外部类的一个成员,与其他成员一样, ...

随机推荐

  1. 修改session垃圾回收几率

    <?php //修改session垃圾回收几率 ini_set('session.gc_probability','1'); ini_set('session.gc_divisor','2'); ...

  2. JavaScript学习笔记(二)——闭包、IIFE、apply、函数与对象

    一.闭包(Closure) 1.1.闭包相关的问题 请在页面中放10个div,每个div中放入字母a-j,当点击每一个div时显示索引号,如第1个div显示0,第10个显示9:方法:找到所有的div, ...

  3. Android之文件数据存储

    一.文件保存数据介绍 Activity提供了openFileOutput()方法可以用于把数据输出到文件中,具体的实现过程与在J2SE环境中保存数据到文件中是一样的.文件可用来存放大量数据,如文本.图 ...

  4. mysql删除重复记录语句的方法

    例如: id name value 1 a pp 2 a pp 3 b iii 4 b pp 5 b pp 6 c pp 7 c pp 8 c iii id是主键 要求得到这样的结果 id name ...

  5. Selenium的PO模式(Page Object Model)[python版]

     Page Object Model 简称POM  普通的测试用例代码: .... #测试用例 def test_login_mail(self): driver = self.driver driv ...

  6. 从零开始,DIY一个jQuery(3)

    在前两章,为了方便调试,我们写了一个非常简单的 jQuery.fn.init 方法: jQuery.fn.init = function (selector, context, root) { if ...

  7. 《MSSQL2008技术内幕:T-SQL语言基础》读书笔记(下)

    索引: 一.SQL Server的体系结构 二.查询 三.表表达式 四.集合运算 五.透视.逆透视及分组 六.数据修改 七.事务和并发 八.可编程对象 五.透视.逆透视及分组 5.1 透视 所谓透视( ...

  8. js中几种实用的跨域方法原理详解(转)

    今天研究js跨域问题的时候发现一篇好博,非常详细地讲解了js几种跨域方法的原理,特分享一下. 原博地址:http://www.cnblogs.com/2050/p/3191744.html 下面正文开 ...

  9. 2、利用蓝牙定位及姿态识别实现一个智能篮球场套件(二)——CC2540/CC2541基于广播的RSSI获得

    CC2541一拖多例程中RSSI获得是通过一个事件回调函数实现的,前提是需要连接上蓝牙设备. 这个对于多点定位来说是不可行的,由于主机搜索蓝牙设备过程中也能获得当前蓝牙设备的RSSI等信息,因此可基于 ...

  10. ABP源码分析十七:DTO 自动校验的实现

    对传给Application service对象中的方法的DTO参数,ABP都会在方法真正执行前自动完成validation(根据标注到DTO对象中的validate规则). ABP是如何做到的? 思 ...