java序列化與反序列化
最近抽空回顧了下java的序列化和反序列化,覺得之前了解的很淺顯,索性對底層代碼做了些分析。看了些師傅的文章,把筆記整理了下。個人覺得還可以。
序列化與反序列化
java序列化指的是將java對象轉化為字節序列的過程。
java反序列化指字節序列恢復到java對象。
基礎知識
計算機內存最小單位為一個二進制位,即 0或1。
我們把這個二進制位稱為一個bit(比特)位。
一個字節(byte)有八個比特位,即 byte = 8*bit。
如果八個bit位都為1,即這個字節最大為 FF = 1111 1111。
一個字(word)是兩個byte,即 word = 2 byte = 16 bit,
則一個字最大為 FFFF。
doubleword 雙字,是兩個word ,即四個byte,32*bit,
一個doubleword為FFFF FFFF。
一般情況下使用最多的是字節,字節相當于人民幣的元一樣,雖然不是最低的,但卻是最常用的。
一串字符在內存中一般是以ascii編碼形式存在,不同編碼占用子節長度不同
一個ascii碼的占用一個字節。
unicode碼占用一個字(兩個字節)。
utf-8 是我們國內常用的是針對unicode碼的一種可變編碼方式。
ascii


字節序
當一串數據太大的時候,一個字節放不下,就需要使用多個字節。
比如0x12345678就需要四個字節。
而現在就有了兩種存放方式,
我們稱這兩種為 小端序和大端序。
小端序從屁股開始,大端序從頭開始。
小端序

大端序

