Java Class檔案結構
平臺無關性和語言無關性
Java在剛剛誕生的時候提出過一個宣傳口號:“一次編寫,到處執行”,這句話充分表達了軟體開發人員對衝破平臺界限的渴求。“與平臺無關”的理想最終實現在作業系統的應用層上:Sun公司及其他虛擬機器提供商釋出了許多可以執行在各種不同平臺上的虛擬機器,這些虛擬機器都可以載入和執行同一種平臺無關的位元組碼,從而實現了程式的“一次編寫,到處執行”。
各種不同平臺的虛擬機器與所有平臺都統一使用的程式儲存格式——位元組碼(ByteCode)是構成平臺無關性的基石,這裡說的是統一的儲存格式——位元組碼,而不是Java語言,所以如果其它語言也能夠被編譯成這種位元組碼,是不是也能夠執行在Java虛擬機器上?在Java發展之初,設計者就曾經考慮過並實現了讓其它語言執行在Java虛擬機器之上的可能性,他們在釋出規範文件的時候也可以把Java的規範拆分成了Java語言規範《The Java Language Specification》和Java虛擬機器規範《The Java Virtual Machine Specification》。而到了如今,除了Java語言之外,已經發展出了一大批正在Java虛擬機器紙上執行的語言,如Clojure、Groovy、JRuby、Jyhon、Scala等。
語言無關性
Class類檔案結構
Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎全部都是程式執行的必要資料,沒有空隙存在。當遇到需要佔用8個位元組以上的空間資料項時,則會按照高位在前的方式分割成若干個8位位元組進行儲存。
根據Java虛擬機器規範的規定,Class檔案格式採用一種類似於C語言結構體的偽結構來儲存資料,這種偽結構只有兩種資料型別:無符號數和表
無符號數: 無符號數屬於基本的資料型別,以u1、u2、u4和u8來分別代表1個位元組、2個位元組、4個位元組和8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成的字串。
表: 表是由多個無符號數或者其它表作為資料項構成的複合資料型別,所有表都習慣性地以”_info”結尾。用於描述有層次關係的複合結構的資料,整個Class檔案本質上就是一張表,它的組成如下圖所示。
型別 | 名稱 | 中文別名 | 數量 |
---|---|---|---|
u4 | magic | 魔數 | 1 |
u2 | minor_version | 次版本號 | 1 |
u2 | major_version | 主版本號 | 1 |
u2 | constant_pool_count | 常量池容量計數值 | 1 |
cp_info | constant_pool | 常量池 | constant_pool_count-1 |
u2 | access_flags | 訪問標誌 | 1 |
u2 | this_class | 類索引 | 1 |
u2 | super_class | 父類索引 | 1 |
u2 | interfaces_count | 介面計數器 | 1 |
u2 | interfaces | 介面索引集合 | interfaces_count |
u2 | fields_count | 欄位表計數器 | 1 |
field_info | fields | 欄位表 | fields_count |
u2 | methods_count | 方法表計數器 | 1 |
method_info | methods | 方法表 | methods_count |
u2 | attributes_count | 屬性表計數器 | 1 |
attribute_info | attributes | 屬性表 | attributes_count |
為了方便說明,所以準備了一段簡單的程式碼作為示例:
package com.overridere.six;
public class Test {
public static void main(String[] args) {
TestClass tc = new TestClass();
System.out.println(tc.print());
}
}
用16進位制編輯器(WinHex)開啟這個類的Class檔案如下圖所示:
前面也說過了,Class檔案裡面的內容是嚴格按照順序緊湊排列的,所以Class檔案可以看做就是一行很長很長的資料(當做一個很長很長的陣列),如何找到需要的資料呢?就是按偏移量來尋找(相當於陣列中的下標),上圖只不過將這個很長很長的資料以16進位制的二維形式顯示了,滿16個進一位就加一行。
接下來將圍繞這個檔案對Class檔案進行分析
魔數與Class檔案版本
每個Class檔案的頭4個位元組稱為魔數(Magic Number) 16進製表中為0xCAFEBABE,它的唯一作用是確定這個檔案是否是一個能被虛擬機器接收的Class檔案,是用來標識Class檔案的。
第5和第6個位元組是次版本號(Minor Version),第7和第8個位元組是主版本號(Major Version),Java的版本號是從45開始的,JDK1.1之後的每個JDK大版本釋出主版本號向上加1,高版本的JDK能向下相容以前版本的Class檔案,但不能執行以後版本的Class檔案。次版本號值為0x0000,主版本號值為0x0034,也就是十進位制的52,代表JDK是1.8版本。
常量池
緊接著主版本號之後兩個位元組的是常量池計數值(constant_pool_count),表示常量池中的專案數量,這個容量計數是從1開始的,在本示例當中常量池容量(偏移量:0x0000008)值為十六進位制的0x0016,即十進位制的22,代表常量池中有21項常量,索引值範圍為1~22。
常量池(constant_pool) 主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。字面量相當於Java語言中的常量的概念,符號引用包括下面三類常量:
- 類和介面的全限定名(Fully Qualified Name)
- 欄位的名稱和描述符(Descriptor)
- 方法的名稱和描述符
常量池中的項有各種各樣的型別,在JDK1.7之前有11種類型,JDK1.7的時候為了更好的支援動態語言呼叫有額外增加了3種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info),總共14種類型,這14種類型都是表結構,並且表開始的第一位都是一個u1型別的標誌位,代表當前這個常量屬於哪種型別。這14種類型如下表所示:
型別 | 標誌 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8編碼的Unicode字串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮點型字面量 |
CONSTANT_Long_info | 5 | 長整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點型字面量 |
CONSTANT_Class_info | 7 | 類或介面的符號引用 |
CONSTANT_String_info | 8 | 字串型別字面量 |
CONSTANT_Fieldref_info | 9 | 欄位的符號引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
CONSTANT_InterfaceMethodref_info | 11 | 介面中方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 欄位或方法的部分符號引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法控制代碼 |
CONSTANT_MethodType_info | 16 | 標識方法型別 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法呼叫點 |
試著分析常量池中的常量,看圖中常量池的第一項常量,無論什麼型別的常量,第一個位置都是標誌位且佔一個位元組,所以它的標誌位(偏移地址:0x0000000A)是0x07,檢視上面的型別表,得知標誌位為7的型別是CONSTANT_Class_info型別,此型別代表一個類或者介面的符號引用,它的結構如下表所示:
型別 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
tag是標誌位,name_index是一個索引值,指向常量池中一個CONSTANT_Utf8_info常量,磁場量代表了這個類或介面的全限定名,這裡的name_index值(偏移地址:0x0000000B)位0x0002,也即指向常量池中的第二項常量。繼續分享第二項常量,它的標誌位(偏移地址:0x0000000D)是0x01,查表得知是一個CONSTANT_Utf8_info型別的常量。該型別結構如下表所示:
型別 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length值為長度,後面長度為length的連續位元組是一個使用UTF-8縮略編碼表示的字串。
UTF-8縮略編碼與普通UTF-8編碼的區別:從’u\0001’到’\u007f’之間的字元的縮略碼使用一個位元組表示,’\u0080’到’\u07ff’之間用兩個位元組表示,’\u0800’到’\uffff’用三個位元組表示。
本示例中的length值(偏移地址:0x0000000E)為0x001C=28,往後28個位元組為該字串的內容,內容為“com/overridere/six/TestClass”。
通過javap -verbose命令可以輸出TestClass.class檔案位元組碼內容。
E:\JAVA\WorkPlace\JVMTest\bin\com\overridere\six>javap -verbose TestClass
Classfile /E:/JAVA/WorkPlace/JVMTest/bin/com/overridere/six/TestClass.class
Last modified 2017-4-6; size 393 bytes
MD5 checksum 3533fcf790f4d442f45fc0f44ccbfb99
Compiled from "TestClass.java"
public class com.overridere.six.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // com/overridere/six/TestClass
#2 = Utf8 com/overridere/six/TestClass
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."<init>":()V
#11 = NameAndType #7:#8 // "<init>":()V
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/overridere/six/TestClass;
#16 = Utf8 print
#17 = Utf8 ()I
#18 = Fieldref #1.#19 // com/overridere/six/TestClass.m:I
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 SourceFile
#21 = Utf8 TestClass.java
從以上程式碼可以看出,計算機已經幫我計算好了所有常量,並且第1、2項常量與我們手動計算的一樣。其中一些從沒在程式碼中出現過的常量如”I”、”V”、”《init》”、”LineNumberTable”、”LocalVariableTable”等,是用來描述一些不方便使用“固定位元組”進行表達的內容。比如描述方法的返回值是什麼,有幾個引數,每個引數型別是什麼。
最後放上所有常量型別的結構:
訪問標誌
在常量池後面的是訪問標誌(access_flags),這個標誌用於識別一些類或者介面層次的訪問資訊,包括:這個Class是類還是介面;是否定義為public型別;是否定義為abstract型別;如果是類的話,是否被宣告為final等。具體的標誌位及標誌含義如下表:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x001 | 是否為public型別 |
ACC_FINAL | 0x0010 | 是否被宣告為final |
ACC_SUPER | 0x0020 | 是否允許使用invokespecial位元組碼指令的新語意 |
ACC_INTERFACE | 0x0200 | 標識這是一個介面 |
ACC_ABSTRACT | 0x0400 | 是否為abstract型別,對於介面或者抽象類來說,此標誌值為真,其他類為假 |
ACC_SYNTHETIC | 0x1000 | 標識這個類並非由使用者程式碼產生 |
ACC_ANNOTATION | 0x2000 | 標識這是啥一個註解 |
ACC_ENUM | 0x4000 | 標識這是一個列舉 |
試著分析本示例中的access_flags,TestClass是一個普通類,不是介面、列舉或註解,不是final和abstract,被public修飾,所以ACC_PUBLIC、ACC_SUPER標誌應當為真,其它應為假,所以值應該是0x0001|0x0020=0x0021。檢視上面的十六進位制文字,access_flags標誌(偏移地址:0x000000EF)的確為0x0021。
類索引、父類索引和介面索引集合
類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引集合(interfaces)是一組u2型別的資料集合。類索引用來確定本類的全限定名,父類索引用來確定本類的父類全限定名,除了java.lang.Object外所有類都有父類,所以父類索引都不為0。
介面索引集合就是描述本類實現了哪些介面,介面集合之前有一個介面計數器(interface_count),表示介面集合的容量,如果為0則表示沒有實現介面,後面的介面集合不佔用任何位元組。
類索引、父類索引和介面集合裡面的介面索引都是指向一個CONSTANT_Class_info型別的常量,通過CONSTANT_Class_info就可以找到CONSTANT_Utf8_info型別的常量中的全限定名字串。
欄位表集合
欄位表(field_info)用於描述介面或類中宣告的變數。欄位包括類級變數以及例項級變數,不包括方法內部的區域性變數。欄位表結構如下圖所示:
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
欄位訪問標誌access_flags與類中的access_flags很相似,其標誌位及含義如下表:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 欄位是否public |
ACC_PRIVATE | 0x0002 | 欄位是否private |
ACC_PROTECTED | 0x0004 | 欄位是否protected |
ACC_STATIC | 0x0008 | 欄位是否static |
ACC_FINAL | 0x0010 | 欄位是否final |
ACC_VOLATILE | 0x0040 | 欄位是否volatile |
ACC_TRANSIENT | 0x0080 | 欄位是否transient |
ACC_SYNTHETIC | 0x1000 | 欄位是否由編譯器自動生成的 |
ACC_ENUM | 0x4000 | 欄位是否enum |
name_index代表著欄位的簡單名稱,descriptor_index表示欄位和方法的描述符。
本類中的print()方法和欄位m的簡單名稱分別為“print”和“m”。
描述符(descriptor_index)的作用是描述欄位的資料型別、方法的引數列表和返回值。根據描述符規則,基本型別和和void返回型別都用一個大寫字元來表示,物件型別則用L加物件的全限定名來表示。除了long和boolean型別分別用J和Z表示,其它基本型別都用首字母表示,比如說byte用B表示,char用C表示。
對於陣列來說,每一維陣列將用一個前置的“[”來描述,比如說”java.lang.String[][]”型別的陣列將被記錄為“[[Ljava/lang/String”,一個整型陣列”int[]”將被記錄為”[I”。
用描述符來描述方法時,按照先引數列表,後返回值的順序描述,引數列表按照引數的嚴格順序放在一組”()”內。如方法void print()的描述符為”()V”,方法java.lang.String toString()的描述符為”()Ljava/lang/String;”。
後面還有一個欄位屬性表(attributes)會在後面屬性表集合中講。
方法表集合
方法表集合與欄位表集合差不多。包含的內容跟欄位表一樣,如下表:
型別 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
訪問標誌欄位access_flags與欄位有些不同,如下表:
標誌名稱 | 標誌值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否public |
ACC_PRIVATE | 0x0002 | 方法是否private |
ACC_PROTECTED | 0x0004 | 方法是否protected |
ACC_STATIC | 0x0008 | 方法是否static |
ACC_FINAL | 0x0010 | 方法是否final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否為synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是由編譯器產生的橋接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定引數 |
ACC_NATIVE | 0x0100 | 方法是否為native |
ACC_ABSTRACT | 0x0400 | 方法是否為abstract |
ACC_STRICTFP | 0x0800 | 方法是否為strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由編譯器自動生成的 |
方法中的程式碼會儲存在方法表中的屬性表中的Code屬性中
如果沒有重寫父類方法,就不會出現父類的方法,但是也會出現由編譯器自動新增的方法,最典型的的如類構造器”《clinit》”方法和例項構造”《init》”方法。
屬性表集合
屬性表集合(attribute_info)。虛擬機器規範預定義的屬性表中的項有下表所示的屬性:
屬性名稱 | 使用位置 | 含義 |
---|---|---|
Code | 方法表 | Java程式碼編譯成的位元組碼指令 |
ConstantValue | 欄位表 | final關鍵字定義的常量值 |
Deprecated | 類、方法表、欄位表 | 被宣告為deprecated的方法和欄位 |
Exceptions | 方法表 | 方法丟擲的異常 |
EnclosingMethod | 類檔案 | 僅當一個類為區域性類或者匿名類時才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法 |
InnerClasses | 類檔案 | 內部類列表 |
LineNumberTable | Code屬性 | Java原始碼的行號與位元組碼指令的對應關係 |
LocalVariableTable | Code屬性 | 方法的區域性變數描述 |
StackMapTable | Code屬性 | JDK1.6中新增的屬性,供新的型別檢查驗證器(Type Checker)檢查和處理目標方法的區域性變數和運算元棧所需要的型別是否匹配 |
Signature | 類、方法表、欄位表 | JDK1.5中新增的屬性,這個屬性用於支援泛型情況下的方法簽名,在Java語言中,任何類、介面、初始化方法或成員的泛型簽名如果包含了型別變數(Type Variables)或引數化型別(Parameterized Types),則Signature屬性會為它記錄泛型簽名信息。由於Java泛型採用擦除法實現,在為了避免型別資訊被擦除後導致簽名混亂,需要用這個屬性記錄泛型中的相關資訊 |
SourceFile | 類檔案 | 記錄原始檔名稱 |
SourceDebugExtension | 類檔案 | JDK1.6中新增的屬性,SourceDebugExtension用於儲存額外的除錯資訊。 |
Synthetic | 類、方法表、欄位表 | 標識方法或欄位為編譯器自動生成 |
LocalVariableTypeTable | 類 | JDK1.5中新增的屬性,它使用特徵前面代替描述符,是為了引入泛型語法之後能描述泛型引數化型別而新增 |
RuntimeVisibleAnnotations | 類、方法表、欄位表 | JDK1.5中新增的屬性,為動態註解提供支援。用於知名哪些註解是執行時可見的 |
RuntimeInvisibleAnnotations | 類、方法表、欄位表 | JDK1.5中新增的屬性,與RuntimeVisibleAnnotations作用正好相反,用於指明哪些註解是執行時不可見的 |
RuntimeVisibleParameterAnnotations | 方法表 | JDK1.5中新增的屬性,作用與RuntimeVisibleAnnotations類似,不過作用物件是方法引數 |
RuntimeInvisibleParameterAnnotations | 方法表 | JDK1.5中新增的屬性,作用與RuntimeInvisibleAnnotations類似,不過作用物件是方法引數 |
AnnotationsDefault | 方法表 | JDK1.5中新增的屬性,用於記錄註解類元素的預設值 |
BootstrapMethods | 類檔案 | JDK1.7中新增的屬性,用於儲存invokedynamic指令引用的引導方法限定符 |
屬性表結構:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attributes_length | 1 |
u1 | info | attributes_length |
Code屬性
Java程式方法體中的程式碼經過Javac編譯器處理後,最終變為位元組碼指令儲存在方法表中的屬性表中的Code屬性中。Code屬性結構如下表所示:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_local | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index是一項指向CONSTANT_Utf8_info型別常量的索引,常量固定為“Code”,代表該屬性的名稱。
attribute_length指示了屬性值的長度。
max_locals代表了局部變量表(以後文章分析)所需的儲存空間。
code_length和code用來儲存Java源程式編譯後生成的位元組碼指令。code_length代表位元組碼長度,code是用於儲存位元組碼指令的一系列位元組流。
code屬性是Class檔案中最重要的一個屬性,如果把一個Java程式中的資訊分為程式碼(Code,方法體裡面的程式碼)和元資料(Metadata,包括類、欄位、方法定義及其他資訊)兩部分,則Code屬性用於描述程式碼,所有其他資料都是用於描述元資料。
以本文中的示例為例試分析一下code。
根據位元組碼指令表翻譯code屬性(2A B7 00 0A B1)所對應的位元組碼指令。
1. 讀入2A,查表得知對應的指令為aload_0
2. 讀入B7,差表得知對應的指令為invokespecial
3. 讀入00 0A,這是invokespecial的引數,查常量池得知0x000A對應的常量為例項構造器“《init》”方法的符號引用。
4. 讀入B1,查表得知B1對應的指令為return,返回此方法並且返回值是void。
用javap -verbose指令顯示Class檔案位元組碼指令如下:
{
public com.overridere.six.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/overridere/six/TestClass;
public int print();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #18 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/overridere/six/TestClass;
}
我們可以看到其中Args_size和Locals都不為0,明明init()方法和print()方法都沒有引數和區域性變數,為什麼這兩個值不為0呢?以前學習的時候應該都注意到了,在任何方法裡面都可以通過this關鍵字訪問到此方法所屬物件。這個訪問機制的實現就是通過Javac編譯器編譯的時候將this關鍵字的訪問轉變為對一個普通方法引數的訪問,然後在虛擬機器呼叫例項方法的時候自動傳入這個引數,所以在例項方法的區域性變量表中至少會存在一個指向當前物件例項的區域性變數。
位元組碼指令之後就是異常處理表集合(略)
Exceptions屬性
Exceptions屬性的作用是列舉出方法中可能丟擲的受查異常(Chexked Exceptions),也就是方法描述時在throws關鍵字後面列舉的異常。結構如下表:
型別 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
number_of_exceptions項表示方法可能丟擲number_of_exceptions種受查異常,每一種受查異常使用一個exception_index_table項表示,exception_index_table是一個指向常量池中CONSTANT_Class_info型常量的索引,代表了該受查異常的型別。
LineNumberTable屬性
LineNumberTable屬性用於描述Java原始碼行號與位元組碼行號(位元組碼偏移量)之間的對應關係。它並不是執行時必需的屬性,但是預設會生成到Class檔案之中,可以在Javac中分別使用-g:none或-g:line選中來取消或要求生成這項資訊。如果選中不生成,對程式產生的主要影響就是當丟擲異常時,對戰中將不會顯示出錯行號,並且在除錯程式的時候,也無法按照原始碼行來設定斷點。
LocalVariableTable屬性
LocalVariableTable屬性用於描述棧幀(以後文章分析)中區域性變量表中的變數與Java原始碼中定義的變數之間的關係,它也不上執行時必需的屬性,但預設會生成到Class檔案中,可以在Javac中分別使用-g:none或-g:vars選項來取消或要求生成這項資訊。如果沒有生成,最大的影響就是當其他人呼叫這個房東時,所有引數名稱都將會丟失,IDE會使用諸如arg0、arg1之類的佔位符代替原有的引數名。
其它屬性(略)
以上所有內容都是在閱讀深入理解Java虛擬機器——JVM高階特性與最佳實踐(第2版)第六章內容之後的選擇性總結歸納,如果想更詳細地瞭解的話,請閱讀正版書籍,如果發現以上內容有一些錯誤也歡迎指出,謝謝。