美文网首页程序员
从反编译内部类来认识final

从反编译内部类来认识final

作者: 首席咸鱼腌制师 | 来源:发表于2020-06-12 00:37 被阅读0次

为什么内部类可以无条件访问外部类成员?

public class Outter {
    private Inner inner = null;
    public Outter() {
    }
}
 
public Inner getInnerInstance() {
    if(inner == null)
        inner = new Inner();
    return inner;
}
  
protected class Inner {
    public Inner() {
         
    }
}

我们通过 javap -v Outter$Inner 来反编译代码,最终得到字节码文件:

{
    // 注意这里
    final com.cxh.test2.Outter this$0;
  public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
    Code:
     Stack=2, Locals=2, Args_size=2
     0:   aload_0
     1:   aload_1
     2:   putfield        #10; //Field this$0:Lcom/cxh/test2/Outter;
     5:   aload_0
     6:   invokespecial   #12; //Method java/lang/Object."<init>":()V
     9:   return
    LineNumberTable:
     line 16: 0
     line 18: 9

    LocalVariableTable:
     Start  Length  Slot  Name   Signature
     0      10      0    this       Lcom/cxh/test2/Outter$Inner;

}

第一行就明确了一个外部类的引用,看构造方法就更清晰了:传入了一个外部类的引用。由此可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在内部类随意访问外部类的成员。

为什么局部内部类和匿名内部类访问局部变量要用final修饰?

我们先看这样一段代码

public class Test {

    public static void main(String[] args) {

    }

    public void test(final int a) {
        new Thread() {
            public void run() {
                System.out.println(a);
            }
        }.start();
    }

    public void test1(final int b) {
        final int a = 10;
        new Thread() {
            public void run() {
                System.out.println(a);
                System.out.println(b);
            }
        }.start();
    }

}

我们反编译得到的结果是:

Constant pool:
   #1 = Methodref          #9.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // com/roger/algorithm/Test$1
   #3 = Methodref          #2.#24         // com/roger/algorithm/Test$1."<init>":(Lcom/roger/algorithm/Test;I)V
   #4 = Methodref          #2.#25         // com/roger/algorithm/Test$1.start:()V
   #5 = Class              #26            // com/roger/algorithm/Test$2
   #6 = Methodref          #5.#24         // com/roger/algorithm/Test$2."<init>":(Lcom/roger/algorithm/Test;I)V
   #7 = Methodref          #5.#25         // com/roger/algorithm/Test$2.start:()V
   #8 = Class              #27            // com/roger/algorithm/Test
   #9 = Class              #28            // java/lang/Object
  #10 = Utf8               InnerClasses
  
  public void test(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=2, args_size=2
         0: new           #2                  // class com/roger/algorithm/Test$1
         3: dup
         4: aload_0
         5: iload_1
         6: invokespecial #3                  // Method com/roger/algorithm/Test$1."<init>":(Lcom/roger/algorithm/Test;I)V
         9: invokevirtual #4                  // Method com/roger/algorithm/Test$1.start:()V
        12: return
      LineNumberTable:
        line 10: 0
        line 16: 9
        line 17: 12

  public void test1(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=3, args_size=2
         0: new           #5                  // class com/roger/algorithm/Test$2
         3: dup
         4: aload_0
         5: iload_1
         6: invokespecial #6                  // Method com/roger/algorithm/Test$2."<init>":(Lcom/roger/algorithm/Test;I)V
         9: invokevirtual #7                  // Method com/roger/algorithm/Test$2.start:()V
        12: return
      LineNumberTable:
        line 21: 0
        line 26: 9
        line 27: 12

反编译结果依赖 Java1.8.0_201,注意最开始的Constant pool,在内部类的初始化方法声明了外部类引用,构造方法中的引用声明省去了,应该是JVM的一个优化。总的原理与JVM低版本相同:如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?

对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。

静态内部类有特殊的地方吗?

从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

这篇文章很好,给了我很大的帮助,我把其中的内容手动实现了一遍,感谢原作者。

https://blog.csdn.net/davidluo001/java/article/details/50377919

相关文章

  • 从反编译内部类来认识final

    为什么内部类可以无条件访问外部类成员? 我们通过 javap -v Outter$Inner 来反编译代码,最终得...

  • 类的编译期与运行期

    非静态内部类 非静态内部类到底可以有静态属性吗? static成员变量,或者static final常量 非静态内...

  • Java基础内部类与Static,final

    成员内部类,局部内部类,匿名内部类 非 静态成员内部类 非 静态成员内部类反编译class 静态内部类 静态内部类...

  • Java内部类

    代码中包含成员内部类、内部类的同名变量访问、局部内部类、局部内部类final、匿名内部类

  • Java面试核心框架

    常量池 常用关键字 final static 内部类 抽象类 接口 异常 注解 容器 内存管理内存模型、工作内...

  • 07.成员内部类的修饰符

    成员内部类 成员内部类可以使用的修饰符:private,public,procted,final,static,a...

  • Java匿名类遇上final

    时间: 2018/10/19 Content final的普通语义 final遇见内部类 闭包 内存泄漏​ 1. ...

  • javap反编译探寻内部类如何捕获final变量

    final 关键字的用法很多,其中有一种是修饰局部变量,使之能在内部类中使用。 显然,为了能在内部类中使用,fin...

  • Java学习Day06

    今日学习内容总结 final补充 权限修饰符 内部类 final final关键字代表最终的、不可改变的。 当fi...

  • java面试总结

    final匿名内部类内部类并不是直接调用方法传递的参数,而是利用构造器对外部类方法形式参数进行复制,而内部类自己方...

网友评论

    本文标题:从反编译内部类来认识final

    本文链接:https://www.haomeiwen.com/subject/xiywtktx.html