日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

java class 文件分析_大概优秀的java程序员都要会分析class文件吧

發(fā)布時間:2025/3/15 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java class 文件分析_大概优秀的java程序员都要会分析class文件吧 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

相信大家在學(xué)java的時候都會聽到這樣的一些結(jié)論:

enum 是一個類

泛型的實現(xiàn)使用了類型擦除技術(shù)

非靜態(tài)內(nèi)部類持有外部類的引用

需要將自由變量聲明成final才能給匿名內(nèi)部類訪問

...

初學(xué)的時候的時候可能在書上讀過,但是很容易就會忘記,等到踩坑踩多了,就會形成慢慢記住。但是很多的同學(xué)也只是記住了而已,對于實際的原理或者原因并不了解。

這篇文章的目的主要就是教會大家查看java的字節(jié)碼,然后懂得去分析這些結(jié)論背后的原理。

枚舉最后會被編譯成一個類

我們先從簡單的入手.

java的新手對于枚舉的理解可能是:存儲幾個固定值的集合,例如下面的Color枚舉,使用的時候最多也就通過ordinal()方法獲取下枚舉的序號或者從Color.values()里面使用序號拿到一個Color:

public enum Color {

RED,

GREEN,

BLUE

}

int index = Color.BLUE.ordinal();

Color color = Color.values()[index];

如果是從C/C++過來的人比如我,很容易形成這樣一種固定的思維:枚舉就是一種被命名的整型的集合。

在c/c++里面這種想法還能說的過去,但是到了java就大錯特錯了,錯過了java枚舉的一些好用的特性。

還是拿我們上面的Color枚舉,顏色我們經(jīng)常使用0xFF0000這樣的16進(jìn)制整型或者“#FF0000”這樣的字符串去表示。

在java中,我們可以這樣將這個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這個枚舉,增加了兩個成員變量用來存整型和字符串的表示,然后還提供兩個get方法給外部獲取。

甚至進(jìn)一步的,枚舉的一種比較常用的技巧就是在static塊中創(chuàng)建映射:

public enum Color {

RED(0xFF0000, "#FF0000"),

GREEN(0x00FF00, "#00FF00"),

BLUE(0x0000FF, "#0000FF");

private static final Map 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());

看起來是不是感覺和一個類的用法很像?"enum 是一個類"這樣句話是不是講的很有道理。

當(dāng)然用法和類很像并不能說明什么。

接下來就到了我們這篇文章想講的第一個關(guān)鍵知識點了。

反編譯class文件

首先我們還是將Color簡化回最初的樣子,然后保存在Color.java文件中:

// Color.java

public enum Color {

RED,

GREEN,

BLUE

}

然后通過javac命令進(jìn)行編譯,得到Color.class

javac Color.java

得到的class文件就是jvm可以加載運行的文件,里面都是一些java的字節(jié)碼。

java其實默認(rèn)提供了一個javap命令,給我們?nèi)ゲ榭碿lass文件里面的代碼。例如,在Color.class所在的目錄使用下面命令:

javap Color

可以看到下面的輸出:

Compiled from "Color.java"

