java代碼是經過java編譯器編譯成class文件,而後由jvm加載執行的,jvm屏蔽了底層平臺系統執行細節,因此能夠作到Compile Once,Run Anywhere。java
編譯後的class文件,是一個二進制流文件,例以下面的類:bootstrap
public class ServiceResult<T> {
private static final int SUCCESS_CODE = 200;
private static final String SUCCESS_MESSAGE = "Success";
private int code;
private String message;
private T data;
public ServiceResult(T data) {
this.code = SUCCESS_CODE;
this.message = SUCCESS_MESSAGE;
this.data = data;
}
public ServiceResult(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public boolean isSuccess() {
return code == SUCCESS_CODE;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public T getData() {
return data;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ServiceResult{");
sb.append("code=").append(code);
sb.append(", message='").append(message).append('\'');
sb.append(", data=").append(data);
sb.append('}');
return sb.toString();
}
}
複製代碼
編譯後獲得的class,以16進制格式打開以下:數組
注:class文件以字節(8比特)爲單位,用u1,u2,u4,u8分別表示1個字節,2個字節,4個字節,8個字節的無符號數,採用Big-edian形式,即高位字節在前。微信
二進制class文件若是用類c語言結構體的形式來描述其邏輯結構,則以下圖所示:app
從圖中可知,class文件主要包含magic,minor version,major version,constant pool,access flags,this_class,super class,interfaces,fields,methods,attributes 11個部分,每一個部分之間緊湊的拼接在一塊兒,沒有分界符分割,下面分別介紹每一個結構。jvm
在開始介紹各個結構以前,須要說明本文以jvm1.8爲準ide
本文有些長,這裏排版看起來更舒服些工具
魔數: 佔4個字節的無符號數,固定爲0xCAFEBABE,用來標識改文件是一個class文件ui
次版本號: 佔兩個字節的無符號數,範圍0~65535,與major version一塊兒表示當前class文件的版本,jvm能夠向前兼容以前的版本,但不能向後兼容,即jdk7的虛擬機不能運行jdk8編譯的classthis
主版本號: 佔兩個字節的無符號數,jdk1.1使用的主版本號是45,之後每一個大版本加1,如jdk1.8爲52
常量池: 常量池是class中十分重要的一部分,它可不是隻保存着類中定義的常量而已,還保存着class文件中的各類元數據,包括一些字符串,類名,接口名,字段名,方法名等等……,它的做用就是被引用,常量池部分首先有兩個字節u2記錄它包含的常量個數。
PS1:常量池就是一系列常量的數組,它的下標是從1開始的,即有效大小是constant_pool_count-1,第0項是無效的,有些結構能夠用索引0來表示沒有對常量的引用
PS2:常量池的設計有效的減少的class文件的大小,想一想那些重複使用的類名稱,字符串如今只需保留一份,而且引用的地方只須要用u2保存它在常量池中的索引就能夠了
由於每一個常量都有一種具體的類型來表明不一樣的含義,光知道常量的個數還沒辦法解析出具體的常量項來,因此定義每一個常量的第一個字節u1表示該常量的類型tag,而後就能夠根據該類型常量的存儲結構來解析了。
常量的tag有CONSTANT_Utf8,CONSTANT_Integer,CONSTANT_Float,CONSTANT_Long,CONSTANT_Double,CONSTANT_Class,CONSTANT_String,CONSTANT_Fieldref,CONSTANT_Methodref,CONSTANT_InterfaceMethodref,CONSTANT_NameAndType,CONSTANT_MethodHandle,CONSTANT_MethodType,CONSTANT_InvokeDynamic等14種,下面對每種類型結構(類型+「_info」)做下介紹:
常量池中最基本的常量,用來保存一個utf8編碼字符串,如常量字符串,類名,字段名,方法名等的值都是一個對它的引用(索引)
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
複製代碼
tag=1,length表示字符串字節長度,如length=20,則表示接下來20個bytes是一個utf8編碼的字符串。
這裏補充兩點:
java使用的是可變utf8編碼:ASCII 字符('\u0001
' ~ '\u007F
',即1~127)用1個字節表示,null('\u0000
')和 '\u0080
' 到 '\u07FF
'之間的字符用2個字節表示, '\u0800
' 到 '\uFFFF
'之間的字符用3個字節表示。
逆向來看就是若是讀到一個字節最高位是0,則是一個單字節字符。
讀到一個字節最高3位是110
則是一個雙字節字符,緊接着還要再讀1個字節。
讀到一個字節最高4位是1110
,則是一個三字節字符,緊接着還要再讀2個字節。
關於如何解碼能夠查看官方文檔,在java中,咱們只須要使用new String(bytes, StandardCharset.UTF8)
便可獲得解碼字符串
length使用了u2(0-65535)來表示,則其表示的字符串最大長度爲65535
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
複製代碼
int,tag=3,接下來4個字節表示該int的值。關於CONSTANT_Integer補充如下幾點:
big-endian,字節高位在前,下文同理
若是本身解析則要像下面這樣:
int value = 0;
byte[] data = new byte[4];
is.read(data);
value = (value | (((int) data[0]) & 0xff)) << Byte.SIZE * 3;
value = (value | (((int) data[1]) & 0xff)) << Byte.SIZE * 2;
value = (value | (((int) data[2]) & 0xff)) << Byte.SIZE;
value = (value | (((int) data[3]) & 0xff));
複製代碼
咱們可使用DataInputStream的readInt()方法讀取一個int值。
java中short
, char
, byte
, boolean
使用int來表示,boolean數組則用byte數組來表示(1個byte表示1個boolean元素)
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
複製代碼
float浮點數,tag=4,接下來4個字節表示它的值,採用 IEEE 754標準定義。可使用DataInputStream的readFloat()方法讀取一個float值。
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
複製代碼
tag=5,長整數,long和double在class中用兩個部分(高位4字節,地位4字節)保存。可使用DataInputStream的readLong()方法讀取一個float值。
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
複製代碼
tag=6,雙精度浮點數,採用 IEEE 754標準定義。存儲同CONSTANT_Long同樣。
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
複製代碼
tag=7,表示一個類或接口,注意不是field的類型或method的參數類型、返回值類型。name_index是常量池索引,該索引處常量確定是一個CONSTANT_Utf8_info
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
複製代碼
tag=8,表示一個常量字符串,string_index是常量池索引,該索引處常量確定是一個CONSTANT_Utf8_info
,存儲着該字符串的值
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
複製代碼
tag=9,表示一個引用field信息,包括靜態field和實例field。
class_index是常量池中一個CONSTANT_Class_info類型常量(類/接口)索引,表示field所屬類。name_and_type_index是常量池中一個CONSTANT_NameAndType_info(見下文)類型常量索引,表示field的名稱和類型。
關於field引用解釋一下,包括下面的method,接口method引用同理:
code
field爲例,code在多個方法中都有用到,相比保存多份該field信息來說,在常量池中保存一份該field信息,而後在其餘用到的地方保存其索引顯然更合適。field_info
不要混淆CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
複製代碼
tag=10,表示一個引用method信息,包括靜態method和實例method。
class_index是常量池中一個CONSTANT_Class_info類型常量(這裏只能是類)索引,表示method所屬類。name_and_type_index是常量池中一個CONSTANT_NameAndType_info類型常量索引,表示method的名稱和參數,返回值信息。
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
複製代碼
tag=11,表示一個接口method信息。
class_index是常量池中一個CONSTANT_Class_info類型常量(這裏只能是接口)索引,表示method所屬接口。name_and_type_index同CONSTANT_Methodref_info。
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
複製代碼
tag=12,存儲field或method的名稱,類型等信息,能夠看出它又是兩個引用。name_index指向一個CONSTANT_Utf8_info,表示字段或方法的非全限定名稱。descriptor_index也指向一個CONSTANT_Utf8_info,表示該字段/方法的描述信息。
Descriptor
descriptor用一個字符串CONSTANT_Utf8_info保存。
字段描述符(FieldType),FieldType能夠是基本類型:B(byte)
C(char)
D(double)
F(float)
I(int)
J(long)
S(short)
Z(boolean)
,對象類型:L+全限定類名,數組類型:[+元素類型
int a; // I
Integer b; //Ljava/lang/Integer
double[] c; //[D
double[][] d; //[[D
Object[] e; //[Ljava/lang/Object
Object[][][] f; //[[[Ljava/lang/Object
複製代碼
方法描述符(MethodDescriptor),MethodDescriptor格式爲(參數類型)返回類型
/** * 描述符:(IDLjava/lang/Thread;)Ljava/lang/Object; */
Object m(int i, double d, Thread t) {...}
複製代碼
CONSTANT_MethodHandle_info {
u1 tag;
u1 reference_kind;
u2 reference_index;
}
複製代碼
tag=15,方法句柄,好比獲取一個類靜態字段,實例字段,調用一個方法,構造器等都會轉化成一個句柄引用。
reference_kind
Kind | Description | Interpretation |
---|---|---|
1 | REF_getField |
getfield C.f:T |
2 | REF_getStatic |
getstatic C.f:T |
3 | REF_putField |
putfield C.f:T |
4 | REF_putStatic |
putstatic C.f:T |
5 | REF_invokeVirtual |
invokevirtual C.m:(A*)T |
6 | REF_invokeStatic |
invokestatic C.m:(A*)T |
7 | REF_invokeSpecial |
invokespecial C.m:(A*)T |
8 | REF_newInvokeSpecial |
new C; dup; invokespecial C.<init>:(A*)V |
9 | REF_invokeInterface |
invokeinterface C.m:(A*)T |
f: field,m: method,:實例構造器
reference_index
CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}
複製代碼
tag=16,描述一個方法類型。descriptor_index引用一個CONSTANT_Utf8_info,表示方法的描述符
CONSTANT_InvokeDynamic_info {
u1 tag;
u2 bootstrap_method_attr_index;
u2 name_and_type_index;
}
複製代碼
tag=18,invokedynamic動態調用指令引用信息。
access flags表示類,接口,字段,方法的訪問控制和修飾信息。
Access Flag(u2) | Value | 做用對象 |
---|---|---|
ACC_PUBLIC | 0x0001 | class, inner, field, method |
ACC_PRIVATE | 0x0002 | inner, field, method |
ACC_PROTECTED | 0x0004 | inner, field, method |
ACC_STATIC | 0x0008 | inner, field, method |
ACC_FINAL | 0x0010 | class, inner, field, method |
ACC_SUPER | 0x0020 | class |
ACC_SYNCHRONIZED | 0x0020 | method |
ACC_VOLATILE | 0x0040 | field |
ACC_BRIDGE | 0x0040 | method |
ACC_TRANSIENT | 0x0080 | field |
ACC_VARARGS | 0x0080 | method |
ACC_NATIVE | 0x0100 | method |
ACC_INTERFACE | 0x0200 | class, inner |
ACC_ABSTRACT | 0x0400 | class, inner, method |
ACC_STRICT | 0x0800 | method |
ACC_SYNTHETIC | 0x1000 | class, inner, field, method |
ACC_ANNOTATION | 0x2000 | class, inner |
ACC_ENUM | 0x4000 | class, inner, field |
其中大部分都能見名知意,補充如下幾點:
當前類或接口,指向一個CONSTANT_Class_info常量,能夠從中解析當前類的全限定名稱。包名層次用/
分割,而不是.
,如java/lang/Object
。
當前類的直接父類索引,指向一個CONSTANT_Class_info常量,當沒有直接父類時super_class=0
首先用u2代表當前類或接口的直接父接口數量n。緊接着n個u2組成的數組便是這些父接口在常量池的索引,類型是CONSTANT_Class_info,按聲明順序從左至右。
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製代碼
field_info保存當前類的fields信息。很簡單,其中大部分前面都講過了,關於attributes放在下文第11節專門講解。須要注意的是fields只包含當前類的字段,如A的內部類B的字段c,則是在類A$B中
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製代碼
保存當前類的方法信息,同field_info
屬性表:屬性存在與ClassFile
, field_info
, method_info
中,此外Code屬性中又包含嵌套屬性信息,屬性用來描述指令碼,異常,註解,泛型等信息,JLS8預約義了23種屬性,每種屬性結構不一樣(變長),但能夠抽象成下面通用結構。
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
複製代碼
attribute_name_index:是該屬性名稱在常量池中的索引,經過該名稱才能夠斷定當前屬性屬於具體哪種,如「Code」表示當前是一個Code_attribute
attribute_length:表示接下來多少字節是該屬性的內容信息,java容許自定義新的屬性,若是jvm不認識,則按通用結構直接讀取attribute_length個字節。
23種屬性按做用能夠分爲3組:
注:後面我會介紹如何解析class,因此本文只對每一個屬性的結構和做用作一個簡單介紹
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
複製代碼
存在於field_info,表明一個常量值,如private final int x = 5
中的5
。attribute_name_index引用的值是「ConstantValue」,attribute_length固定爲2,接下來兩個字節的constantvalue_index是該常量值在常量池中的索引,是CONSTANT_Long,CONSTANT_Float,CONSTANT_Double,CONSTANT_Integer,CONSTANT_String的一種。
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製代碼
描述方法體編譯後的字節碼指令。前面講過描述方法的method_info
結構,而方法的方法體信息就存在它的屬性表中code屬性內。若是是抽象方法,那就沒有這個屬性。
前面在講屬性通用結構attribute_info
的時候已經講過attribute_name_index
, attribute_length
,它是每一個屬性都有的,下文就不在說明了,只對其餘部分介紹。
max_stack , 操做數棧的最大深度,用來分配棧的大小
max_locals, 方法棧幀中局部變量表最大容量,存儲局部變量,方法參數,異常參數等。以slot爲單位,32bit之內的變量用分配1個slot,大於32bit,如long、double分配2個slot,注意對象存的是引用。另外指出一點,對於實例方法,默認會傳入this對象指針,因此這時的max_locals最小爲1。
code[code_length],存儲字節碼指令列表,每條字節碼指令是一個byte,這樣8bit最多能夠表示256條不一樣指令,須要指出的是這個字節流數組存的不全是指令,有的指令還有對應的操做數,跳過相應n個字節的操做數再日後纔是下一條指令,詳細內容我會在另外的文章中演示。
exception_table[exception_table_length],方法異常表,注意不是方法聲明拋出的異常,而是顯示try-catch的異常,每一個catch的異常時exception_table的一項。
這幾項表示的意思是:若是在[start_pc, end_pc)區間發生了catch_type類型或其子類的異常(catch_type=0表示捕獲任意異常),則跳轉至handler_pc處的指令繼續執行。
補充三點:
1)關於finaly塊中的指令採用的方式是在每一個代碼分支中冗餘一份。
2)關於未顯示捕獲的異常則經過athrow
指令繼續拋出
3)雖然指令長度code_length是u4,但start_pc,end_pc,handler_pc都只有2個字節的無符號數u2,最大表示範圍只有65535,所以方法最多隻能有65535條指令(每條指令都不帶操做數的狀況下)
attributes[attributes_count],嵌套屬性列表
StackMapTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_entries;
stack_map_frame entries[number_of_entries];
}
複製代碼
上面講到Code_attribute中也能夠包含屬性表,StackMapTable就位於Code屬性的屬性表中,它是爲了在jvm字節碼驗證階段作類型推導驗證而添加的
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}
複製代碼
表示經過throws
聲明的可能拋出的異常,結構很簡單exception_index_table每一項u2指向一個CONSTANT_Class_info常量
BootstrapMethods_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 num_bootstrap_methods;
{ u2 bootstrap_method_ref;
u2 num_bootstrap_arguments;
u2 bootstrap_arguments[num_bootstrap_arguments];
} bootstrap_methods[num_bootstrap_methods];
}
複製代碼
位於ClassFile中,保存 invokedynamic 指令引用的引導方法
CONSTANT_String_info
, CONSTANT_Class_info
, CONSTANT_Integer_info
, CONSTANT_Long_info
, CONSTANT_Float_info
, CONSTANT_Double_info
, CONSTANT_MethodHandle_info
, or CONSTANT_MethodType_info
引用InnerClasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes;
{ u2 inner_class_info_index;
u2 outer_class_info_index;
u2 inner_name_index;
u2 inner_class_access_flags;
} classes[number_of_classes];
}
複製代碼
記錄內部類信息,classes就是當前類的內部類列表,其中inner_class_info_index,outer_class_info_index指向CONSTANT_Class型常量,分別表明內部類和外部類信息引用,inner_name_index是內部類名稱的引用(CONSTANT_Utf8_info),等於0則表明是匿名內部類,inner_class_access_flags是內部類訪問標誌,同access_flags
EnclosingMethod_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 class_index;
u2 method_index;
}
複製代碼
位於ClassFile結構中,存儲局部類或匿名類信息。
Synthetic_attribute {
u2 attribute_name_index;
u4 attribute_length;
}
複製代碼
標記是否類、方法、字段爲編譯器生成,與ACC_SYNTHETIC同義,attribute_length=0,存在該屬性則表示true。
Signature_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 signature_index;
}
複製代碼
存在於類,方法,字段的屬性表中,用於存儲類,方法,字段的泛型信息(類型變量Type Variables,參數化類型Parameterized Types)。
關於泛型能夠參考這裏
RuntimeVisibleAnnotations_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 num_annotations;
annotation annotations[num_annotations];
}
複製代碼
存在於類,方法,字段,存儲運行時可見的(RetentionPolicy.RUNTIME)註解信息,能夠被反射API獲取到,關於註解能夠參考這裏
annotation結構存儲了註解名稱,元素值對的信息,具體能夠參考官方文檔,或者我後面class解析的文章
RuntimeInvisibleAnnotations_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 num_annotations;
annotation annotations[num_annotations];
}
複製代碼
與RuntimeVisibleAnnotations結構相同,但不可見,即不能被反射API獲取到,目前jvm忽略此屬性
RuntimeVisibleParameterAnnotations_attribute {
u2 attribute_name_index;
u4 attribute_length;
u1 num_parameters;
{ u2 num_annotations;
annotation annotations[num_annotations];
} parameter_annotations[num_parameters];
}
複製代碼
存在於method_info的屬性表中,存儲運行時可見的方法參數註解信息,與RuntimeVisibleAnnotations對比發現,RuntimeVisibleParameterAnnotations存儲的是方法的參數列表上每一個參數的註解(至關與一組RuntimeVisibleParameterAnnotations),順序與方法描述符中參數順序一致
RuntimeInvisibleParameterAnnotations_attribute {
u2 attribute_name_index;
u4 attribute_length;
u1 num_parameters;
{ u2 num_annotations;
annotation annotations[num_annotations];
} parameter_annotations[num_parameters];
}
複製代碼
不想再囉嗦了
RuntimeVisibleTypeAnnotations_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 num_annotations;
type_annotation annotations[num_annotations];
}
複製代碼
存在於class_file,method_info,field_info,code的屬性表中,java8新增。JLS8新增兩種ElementType(ElementType.TYPE_PARAMETER, ElementType.TYPE_USE),相應用來描述的註解屬性也作了相應的改的,就有了該屬性,type_annotation存儲着註解信息及其做用對象。
RuntimeInvisibleTypeAnnotations_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 num_annotations;
type_annotation annotations[num_annotations];
}
複製代碼
略。。。
AnnotationDefault_attribute {
u2 attribute_name_index;
u4 attribute_length;
element_value default_value;
}
複製代碼
存在於method_info
屬性表 ,記錄註解元素的默認值
MethodParameters_attribute {
u2 attribute_name_index;
u4 attribute_length;
u1 parameters_count;
{ u2 name_index;
u2 access_flags;
} parameters[parameters_count];
}
複製代碼
存在於method_info
屬性表 ,記錄方法參數信息,name_index形參名稱,access_flags有ACC_FINAL,ACC_SYNTHETIC,ACC_MANDATED
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
複製代碼
class_file屬性表中,記錄生成該的文件名,異常堆棧可能顯示此信息,通常與類名相同,但內部類不是。這是一個可選屬性,意味着不強制編譯器生成此信息。
SourceDebugExtension_attribute {
u2 attribute_name_index;
u4 attribute_length;
u1 debug_extension[attribute_length];
}
複製代碼
存在於class結構中,可選,保存非java語言的擴展調試信息。debug_extension
數組是指向CONSTAN_Utf8_info的索引
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
複製代碼
code的屬性表中,存儲源碼行號與字節碼偏移量(方法第幾條指令)之間映射關係,start_pc字節碼偏移量,line_number源碼行號,可選。
問題:在錯誤堆棧中如何打印出出錯的源碼行號的?如何支持在源碼上斷點調試?
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
複製代碼
code的屬性表中,存儲棧幀中局部變量表的變量與源碼中定義的變量的映射,能夠在解析code屬性時關聯到局部變量表變量在源碼中的變量名等,可選。
LocalVariableTypeTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_type_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 signature_index;
u2 index;
} local_variable_type_table[local_variable_type_table_length];
}
複製代碼
code的屬性表中,與LocalVariableTable類似,signature_index也引用一個CONSTANT_Utf8_info
常量,對應含有泛型的變量會同時存儲到LocalVariableTable和LocalVariableTypeTable中個一份
Deprecated_attribute {
u2 attribute_name_index;
u4 attribute_length;
}
複製代碼
類、方法、字段過時標記,沒有額外信息,attribute_length=0,若是出現該屬性則說明加了@deprecated註解
完!若是以爲寫的還能夠,給個贊鼓勵一下吧!
下期預告:動手編寫一個解析class(字節碼)文件的程序
關注微信號,更多精彩等着你