(一)什么是序列化和反序列化
序列化和反序列化是將對(duì)象轉(zhuǎn)化成字節(jié)數(shù)組以方便保存或者用于網(wǎng)絡(luò)傳輸,這個(gè)對(duì)象可以是一個(gè)圖片,一個(gè)字符串,一個(gè)class等等,常見(jiàn)的序列化格式有字節(jié)數(shù)組,json格式,xml格式,更加高效的有g(shù)oogle開(kāi)源的Protocol Buffers,以及Apache Avro。
(二)為什么需要序列化和反序列化
(1)實(shí)現(xiàn)數(shù)據(jù)持久化,一般jvm的里面數(shù)據(jù),在java程序退出時(shí),所有的狀態(tài)都不會(huì)保留,通過(guò)序列化可以將需要的數(shù)據(jù)給持久化到磁盤文件或者數(shù)據(jù)庫(kù),這樣就可以在下次jvm啟動(dòng)的時(shí)候再把數(shù)據(jù)重新還原出來(lái)。
(2)利用序列化實(shí)現(xiàn)遠(yuǎn)程通信,即在網(wǎng)絡(luò)上傳送對(duì)象的字節(jié)序列,這種場(chǎng)景一般在socket或者rpc的服務(wù)中比較常見(jiàn)。
(三)Java里面如何實(shí)現(xiàn)序列化和反序列化
在java里面有兩種方式可以實(shí)現(xiàn)對(duì)象的序列化:
(1)實(shí)現(xiàn)Serializable接口的類,jdk會(huì)自動(dòng)幫我們序列化該類所有的信息, 但如果用戶定義了writeObject和readObject方法,那么在序列化和反序列化的時(shí)候會(huì)通過(guò)反射優(yōu)先調(diào)用自定義的方法
(2)實(shí)現(xiàn)Externalizable接口的類,需要用戶自定義序列化和反序列化的邏輯,分別重寫writeExternal和readExternal方法。
下面看一個(gè)例子,首先我們定義一個(gè)Person類并實(shí)現(xiàn)了序列化接口:
package cn.xby;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private transient String address;
private String name;
private int age;
@Override
public String toString() {
return "Person [address=" + address + ", name=" + name + ", age=" + age + "]";
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(String address, String name, int age) {
super();
this.address = address;
this.name = name;
this.age = age;
}
public Person() {
super();
}
}
然后我們定義了幫助實(shí)現(xiàn)序列化和反序列化的工具類:
package cn.xby;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializeTools {
/**
* 將任何實(shí)現(xiàn)了序列化接口的對(duì)象轉(zhuǎn)成字節(jié)數(shù)組
* @param obj
* @return
* @throws Exception
*/
public static byte[] toBytes(Serializable obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
/**
* 將任何序列化的字節(jié)數(shù)組給還原成對(duì)象
* @param bytes
* @return
* @throws Exception
*/
public static Object toObj(byte[] bytes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object obj = objectInputStream.readObject();
return obj;
}
/**
* 將一個(gè)實(shí)現(xiàn)了序列化的對(duì)象給序列化成文件
* @param obj
* @param storePath
* @throws Exception
*/
public static void toFile (Serializable obj,String storePath) throws Exception {
FileOutputStream fileOutputStream = new FileOutputStream(new File(storePath));
@SuppressWarnings("resource")
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(obj);
}
/**
* 將序列化對(duì)象還原成文件
* @param storePath
* @return
* @throws Exception
*/
public static Object fromFile(String storePath) throws Exception {
FileInputStream fileInputStream = new FileInputStream(new File(storePath));
@SuppressWarnings("resource")
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object obj = objectInputStream.readObject();
return obj;
}
}
然后看下我們的測(cè)試類,分別測(cè)試文件的序列化和字節(jié)的序列化
package cn.xby;
public class TestSerialize {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Person person = new Person("北京海淀","張三",25);
fileTest(person);//文件測(cè)試序列化和反序列化
System.out.println("======================================");
byteTest(person);//字節(jié)測(cè)試序列化和反序列化
}
public static void fileTest(Person p1) throws Exception {
String storePath = "D://temp.out";
System.out.println("基于文件序列化前:" + p1);
SerializeTools.toFile(p1, storePath);
Person p2 = (Person)SerializeTools.fromFile(storePath);
System.out.println("基于文件序列化后:" + p2);
}
public static void byteTest(Person p1) throws Exception {
System.out.println("基于字節(jié)序列化前:" + p1);
byte[] bytes = SerializeTools.toBytes(p1);//序列化成字節(jié)數(shù)組
Person p2 = (Person)SerializeTools.toObj(bytes);//反序列化成對(duì)象
System.out.println("基于字節(jié)反序列化后:" + p2);
}
}
運(yùn)行后輸出如下:
基于文件序列化前:Person [address=北京海淀, name=張三, age=25]
基于文件序列化后:Person [address=null, name=張三, age=25]
======================================
基于字節(jié)序列化前:Person [address=北京海淀, name=張三, age=25]
基于字節(jié)反序列化后:Person [address=null, name=張三, age=25]
細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)地址這個(gè)字段,在反序列化后字段值丟失了,這里說(shuō)明下:
(1)在java里面transient關(guān)鍵詞修飾的成員變量是不會(huì)被序列化的,這一點(diǎn)在上面的輸出中已經(jīng)得到驗(yàn)證了,注意transient關(guān)鍵詞只能修飾成員變量,不能修飾類和方法
(2)在java里面static關(guān)鍵詞修飾的字段也是不會(huì)被序列化的,因?yàn)檫@個(gè)是類的字段,而序列化是針對(duì)對(duì)象的。
引申一下:java的內(nèi)存分配有棧和堆以及永久代,棧中存放基本變量,數(shù)組和對(duì)象引用,堆中存放對(duì)象,當(dāng)有static修飾的變量或方法會(huì)被放到永久代里面。它先于對(duì)象而存在,不依賴實(shí)例,無(wú)論是變量,方法,還是代碼塊,只要用static修飾,就是在類被加載時(shí)就已經(jīng)準(zhǔn)備好了,也就是可以被使用或者已經(jīng)被執(zhí)行,都可以脫離對(duì)象而執(zhí)行,所以在類加載時(shí)靜態(tài)變量的值其實(shí)已經(jīng)還原出來(lái)了之后才是反序列化出來(lái)成員變量的值。
(3)在上面的Person類里面,相信大家還看到了一個(gè)用static final long修飾的 serialVersionUID字段,這個(gè)字段的功能是用來(lái)標(biāo)識(shí)類版本的兼容性:
舉個(gè)例子,假如現(xiàn)在沒(méi)有定義serialVersionUID這個(gè)字段,jdk默認(rèn)是根據(jù)類信息計(jì)算一個(gè)版本值,在類已經(jīng)被序列化成文件后,我們又修改了類結(jié)構(gòu),比如新增了幾個(gè)字段,這個(gè)時(shí)候拿著新版本的類去反序列化舊版本的類,就會(huì)拋出下面的異常:
意思就是版本不一致,導(dǎo)致失敗,如果我們定義這個(gè)值,并且新舊版本的值一樣,不管新增沒(méi)新增字段,都可以反序列化成功,默認(rèn)新增字段的值是jdk給成員變量初始化的值,比如字符串就是null。
(四)定制自己的序列化和反序列化方法
上面提到過(guò)實(shí)現(xiàn)了Serializable接口的類,我們可以重寫下面的方法來(lái)自定義序列化邏輯:
private void writeObject(ObjectOutputStream out) throws Exception {
System.out.println("call write");
out.writeObject(address);
out.writeObject(name);
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws Exception {
System.out.println("call read");
address = (String)in.readObject();
name = (String)in.readObject();
age = in.readInt();
}
再次執(zhí)行測(cè)試方法,輸出結(jié)果如下:
基于文件序列化前:Person [address=北京海淀, name=張三, age=25]
call write
call read
基于文件序列化后:Person [address=北京海淀, name=張三, age=25]
======================================
基于字節(jié)序列化前:Person [address=北京海淀, name=張三, age=25]
call write
call read
基于字節(jié)反序列化后:Person [address=北京海淀, name=張三, age=25]
這次我們發(fā)現(xiàn)了被transient修飾的address字段竟然也有值了,為什么?因?yàn)槲覀冏远x序列化的時(shí)候把地址也給序列化了,所以這個(gè)時(shí)候無(wú)論你用不用transient關(guān)鍵詞都無(wú)關(guān)緊要了。
注意如果實(shí)現(xiàn)了上面的方法其實(shí)和使用Externalizable就相差無(wú)幾了,所以在這里不再給出Externalizable的例子
(五)什么時(shí)候應(yīng)該readObject和writeObject
在effective java里面提到過(guò):
當(dāng)一個(gè)對(duì)象的物理表示方法與它的邏輯數(shù)據(jù)內(nèi)容有實(shí)質(zhì)性差別時(shí),使用默認(rèn)序列化形式有N種缺陷。
其實(shí)是建議我們重寫的,這樣可以更好的控制序列化的過(guò)程,如果能減少一些不必要的序列化的字段,其實(shí)對(duì)我們的程序性能也是一種提升。
總結(jié):
本文介紹了Java里面序列化和反序列化功能和使用以及一些注意事項(xiàng),序列化其實(shí)還提供了深度克隆的功能,尤其是當(dāng)類里面的引用層次比較多及引用特別復(fù)雜的時(shí)候,通過(guò)序列化來(lái)深度拷貝對(duì)象也是一種比較便利的方法,除此之外,我們還應(yīng)該知道序列化和反序列化和反射一樣,弱化了java安全權(quán)限修飾符的作用,無(wú)論你privte還是protected修飾的字段,在序列化和反射面前都是無(wú)作用的,所以一些敏感信息的序列化尤其是在網(wǎng)絡(luò)上傳輸?shù)娜缑艽a,金錢什么的,都應(yīng)該考慮加密或者其他安全措施。