public final class Color extends java.lang.Enum {

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文件里面實際上是被編譯成了一個繼承java.lang.Enum的類,而我們定義的RED、GREEN、BLUE實際上是這個類的靜態(tài)成員變量。

這么去看的話我們那些加成員變量、加方法的操作是不是就變得很常規(guī)了?

所以說"enum 是一個類"的意思其實是enum會被java編譯器編譯成一個繼承java.lang.Enum的類!

java運行時棧幀

相信大家都知道,java虛擬機(jī)里面的方法調(diào)用是以方法棧的形式去執(zhí)行的.壓人棧內(nèi)的元素就叫做棧幀.

一書中是這么介紹棧幀的:

棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運行時數(shù)據(jù)區(qū)的虛擬機(jī)棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表,操作數(shù)棧,動態(tài)連接和方法返回地址等信息。第一個方法從調(diào)用開始到執(zhí)行完成,就對應(yīng)著一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程。

也就是說,java方法的調(diào)用,其實是一個個棧幀入棧出棧的過程,而棧幀內(nèi)部又包含了局部變量表,操作數(shù)棧等部分:

1.png

局部變量表和操作數(shù)棧是棧幀內(nèi)進(jìn)行執(zhí)行字節(jié)碼的重要部分.

局部變量表顧名思義,就是用來保存方法參數(shù)和方法內(nèi)部定義的局部變量的一段內(nèi)存區(qū)域.

而操作數(shù)棧也是一個后入先出的棧,程序運行過程中各種字節(jié)碼指令往其中壓入和彈出棧進(jìn)行運算的.

java字節(jié)碼分析

我們用一個簡單的代碼做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í)行過程.這是因為執(zhí)行過程都被編譯成一個個字節(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."":()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這個常量壓入操作數(shù)棧

0: bipush 12

// 彈出操作數(shù)棧頂?shù)脑?保存到局部變量表第1個位置中,即將12從棧頂彈出,保存成變量1,此時棧已空

2: istore_1

// 將21這個常量壓入操作數(shù)棧

3: bipush 21

// 彈出操作數(shù)棧頂?shù)脑?保存到局部變量表第2個位置中,即將21從棧頂彈出,保存成變量2,此時棧已空

5: istore_2

// 從局部變量表獲取第1個位置的元素,壓入操作數(shù)棧中,即將12壓入棧中

6: iload_1

// 從局部變量表獲取第2個位置的元素,壓入操作數(shù)棧中,即將21壓入棧中

7: iload_2

// 彈出操作數(shù)棧頂?shù)膬蓚€元素,進(jìn)行加法操作,得到的結(jié)果再壓入棧中,即彈出21和12相加得到33,再壓入棧中

8: iadd

// 彈出操作數(shù)棧頂?shù)脑?保存到局部變量表第3個位置中,即將33從棧頂彈出,保存成變量3,此時棧已空

9: istore_3

// 讀取System中的靜態(tài)成員變量out壓入棧中

10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;

// 從局部變量表獲取第3個位置的元素,壓入操作數(shù)棧中,即將33壓入棧中

13: iload_3

// 彈出棧頂?shù)?3,執(zhí)行String.valueOf方法,并將得到的返回值"33"壓回棧中

14: invokestatic #3 // Method java/lang/String.valueOf:(I)Ljava/lang/String;

// 彈出棧頂?shù)?#34;33"和System.out變量去執(zhí)行println方法

17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

// 退出方法

20: return

上面的的流程比較復(fù)雜,可以結(jié)合下面這個動圖理解一下:

gif1.gif

如果看的比較仔細(xì)的同學(xué)可能會有疑問,為什么舉報變量表里一開始位置0就會有個String[]在那呢?

其實這個字符串?dāng)?shù)組就是傳入的參數(shù)args,jvm會把參數(shù)都壓如舉報變量表給方法去使用,如果調(diào)用的是非靜態(tài)方法,還會將該方法的調(diào)用對象也一起壓入棧中.

可能有同學(xué)一開始會對istore、iload...這些字節(jié)碼指令的作用不那么熟悉,或者有些指令不知道有什么作用。不過這個沒有關(guān)系,不需要死記硬背,遇到的時候搜索一下就是了。

類型擦除的原理

泛型是java中十分好用且常用的技術(shù),之前也有寫過兩篇博客 《java泛型那些事》,《再談Java泛型》總結(jié)過.感興趣的同學(xué)可以去看看.

這里我們就從編譯出來的class文件里面看看泛型的實現(xiàn):

public class Test {

public static void main(String[] args) {

foo(1);

}

public static 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."":()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 foo(T);

Code:

0: aload_0

1: areturn

}

可以看到雖然聲明部分還是可以看到泛型的影子:

public static T foo(T);

但是在調(diào)用的時候?qū)嶋H上是

Method foo:(Ljava/lang/Object;)Ljava/lang/Object;

main 方法中先用iconst_1將常量1壓入棧中,然后用Integer.valueOf方法裝箱成Integer最后調(diào)用參數(shù)和返回值都是Object的foo方法.

所以說泛型的實現(xiàn)原理實際上是將類型都變成了Obejct,所以才能接收所有繼承Object的類型,但是像int,char這種不是繼承Object的類型是不能傳入的.

然后由于類型最后都被擦除剩下Object了,所以jvm是不知道原來輸入的類型的,于是乎下面的這種代碼就不能編譯通過了:

public T foo(){

return new T(); // 編譯失敗,因為T的類型最后會被擦除,變成Object

}

非靜態(tài)內(nèi)部類持有外部類的引用的原因

我們都知道非靜態(tài)內(nèi)部類是持有外部類的引用的,所以在安卓中使用Handler的話一般會聲明成靜態(tài)內(nèi)部類,然后加上弱引用去防止內(nèi)存泄露.

