相信大家在學(xué)java的時(shí)候都會(huì)聽到這樣的一些結(jié)論:
- enum 是一個(gè)類
- 泛型的實(shí)現(xiàn)使用了類型擦除技術(shù)
- 非靜態(tài)內(nèi)部類持有外部類的引用
- 需要將自由變量聲明成final才能給匿名內(nèi)部類訪問
...
初學(xué)的時(shí)候的時(shí)候可能在書上讀過,但是很容易就會(huì)忘記,等到踩坑踩多了,就會(huì)形成慢慢記住肠缔。但是很多的同學(xué)也只是記住了而已,對(duì)于實(shí)際的原理或者原因并不了解投剥。
這篇文章的目的主要就是教會(huì)大家查看java的字節(jié)碼,然后懂得去分析這些結(jié)論背后的原理。
枚舉最后會(huì)被編譯成一個(gè)類
我們先從簡(jiǎn)單的入手.
java的新手對(duì)于枚舉的理解可能是:存儲(chǔ)幾個(gè)固定值的集合,例如下面的Color枚舉,使用的時(shí)候最多也就通過ordinal()方法獲取下枚舉的序號(hào)或者從Color.values()里面使用序號(hào)拿到一個(gè)Color:
public enum Color {
RED,
GREEN,
BLUE
}
int index = Color.BLUE.ordinal();
Color color = Color.values()[index];
如果是從C/C++過來的人比如我,很容易形成這樣一種固定的思維:枚舉就是一種被命名的整型的集合老虫。
在c/c++里面這種想法還能說的過去,但是到了java就大錯(cuò)特錯(cuò)了,錯(cuò)過了java枚舉的一些好用的特性亲雪。
還是拿我們上面的Color枚舉,顏色我們經(jīng)常使用0xFF0000這樣的16進(jìn)制整型或者“#FF0000”這樣的字符串去表示撞蜂。
在java中,我們可以這樣將這個(gè)Color枚舉和整型還有字符串關(guān)聯(lián)起來:
public enum Color {
RED(0xFF0000, "#FF0000"),
GREEN(0x00FF00, "#00FF00"),
BLUE(0x0000FF, "#0000FF");
private int mIntVal;
private String mStrVal;
Color(int intVal, String strVal) {
mIntVal = intVal;
mStrVal = strVal;
}
public int getIntVal() {
return mIntVal;
}
public String getStrVal() {
return mStrVal;
}
}
System.out.println(Color.RED.getIntVal());
System.out.println(Color.RED.getStrVal());
可以看到我們給Color這個(gè)枚舉,增加了兩個(gè)成員變量用來存整型和字符串的表示,然后還提供兩個(gè)get方法給外部獲取远剩。
甚至進(jìn)一步的,枚舉的一種比較常用的技巧就是在static塊中創(chuàng)建映射:
public enum Color {
RED(0xFF0000, "#FF0000"),
GREEN(0x00FF00, "#00FF00"),
BLUE(0x0000FF, "#0000FF");
private static final Map<String, Color> sMap = new HashMap<>();
static {
for (Color color : Color.values()) {
sMap.put(color.getStrVal(), color);
}
}
public static Color getFromStrVal(String strVal){
return sMap.get(strVal);
}
private int mIntVal;
private String mStrVal;
Color(int intVal, String strVal) {
mIntVal = intVal;
mStrVal = strVal;
}
public int getIntVal() {
return mIntVal;
}
public String getStrVal() {
return mStrVal;
}
}
System.out.println(Color.getFromStrVal("#FF0000").getIntVal());
System.out.println(Color.RED.getIntVal());
看起來是不是感覺和一個(gè)類的用法很像?"enum 是一個(gè)類"這樣句話是不是講的很有道理茴恰。
當(dāng)然用法和類很像并不能說明什么竖配。
接下來就到了我們這篇文章想講的第一個(gè)關(guān)鍵知識(shí)點(diǎn)了。
反編譯class文件
首先我們還是將Color簡(jiǎn)化回最初的樣子,然后保存在Color.java文件中:
// Color.java
public enum Color {
RED,
GREEN,
BLUE
}
然后通過javac命令進(jìn)行編譯,得到Color.class
javac Color.java
得到的class文件就是jvm可以加載運(yùn)行的文件,里面都是一些java的字節(jié)碼里逆。
java其實(shí)默認(rèn)提供了一個(gè)javap命令,給我們?nèi)ゲ榭碿lass文件里面的代碼用爪。例如,在Color.class所在的目錄使用下面命令:
javap Color
可以看到下面的輸出:
Compiled from "Color.java"
public final class Color extends java.lang.Enum<Color> {
public static final Color RED;
public static final Color GREEN;
public static final Color BLUE;
public static Color[] values();
public static Color valueOf(java.lang.String);
static {};
}
是不是有種恍然大明白的感覺?Color在class文件里面實(shí)際上是被編譯成了一個(gè)繼承java.lang.Enum的類,而我們定義的RED原押、GREEN、BLUE實(shí)際上是這個(gè)類的靜態(tài)成員變量偎血。
這么去看的話我們那些加成員變量诸衔、加方法的操作是不是就變得很常規(guī)了?
所以說"enum 是一個(gè)類"的意思其實(shí)是enum會(huì)被java編譯器編譯成一個(gè)繼承java.lang.Enum的類!
java運(yùn)行時(shí)棧幀
相信大家都知道,java虛擬機(jī)里面的方法調(diào)用是以方法棧的形式去執(zhí)行的.壓人棧內(nèi)的元素就叫做棧幀.
<深入理解java虛擬機(jī)>一書中是這么介紹棧幀的:
棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的虛擬機(jī)棧(Virtual Machine Stack)的棧元素颇玷。棧幀存儲(chǔ)了方法的局部變量表笨农,操作數(shù)棧,動(dòng)態(tài)連接和方法返回地址等信息帖渠。第一個(gè)方法從調(diào)用開始到執(zhí)行完成谒亦,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
也就是說,java方法的調(diào)用,其實(shí)是一個(gè)個(gè)棧幀入棧出棧的過程,而棧幀內(nèi)部又包含了局部變量表,操作數(shù)棧等部分:
局部變量表和操作數(shù)棧是棧幀內(nèi)進(jìn)行執(zhí)行字節(jié)碼的重要部分.
局部變量表顧名思義,就是用來保存方法參數(shù)和方法內(nèi)部定義的局部變量的一段內(nèi)存區(qū)域.
而操作數(shù)棧也是一個(gè)后入先出的棧,程序運(yùn)行過程中各種字節(jié)碼指令往其中壓入和彈出棧進(jìn)行運(yùn)算的.
java字節(jié)碼分析
我們用一個(gè)簡(jiǎn)單的代碼做demo:
// Test.java
public class Test {
public static void main(String[] args) {
int a = 12;
int b = 21;
int c = a + b;
System.out.println(String.valueOf(c));
}
}
首先使用javac命令編譯代碼,然后使用javap命令查看字節(jié)碼:
javac Test.java
javap Test
得到下面的輸出:
Compiled from "Test.java"
public class Test {
public Test();
public static void main(java.lang.String[]);
}
可以看到這里只有方法的聲明,并沒有具體的代碼執(zhí)行過程.這是因?yàn)閳?zhí)行過程都被編譯成一個(gè)個(gè)字節(jié)碼指令了.
我們可以用javap -c命令被這些指令也顯示出來:
javap -c Test
輸出為:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 12
2: istore_1
3: bipush 21
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokestatic #3 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return
}
我們來一步步分析main方法里面的字節(jié)碼指令:
// 將12這個(gè)常量壓入操作數(shù)棧
0: bipush 12
// 彈出操作數(shù)棧頂?shù)脑?保存到局部變量表第1個(gè)位置中,即將12從棧頂彈出,保存成變量1,此時(shí)棧已空
2: istore_1
// 將21這個(gè)常量壓入操作數(shù)棧
3: bipush 21
// 彈出操作數(shù)棧頂?shù)脑?保存到局部變量表第2個(gè)位置中,即將21從棧頂彈出,保存成變量2,此時(shí)棧已空
5: istore_2
// 從局部變量表獲取第1個(gè)位置的元素,壓入操作數(shù)棧中,即將12壓入棧中
6: iload_1
// 從局部變量表獲取第2個(gè)位置的元素,壓入操作數(shù)棧中,即將21壓入棧中
7: iload_2
// 彈出操作數(shù)棧頂?shù)膬蓚€(gè)元素,進(jìn)行加法操作,得到的結(jié)果再壓入棧中,即彈出21和12相加得到33,再壓入棧中
8: iadd
// 彈出操作數(shù)棧頂?shù)脑?保存到局部變量表第3個(gè)位置中,即將33從棧頂彈出,保存成變量3,此時(shí)棧已空
9: istore_3
// 讀取System中的靜態(tài)成員變量out壓入棧中
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 從局部變量表獲取第3個(gè)位置的元素,壓入操作數(shù)棧中,即將33壓入棧中
13: iload_3
// 彈出棧頂?shù)?3,執(zhí)行String.valueOf方法,并將得到的返回值"33"壓回棧中
14: invokestatic #3 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
// 彈出棧頂?shù)?33"和System.out變量去執(zhí)行println方法
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 退出方法
20: return
上面的的流程比較復(fù)雜空郊,可以結(jié)合下面這個(gè)動(dòng)圖理解一下:
如果看的比較仔細(xì)的同學(xué)可能會(huì)有疑問份招,為什么舉報(bào)變量表里一開始位置0就會(huì)有個(gè)String[]在那呢?
其實(shí)這個(gè)字符串?dāng)?shù)組就是傳入的參數(shù)args,jvm會(huì)把參數(shù)都?jí)喝缗e報(bào)變量表給方法去使用,如果調(diào)用的是非靜態(tài)方法,還會(huì)將該方法的調(diào)用對(duì)象也一起壓入棧中.
可能有同學(xué)一開始會(huì)對(duì)istore狞甚、iload...這些字節(jié)碼指令的作用不那么熟悉,或者有些指令不知道有什么作用锁摔。不過這個(gè)沒有關(guān)系,不需要死記硬背哼审,遇到的時(shí)候搜索一下就是了谐腰。
類型擦除的原理
泛型是java中十分好用且常用的技術(shù),之前也有寫過兩篇博客 《java泛型那些事》,《再談Java泛型》總結(jié)過.感興趣的同學(xué)可以去看看.
這里我們就從編譯出來的class文件里面看看泛型的實(shí)現(xiàn):
public class Test {
public static void main(String[] args) {
foo(1);
}
public static <T> T foo(T a){
return a;
}
}
讓我們使用"javap -c"命令看看它生成的class文件是怎樣的:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: invokestatic #3 // Method foo:(Ljava/lang/Object;)Ljava/lang/Object;
7: pop
8: return
public static <T> T foo(T);
Code:
0: aload_0
1: areturn
}
可以看到雖然聲明部分還是可以看到泛型的影子:
public static <T> T foo(T);
但是在調(diào)用的時(shí)候?qū)嶋H上是
Method foo:(Ljava/lang/Object;)Ljava/lang/Object;
main 方法中先用iconst_1將常量1壓入棧中,然后用Integer.valueOf方法裝箱成Integer最后調(diào)用參數(shù)和返回值都是Object的foo方法.
所以說泛型的實(shí)現(xiàn)原理實(shí)際上是將類型都變成了Obejct,所以才能接收所有繼承Object的類型,但是像int,char這種不是繼承Object的類型是不能傳入的.
然后由于類型最后都被擦除剩下Object了,所以jvm是不知道原來輸入的類型的,于是乎下面的這種代碼就不能編譯通過了:
public <T> T foo(){
return new T(); // 編譯失敗,因?yàn)門的類型最后會(huì)被擦除,變成Object
}
非靜態(tài)內(nèi)部類持有外部類的引用的原因
我們都知道非靜態(tài)內(nèi)部類是持有外部類的引用的,所以在安卓中使用Handler的話一般會(huì)聲明成靜態(tài)內(nèi)部類,然后加上弱引用去防止內(nèi)存泄露.
接下來就讓我們一起看看非靜態(tài)內(nèi)部類是怎么持有外部類的引用的。先寫一個(gè)簡(jiǎn)單的例子:
public class Test {
public void foo() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(String.valueOf(Test.this));
}
};
}
}
通過javac命令編譯之后發(fā)現(xiàn)得到了兩個(gè)class文件:
Test$1.class Test.class
Test.class文件好理解應(yīng)該就是Test這個(gè)類的定義,那Test$1.class定義的Test$1類又是從哪里來的呢涩盾?
這里還有個(gè)大家可能忽略的知識(shí)點(diǎn),java里面變量名類名是可以包含$符號(hào)的,例如下面的代碼都是合法且可以通過編譯并且正常運(yùn)行的
int x$y = 123;
System.out.println(x$y);
回到正題,讓我們先來用"javap -c"命令看看Test.class里面的內(nèi)容:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void foo();
Code:
0: new #2 // class Test$1
3: dup
4: aload_0
5: invokespecial #3 // Method Test$1."<init>":(LTest;)V
8: astore_1
9: return
}
我們來解析下foo方法:
// new一個(gè)Test$1類的對(duì)象,壓入棧中
0: new #2 // class Test$1
// 復(fù)制一份棧頂?shù)脑貕喝霔V?即現(xiàn)在棧里面有兩個(gè)相同的Test\$1對(duì)象
3: dup
// 將局部變量表位置為0的元素壓入棧中,由于foo方法不是靜態(tài)方法,所以這個(gè)元素實(shí)際上就是Test對(duì)象,即this
4: aload_0
// 調(diào)用Test$1(Test)這個(gè)構(gòu)造方法,它有一個(gè)Test類型的參數(shù),我們傳入的就是棧頂?shù)腡est對(duì)象,同時(shí)我們會(huì)將棧頂?shù)诙€(gè)元素Test$1對(duì)象也傳進(jìn)去(也就是說用這個(gè)Test$1對(duì)象去執(zhí)行構(gòu)造方法)十气。于是我們就彈出了棧頂?shù)囊粋€(gè)Test對(duì)象和一個(gè)Test$1對(duì)象
5: invokespecial #3 // Method Test$1."<init>":(LTest;)V
// 將棧剩下的最后一個(gè)Test$1保存到局部變量表的位置1中。
8: astore_1
// 退出方法
9: return
根據(jù)上面的字節(jié)碼,我們可以逆向得到下面的代碼:
public class Test {
public void foo() {
Runnable r = new Test$1(this);
}
}
接著我們?cè)賮砜纯碩est$1.class:
Compiled from "Test.java"
class Test$1 implements java.lang.Runnable {
final Test this$0;
Test$1(Test);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LTest;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
public void run();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #1 // Field this$0:LTest;
7: invokestatic #4 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: return
}
這里定義了一個(gè)實(shí)現(xiàn)Runnable接口的Test$1類旁赊,它有一個(gè)參數(shù)為Test的構(gòu)造方法和一個(gè)run方法桦踊。然后還有一個(gè)Test類型的成員變量this$0。繼續(xù)解析這個(gè)兩個(gè)方法的字節(jié)碼:
Test$1(Test);
Code:
// 將局部變量表中位置為0的元素壓入棧中,由于這個(gè)方法不是靜態(tài)的,所以這個(gè)元素就是Test$1的this對(duì)象
0: aload_0
// 將局部變量表位置為1的元素壓入棧中,這個(gè)元素就是我們傳入的參數(shù)Test對(duì)象
1: aload_1
// 這里彈出棧頂?shù)膬蓚€(gè)元素,第一個(gè)Test對(duì)象,賦值給第二元素Test$1對(duì)象的this$0成員變量终畅。也就是把我們傳進(jìn)來的Test對(duì)象保存給成員變量 this$0
2: putfield #1 // Field this$0:LTest;
// 將局部變量表中位置為0的元素壓入棧中,還是Test$1的this對(duì)象
5: aload_0
// 使用棧頂Test$1的this對(duì)象去初始化
6: invokespecial #2 // Method java/lang/Object."<init>":()V
// 退出方法
9: return
public void run();
Code:
//拿到System的靜態(tài)成員變量out壓入棧中
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
// 將局部變量表中位置為0的元素壓入棧中,由于這個(gè)方法不是靜態(tài)的,所以這個(gè)元素就是Test$1的this對(duì)象
3: aload_0
// 彈出棧頂Test$1的this對(duì)象,獲取它的this$0成員變量,壓入棧中
4: getfield #1 // Field this$0:LTest;
// 彈出棧頂?shù)膖his$0對(duì)象執(zhí)行String.valueOf方法,得到的String對(duì)象壓入棧中
7: invokestatic #4 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
// 彈出棧頂?shù)腟tring對(duì)象和System.out對(duì)象去執(zhí)行println方法,即調(diào)用System.out.println打印這個(gè)String對(duì)象
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 退出方法
13: return
來來來,我們繼續(xù)腦補(bǔ)它的源代碼:
public class Test$1 implements java.lang.Runnable {
final Test this$0;
public Test$1(Test test) {
this$0 = test;
}
@Override
public void run() {
System.out.println(String.valueOf(this$0));
}
}
所以我們通過字節(jié)碼,發(fā)現(xiàn)下面這個(gè)代碼:
public class Test {
public void foo() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(String.valueOf(Test.this));
}
};
}
}
編譯之后最終會(huì)生成兩個(gè)類:
public class Test {
public void foo() {
Runnable r = new Test$1(this);
}
}
public class Test$1 implements java.lang.Runnable {
final Test this$0;
public Test$1(Test test) {
this$0 = test;
}
@Override
public void run() {
System.out.println(String.valueOf(this$0));
}
}
這就是非靜態(tài)內(nèi)部類持有外部類的引用的原因啦籍胯。
到這里這篇文章想講的東西就已經(jīng)都講完了,還剩下一個(gè)問題就當(dāng)做作業(yè)讓同學(xué)們自己嘗試這去分析吧:
需要將自由變量聲明成final才能給匿名內(nèi)部類訪問