各家架構不同,使用的大小端序不同,無需糾結。
但是后來計算機網絡通信出來了,大家如果有不同的話會導致混亂。
tcp/ip協議出來之后就規定網絡通信必須使用大端序。
以上就是字節序的基本知識。
序列化與反序列化
序列化:
對象序列化的最主要的用處就是在傳遞和保存對象的時候,保證對象的完整性和可傳遞性。序列化是把對象轉換成有序字節流,以便在網絡上傳輸或者保存在本地文件中。序列化后的字節流保存了Java對象的狀態以及相關的描述信息。序列化機制的核心作用就是對象狀態的保存與重建。
反序列化:
客戶端從文件中或網絡上獲得序列化后的對象字節流后,根據字節流中所保存的對象狀態及描述信息,通過反序列化重建對象。
序列化就是把實體對象狀態按照一定的格式寫入到有序字節流,反序列化就是從有序字節流重建對象,恢復對象狀態。
上面的簡單點說,進程間通信可以將圖片,視頻,音頻等信息用二進制方式傳輸。但是進程間的對象卻不能這么搞。
比如我創建了一個User u1 = new User(1,”a”,100);
我要將它傳給另一個軟件(進程),
進程間的對象想要傳輸就需要序列化和反序列化。
序列化為二進制數據,可以永久存在硬盤里,也可以進行網絡傳輸。
實現java序列化和反序列化
下面嫌太長了可以直接看例子。
JDK類庫中序列化和反序列化API
java.io.ObjectOutputStream:
表示對象輸出流;
它的writeObject(Object obj)方法可以對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中;
java.io.ObjectInputStream:
表示對象輸入流;它的readObject()方法源輸入流中讀取字節序列,再把它們反序列化成為一個對象,并將其返回;
實現序列化的要求
只有實現了Serializable或Externalizable接口的類的對象才能被序列化,否則拋出異常!
實現Java對象序列化與反序列化的方法
若User類僅僅實現了Serializable接口,則可以按照以下方式進行序列化和反序列化:
ObjectOutputStream采用默認的序列化方式,對User對象的非transient的實例變量進行序列化。
ObjcetInputStream采用默認的反序列化方式,對對User對象的非transient的實例變量進行反序列化。
若User類僅僅實現了Serializable接口,并且還定義了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),則采用以下方式進行序列化與反序列化:
ObjectOutputStream調用User對象的writeObject(ObjectOutputStream out)的方法進行序列化。
ObjectInputStream會調用User對象的readObject(ObjectInputStream in)的方法進行反序列化。
若User類實現了Externalnalizable接口,且User類必須實現readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,則按照以下方式進行序列化與反序列化:
ObjectOutputStream調用User對象的writeExternal(ObjectOutput out))的方法進行序列化。
ObjectInputStream會調用User對象的readExternal(ObjectInput in)的方法進行反序列化。
實例
user對象,使用的是上述第一種方式,所以User要實現Serializable。
import java.io.Serializable; public class User implements Serializable { int id; String name; String phone; #一些get set 構造參數,這里就不列舉了}
序列化與反序列化
import java.io.*; public class userDemo { public static void main(String[] args) throws IOException, ClassNotFoundException { //創建對象 User u1 = new User(1,"AAAAAAA","110"); //被序列化的對象 User u2; //反序列化的對象 //序列化 getSerial(u1); //反序列化 u2 = backSerial(); System.out.println(u2.getName()); } //序列化 static void getSerial(User u1) throws IOException { FileOutputStream fos = new FileOutputStream("obj.out"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(u1); oos.flush(); oos.close(); } //反序列化 static User backSerial() throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream("obj.out"); ObjectInputStream ois = new ObjectInputStream(fis); User u1 = (User) ois.readObject(); return u1; }}
序列化底層分析

ObjdectOutputStream對象的初始化

bout是數據輸出流的底層
writeStreamHeader將文件頭寫入文件


這里根據序列化的文件分析

所以這里是寫入文件頭,表示聲明使用序列化協議以及說明序列化版本
初始化完畢,文件存在且寫入了文件頭。
開始序列化寫入文件
writeObject(u1);

向下調用write0ject0();這個方法的內容比較長。重要點在意思是按照不同類型的方法去寫入序列化數據,可以看上面實現Java對象序列化與反序列化的方法。

我們實例中實現了Serializable,所以執行writeOrdinaryObject方法。

bout.writeByte(TC_OBJECT);

寫入了0x73

調用 writeClassDesc(desc, false);跟進:

這里isProxy是判斷類是否是動態代理模式。
具體可以自行了解,我也不清楚。因為我們實例的類不是動態代理,所以跟進writeNonProxyDesc();

先寫入了描述符號0x72


下面判斷跟進兩個參數一個為1,一個為2。跟進writeClassDescriptor(desc);

和true執行同一個方法:

在開發中,我們經常會遇到要經過for循環來判斷該循環體中是否包含或不包含某一元素,這個時候我們也常用一個boolean值來介入判斷。而“|=”可以輕松的讓我們完成實現。
boolean flag = false; 在一個循環體中,flag |= (c==e);如果一直不相等,則flag一直為false,一旦有一個相等則為true;
out.writeUTF(name);

寫入類名

out.writeLong(getSerialVersionUID());
寫入序列化uid
再往下一堆if判斷接口的實現方式,將標志位寫入
out.writeByte(flags);

我們使用serializable,所以應該寫入0x02

所以從0x000B - 0x0013 都是序列化uid
然后調用writeShort寫入兩個字節的域長度(比如說有3個變量,就寫入 00 03 )。
實例中有三個參數

接下來就是循環寫入變量名和變量類型。
每輪循環:
writeByte寫入一個字節的變量類型,writeUTF()寫入變量名,判斷是不是原始類型,即是不是對象。不是原始類型(基本類型)的話,就調用writeTypeString()。
這個writeTypeString(),如果是字符串,就會調用writeString()。而這個writeString()往往是這樣寫的,字符串長度(不是大小)小于兩個字節,就先寫入一個字節的TC_STRING(16進制 74),然后調用writeUTF(),寫入一個signature,這好像跟jvm有關。
最后一般寫的是類似下面這串74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b“翻譯”過來就是,字符串類型,占18個字節長度,變量名是 Ljava/lang/string;
紅色 id參數 int 類型
綠色 name 參數 string 因為 String是引用數據類型所以調用了writeTypeString() 寫入了Ljava/lang/string;
黃色 phone 參數 string
這里第一次看有個疑問,phone參數也是string,但是他卻沒Ljava/lang/string;這一串,后邊又增加一個string的參數,確定同一種引用數據類型只寫入一次。
循環執行完,返回到writeNonProxyDesc方法,寫入結束標志位0x78
bout.writeByte(TC_ENDBLOCKDATA);


準備開始寫入序列化數據,回到writeOrdinaryObject()方法,writeSerialData(obj, desc);方法來寫入序列化數據

這里根據使用方式來判斷,所以調用了 defaultWriteFields();

第二個if是判斷是否為基本數據類型,是的話就會直接寫入序列化數據,不是的話向下到for循環附近。獲取變量數,然后循環調用writeObject0();寫入
循環結束,直到所有運行完成,回到主函數。
反序列化就不寫了,反反過來推一遍就成。
java反射機制
反射機制允許程序在運行期借助于Reflection API取得任何類的內部信息,并能直接操作任意類和對象的所有屬性及方法。
要使用一個類,就要先把它加載到虛擬機中,在加載完類之后,堆內存的方法區中就產生了一個Class類型的對象(一個類只有一個class對象),這個對象就包含了完整的類的結構信息,我們可以通過這個對象看到類的結構,這個對象就像一面鏡子,透過鏡子可以看到類的結構,所以形象的稱之為:反射。
實例:
import java.lang.reflect.Method; public class test { public static void main(String[] args) throws Exception { a1Class a1 = new a1Class(); //通過運行時的對象調用getClass(); Class c = a1.getClass(); try { //getMethod(方法名,參數類型) //getMethod第一個參數是方法名,第二個參數是該方法的參數類型 //因為存在同方法名不同參數這種情況,所以只有同時指定方法名和參數類型才能唯一確定一個方法 Method m1 = c.getMethod("print", int.class, int.class); //相當于r1.print(1, 2);方法的反射操作是用m1對象來進行方法調用 和r1.print調用的效果完全相同 //使用r1調用m1獲得的對象所聲明的公開方法即print,并將int類型的1,2作為參數傳入 Object i = m1.invoke(a1,1,1); }catch (Exception e){ e.printStackTrace(); } } static class a1Class { public void print(int a, int b) { System.out.println(a + b); } }}
嘗試簡化上面的代碼
創建另一個文件
public class testMiao { public static void maio(){ System.out.println("miao!"); }}
使用反射來執行miao();
public class test { public static void main(String[] args) throws Exception { try { Object s = Class.forName("testMiao").getMethod("maio").invoke(null); }catch (Exception e){ e.printStackTrace(); } }}
嘗試添加參數簡化
public class testMiao { public static void maio(String s){ System.out.println("miao!"+s); }}
反射
public class test { public static void main(String[] args) throws Exception { try { Class.forName("testMiao").getMethod("maio", String.class).invoke(Class.forName("testMiao"),"aaa"); }catch (Exception e){ e.printStackTrace(); } }}
java執行命令
java中可以使用Runtime.getRuntime.exec();來執行系統命令
如:
嘗試使用反射來執行
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke("open /System/Applications/Calculator.app");
這樣會報錯,報錯的信息:是對象不是聲明類的實例
說明exec只能是通過getRuntime來執行
import java.lang.reflect.Method; public class test { public static void main(String[] args) throws Exception { Object o = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null); Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(o,"open /System/Applications/Calculator.app"); }}
這樣會成功,原理跟隨反射實例第一個實例來理解。
現在可以打開計算器,明白什么是序列與反序列化了。
關于cc1的鏈,之后再寫,可以看bilibili 白日夢組長分析思路,我個人覺得他的思路是真的超級棒。