接下來就讓我們一起看看非靜態(tài)內(nèi)部類是怎么持有外部類的引用的。先寫一個簡單的例子:

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)得到了兩個class文件:

Test$1.class Test.class

Test.class文件好理解應(yīng)該就是Test這個類的定義,那Test$1.class定義的Test$1類又是從哪里來的呢?

這里還有個大家可能忽略的知識點,java里面變量名類名是可以包含$符號的,例如下面的代碼都是合法且可以通過編譯并且正常運行的

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

4: return

public void foo();

Code:

0: new #2 // class Test$1

3: dup

4: aload_0

5: invokespecial #3 // Method Test$1."":(LTest;)V

8: astore_1

9: return

}

我們來解析下foo方法:

// new一個Test$1類的對象,壓入棧中

0: new #2 // class Test$1

// 復(fù)制一份棧頂?shù)脑貕喝霔V?即現(xiàn)在棧里面有兩個相同的Test\$1對象

3: dup

// 將局部變量表位置為0的元素壓入棧中,由于foo方法不是靜態(tài)方法,所以這個元素實際上就是Test對象,即this

4: aload_0

// 調(diào)用Test$1(Test)這個構(gòu)造方法,它有一個Test類型的參數(shù),我們傳入的就是棧頂?shù)腡est對象,同時我們會將棧頂?shù)诙€元素Test$1對象也傳進(jìn)去(也就是說用這個Test$1對象去執(zhí)行構(gòu)造方法)。于是我們就彈出了棧頂?shù)囊粋€Test對象和一個Test$1對象

5: invokespecial #3 // Method Test$1."":(LTest;)V

// 將棧剩下的最后一個Test$1保存到局部變量表的位置1中。

8: astore_1

// 退出方法

9: return

根據(jù)上面的字節(jié)碼,我們可以逆向得到下面的代碼:

public class Test {

public void foo() {

Runnable r = new Test$1(this);

}

}

接著我們再來看看Test$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."":()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

}

這里定義了一個實現(xiàn)Runnable接口的Test$1類,它有一個參數(shù)為Test的構(gòu)造方法和一個run方法。然后還有一個Test類型的成員變量this$0。繼續(xù)解析這個兩個方法的字節(jié)碼:

Test$1(Test);

Code:

// 將局部變量表中位置為0的元素壓入棧中,由于這個方法不是靜態(tài)的,所以這個元素就是Test$1的this對象

0: aload_0

// 將局部變量表位置為1的元素壓入棧中,這個元素就是我們傳入的參數(shù)Test對象

1: aload_1

// 這里彈出棧頂?shù)膬蓚€元素,第一個Test對象,賦值給第二元素Test$1對象的this$0成員變量。也就是把我們傳進(jìn)來的Test對象保存給成員變量 this$0

2: putfield #1 // Field this$0:LTest;

// 將局部變量表中位置為0的元素壓入棧中,還是Test$1的this對象

5: aload_0

// 使用棧頂Test$1的this對象去初始化

6: invokespecial #2 // Method java/lang/Object."":()V

// 退出方法

9: return

public void run();

Code:

//拿到System的靜態(tài)成員變量out壓入棧中

0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;

// 將局部變量表中位置為0的元素壓入棧中,由于這個方法不是靜態(tài)的,所以這個元素就是Test$1的this對象

3: aload_0

// 彈出棧頂Test$1的this對象,獲取它的this$0成員變量,壓入棧中

4: getfield #1 // Field this$0:LTest;

// 彈出棧頂?shù)膖his$0對象執(zhí)行String.valueOf方法,得到的String對象壓入棧中

7: invokestatic #4 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;

// 彈出棧頂?shù)腟tring對象和System.out對象去執(zhí)行println方法,即調(diào)用System.out.println打印這個String對象

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)下面這個代碼:

public class Test {

public void foo() {

Runnable r = new Runnable() {

@Override

public void run() {

System.out.println(String.valueOf(Test.this));

}

};

}

}

編譯之后最終會生成兩個類:

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)都講完了,還剩下一個問題就當(dāng)做作業(yè)讓同學(xué)們自己嘗試這去分析吧:

需要將自由變量聲明成final才能給匿名內(nèi)部類訪問

創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎

總結(jié)

以上是生活随笔為你收集整理的java class 文件分析_大概优秀的java程序员都要会分析class文件吧的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。