从反编译深入理解JAVA内部类类结构以及finalkeyword
在此之前,我们已经讨论过了成员内部类能够无条件訪问外部类的成员,那详细到底是怎样实现的呢?以下通过反编译字节码文件看看到底。其实,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件。以下是Outter.java的代码:
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() {
}
}
}
编译之后,出现了两个字节码文件:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
反编译Outter$Inner.class文件得到以下信息:
E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
SourceFile: "Outter.java"
InnerClass:
#24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
minor version: 0
major version: 50
Constant pool:
const #1 = class #2; // com/cxh/test2/Outter$Inner
const #2 = Asciz com/cxh/test2/Outter$Inner;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz this$0;
const #6 = Asciz Lcom/cxh/test2/Outter;;
const #7 = Asciz <init>;
const #8 = Asciz (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz Code;
const #10 = Field #1.#11; // com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;// this$0:Lcom/cxh/test2/Outter;
const #12 = Method #3.#13; // java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;// "<init>":()V
const #14 = Asciz ()V;
const #15 = Asciz LineNumberTable;
const #16 = Asciz LocalVariableTable;
const #17 = Asciz this;
const #18 = Asciz Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz SourceFile;
const #20 = Asciz Outter.java;
const #21 = Asciz InnerClasses;
const #22 = class #23; // com/cxh/test2/Outter
const #23 = Asciz com/cxh/test2/Outter;
const #24 = Asciz 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; }
第11行到35行是常量池的内容。以下逐一第38行的内容:
final com.cxh.test2.Outter this$0;
这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默觉得成员内部类加入了一个指向外部类对象的引用,那么这个引用是怎样赋初值的呢?以下接着看内部类的构造器:
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
从这里也间接说明了成员内部类是依赖于外部类的。假设没有创建外部类的对象,则无法对Outter
this&0引用进行初始化赋值。也就无法创建成员内部类的对象了。所以,假设在外部类没有人引用的时候。而成员内部类有人引用,外部类由于被内部类引用所以不会被回收。这就是Android中常见的Activity内存泄露产生的原因。
2.为什么局部内部类和匿名内部类仅仅能訪问局部final变量?
想必这个问题也以前困扰过非常多人,在讨论这个问题之前。先看以下这段代码:
public class Test {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}
这段代码会被编译成两个class文件:Test.class和Test1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class(x为正整数)。
依据上图可知,test方法中的匿名内部类的名字被起为 Test$1。
上段代码中。假设把变量a和b前面的任一个final去掉。这段代码都编译只是。我们先考虑这样一个问题:
当test方法运行完成之后,变量a的生命周期就结束了,而此时Thread对象的生命周期非常可能还没有结束,那么在Thread的run方法中继续訪问变量a就变成不可能了。可是又要实现这种效果,怎么办呢?Java採用了 复制 的手段来解决问题。将这段代码的字节码反编译能够得到以下的内容:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
我们看到在run方法中有一条指令:
bipush 10
这条指令表示将操作数10压栈。表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,假设这个变量的值在编译期间能够确定。则编译器默认会在匿名内部类(局部内部类)的常量池中加入一个内容相等的字面量或直接将对应的字节码嵌入到运行字节码中。
这样一来,匿名内部类使用的变量是还有一个局部变量,仅仅只是值和方法中局部变量的值相等,因此和方法中的局部变量全然独立开。
以下再看一个样例:
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();
}
}
反编译得到:
我们看到匿名内部类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引用的。
从反编译深入理解JAVA内部类类结构以及finalkeyword的更多相关文章
- 通过反编译深入理解Java String及intern(转)
通过反编译深入理解Java String及intern 原文传送门:http://www.cnblogs.com/paddix/p/5326863.html 一.字符串问题 字符串在我们平时的编码工作 ...
- 通过反编译深入理解Java String及intern
一.字符串问题 字符串在我们平时的编码工作中其实用的非常多,并且用起来也比较简单,所以很少有人对其做特别深入的研究.倒是面试或者笔试的时候,往往会涉及比较深入和难度大一点的问题.我在招聘的时候也偶尔会 ...
- (转)通过反编译深入理解Java String及intern
原文链接:https://www.cnblogs.com/paddix/p/5326863.html 一.字符串问题 字符串在我们平时的编码工作中用的非常多,并且用起来非常简单,所以很少有人对其做特别 ...
- 夯实Java基础系列18:深入理解Java内部类及其实现原理
本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...
- Java 干货之深入理解Java内部类
可以将一个类定义在另一个类或方法中,这样的类叫做内部类 --<Thinking in Java> 说起内部类,大家并不陌生,并且会经常在实例化容器的时候使用到它.但是内部类的具体细节语法, ...
- 深入理解Java内部类
内部类就是定义在一个类中的另外一个类,是一种从属关系.在没有实际了解内部类之前,我始终困惑,为什么要在一个类中定义另外一个类,这不是增加代码结构复杂度么?现在才大致能知道这种设计的优势是大于 ...
- 反编译Apk得到Java源代码
原文章转载自:http://hi.baidu.com/%CB%BF%D4%B5%CC%EC%CF%C2/blog/item/2284e2debafc541e495403ec.html 本人转载自:ht ...
- Java反编译工具:Java Decompiler
Java Decompiler项目旨在开发一套工具集,这套工具集可以反编译并分析Java5之后的Java字节码. 它主要包括四个部分. JD-Core:Java Decompiler的核心库,它能够根 ...
- java7(1)——反编译深入理解增强的switch(读字节命令实战)
[本文介绍] 本文主要讲java_7 的改进switch的底层实现.反编译一个使用带String的switch的demo并一步步解析反编译出来的字节命令,从编译的角度解读switch的底层实现. [正 ...
随机推荐
- UI标签库专题九:JEECG智能开发平台 Choose(选则操作标签)
1. Choose(选则操作标签) 1.1. 參数 属性名 类型 描写叙述 是否必须 默认值 hiddenName string 隐藏域的ID 否 null hiddenid string 隐藏 ...
- 知名游戏开发者称 C++ 是一种非常糟糕、可怕的语言(C++不是一门可怕的语言,可怕的是一群没有耐心的程序员来使用C++这门语言)
抛出一个问题:C++ 真的很可怕吗? 2016 年底,C++ 之父 Bjarne Stroustrup 在一次采访中表示:”C++ 让编程专家很容易编写出复杂.高性能.低资源消耗的代码,但不足以成为广 ...
- 深入分析JavaWeb Item23 -- jsp自己定义标签开发入门
一.自己定义标签的作用 自己定义标签主要用于移除Jsp页面中的java代码. 二.自己定义标签开发和使用 2.1.自己定义标签开发步骤 1.编写一个实现Tag接口的Java类(标签处理器类) 要编写一 ...
- 洛谷P1439 最长公共子序列(LCS问题)
题目描述 给出1-n的两个排列P1和P2,求它们的最长公共子序列. 输入输出格式 输入格式: 第一行是一个数n, 接下来两行,每行为n个数,为自然数1-n的一个排列. 输出格式: 一个数,即最长公共子 ...
- Oracle新建表字段,如何使字段自增
oracle的自增需要依靠序列和触发器共同实现 比如 新建一张表 create table test (id int primary key, name varchar2(10)); 创建一个序列 ...
- react-native React Native version mismatch
android/app/build.gradle file: dependencies { compile fileTree(dir: "libs", include: [ ...
- 【DRF版本】
目录 使用内置的URLPathVersioning类 使用自定义的版本控制类 首先,我们开发的项目会有多个版本. 其次,我们的项目版本会随着更新越来越多,我们不可能因出了新版本就不维护旧版本了. 那么 ...
- Linux 内建命令和系统命令
shell内建命令是指bash(或其它版本)工具集中的命令.一般都会有一个与之同名的系统命令,比如bash中的echo命令与/bin/echo是两个不同的命令,尽管他们行为大体相仿.当在bash中键入 ...
- 团队作业-Beta冲刺(2)
这个作业属于哪个课程 https://edu.cnblogs.com/campus/xnsy/SoftwareEngineeringClass2 这个作业要求在哪里 https://edu.cnblo ...
- CMake设置生成vs工程的动态库输出路径
作者:朱金灿 来源:http://blog.csdn.net/clever101 在网上搜了很多的资料,发现CMake不能设置一个动态库工程的输出目录和中间目录,难道除了VC之外其它编译器如gcc中没 ...