从反编译认识内部类

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

  在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是Outter.java的代码:


[java]  view plain  copy
 print ?
  1. public class Outter {  
  2.     private Inner inner = null;  
  3.     public Outter() {  
  4.            
  5.     }  
  6.        
  7.     public Inner getInnerInstance() {  
  8.         if(inner == null)  
  9.             inner = new Inner();  
  10.         return inner;  
  11.     }  
  12.         
  13.     protected class Inner {  
  14.         public Inner() {  
  15.                
  16.         }  
  17.     }  
  18. }  


  编译之后,出现了两个字节码文件:


  反编译Outter$Inner.class文件得到下面信息:


[plain]  view plain  copy
 print ?
  1. E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner  
  2. Compiled from "Outter.java"  
  3. public class com.cxh.test2.Outter$Inner extends java.lang.Object  
  4.   SourceFile: "Outter.java"  
  5.   InnerClass:  
  6.    #24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes  
  7. t2/Outter  
  8.   minor version: 0  
  9.   major version: 50  
  10.   Constant pool:  
  11. const #1 = class        #2;     //  com/cxh/test2/Outter$Inner  
  12. const #2 = Asciz        com/cxh/test2/Outter$Inner;  
  13. const #3 = class        #4;     //  java/lang/Object  
  14. const #4 = Asciz        java/lang/Object;  
  15. const #5 = Asciz        this$0;  
  16. const #6 = Asciz        Lcom/cxh/test2/Outter;;  
  17. const #7 = Asciz        <init>;  
  18. const #8 = Asciz        (Lcom/cxh/test2/Outter;)V;  
  19. const #9 = Asciz        Code;  
  20. const #10 = Field       #1.#11; //  com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t  
  21. est2/Outter;  
  22. const #11 = NameAndType #5:#6;//  this$0:Lcom/cxh/test2/Outter;  
  23. const #12 = Method      #3.#13; //  java/lang/Object."<init>":()V  
  24. const #13 = NameAndType #7:#14;//  "<init>":()V  
  25. const #14 = Asciz       ()V;  
  26. const #15 = Asciz       LineNumberTable;  
  27. const #16 = Asciz       LocalVariableTable;  
  28. const #17 = Asciz       this;  
  29. const #18 = Asciz       Lcom/cxh/test2/Outter$Inner;;  
  30. const #19 = Asciz       SourceFile;  
  31. const #20 = Asciz       Outter.java;  
  32. const #21 = Asciz       InnerClasses;  
  33. const #22 = class       #23;    //  com/cxh/test2/Outter  
  34. const #23 = Asciz       com/cxh/test2/Outter;  
  35. const #24 = Asciz       Inner;  
  36.    
  37. {  
  38. final com.cxh.test2.Outter this$0;  
  39.    
  40. public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);  
  41.   Code:  
  42.    Stack=2, Locals=2, Args_size=2  
  43.    0:   aload_0  
  44.    1:   aload_1  
  45.    2:   putfield        #10; //Field this$0:Lcom/cxh/test2/Outter;  
  46.    5:   aload_0  
  47.    6:   invokespecial   #12; //Method java/lang/Object."<init>":()V  
  48.    9:   return  
  49.   LineNumberTable:  
  50.    line 16: 0  
  51.    line 18: 9  
  52.    
  53.   LocalVariableTable:  
  54.    Start  Length  Slot  Name   Signature  
  55.    0      10      0    this       Lcom/cxh/test2/Outter$Inner;  
  56.    
  57.    
  58. }  


  第11行到35行是常量池的内容,下面逐一第38行的内容:

[java]  view plain  copy
 print ?
  1. final com.cxh.test2.Outter this$0;  

 

 这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:

[java]  view plain  copy
 print ?
  1. public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);  


  从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。所以,如果在外部类没有人引用的时候,而成员内部类有人引用,外部类因为被内部类引用所以不会被回收。这就是Android中常见的Activity内存泄露产生的原因。


2.为什么局部内部类和匿名内部类只能访问局部final变量?

  想必这个问题也曾经困扰过很多人,在讨论这个问题之前,先看下面这段代码:

[java]  view plain  copy
 print ?
  1. public class Test {  
  2.     public static void main(String[] args)  {  
  3.            
  4.     }  
  5.        
  6.     public void test(final int b) {  
  7.         final int a = 10;  
  8.         new Thread(){  
  9.             public void run() {  
  10.                 System.out.println(a);  
  11.                 System.out.println(b);  
  12.             };  
  13.         }.start();  
  14.     }  
  15. }  


  这段代码会被编译成两个class文件:Test.class和Test1.classOutterx.class(x为正整数)。

  

  根据上图可知,test方法中的匿名内部类的名字被起为 Test$1。

  上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题:

  当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制  的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:



我们看到在run方法中有一条指令:

[plain]  view plain  copy
 print ?
  1. bipush 10  


这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

  下面再看一个例子:

[java]  view plain  copy
 print ?
  1. public class Test {  
  2.     public static void main(String[] args)  {  
  3.            
  4.     }  
  5.        
  6.     public void test(final int a) {  
  7.         new Thread(){  
  8.             public void run() {  
  9.                 System.out.println(a);  
  10.             };  
  11.         }.start();  
  12.     }  
  13. }  


反编译得到:



  我们看到匿名内部类Test$1的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。

  也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

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

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

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

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


  从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。 
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值