1.序列化的意義
2.java原生序列化
3.serialVersionUID 的作用
4.靜態(tài)變量序列化
5.父類的序列化
6.Transient 關(guān)鍵字
7.繞開 transient 機(jī)制的辦法
8.序列化的存儲規(guī)則
9.序列化實現(xiàn)深克隆
10.分布式架構(gòu)下常見序列化技術(shù)
序列化的意義
java平臺允許我們在內(nèi)存中創(chuàng)建可復(fù)用的Java對象犁河,但一般情況下已卷,只有當(dāng) JVM處于運行時色瘩,這些對象才可能存在挚歧,即這些對象的生命周期不會比JVM的生命周期更長五督。但在現(xiàn)實應(yīng)用中,就可能要求在JVM停止運行之后能夠保存(持久化)指定的對象介陶,并在將來重新讀取被保存的對象扇单,Java 對象序列化就能夠幫助我們實現(xiàn)該功能换可。
簡單來說椎椰,序列化是把對象的狀態(tài)信息轉(zhuǎn)化為可存儲或傳輸?shù)男问剑簿褪前褜ο筠D(zhuǎn)化為字節(jié)序列的過程稱為對象的序列化沾鳄。反序列化是序列化的逆向過程慨飘,把字節(jié)數(shù)組反序列化為對象,把字節(jié)序列恢復(fù)為對象的過程稱為對象的反序列化译荞。
java原生序列化
前面的代碼中演示了瓤的,如何通過 JDK 提供了 Java 對象的序列化方式實現(xiàn)對象序列化傳輸,主要通過輸出流java.io.ObjectOutputStream和對象輸入流java.io.ObjectInputStream來實現(xiàn)磁椒。
java.io.ObjectOutputStream:表示對象輸出流 , 它的 writeObject(Object obj)方法可以對參數(shù)指定的 obj 對象進(jìn)行序列化堤瘤,把得到的字節(jié)序列寫到一個目標(biāo)輸出流中玫芦。
java.io.ObjectInputStream:表示對象輸入流 ,它的 readObject()方法源輸入流中讀取字節(jié)序列浆熔,再把它們反序列化成為一個對象,并將其返回
需要注意的是桥帆,被序列化的對象需要實現(xiàn) java.io.Serializable 接口
例:基于 socket 進(jìn)行對象傳輸
public class User implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class SocketServerProvider {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=null;
BufferedReader in=null;
try{
serverSocket=new ServerSocket(8080);
Socket socket=serverSocket.accept();
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
User user=(User)objectInputStream.readObject();
System.out.println(user);
}catch (Exception e){
e.printStackTrace();
}finally {
if(in!=null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(serverSocket!=null){
serverSocket.close();
}
}
}
}
public class SocketClientConsumer {
public static void main(String[] args) {
Socket socket=null;
ObjectOutputStream out=null;
try {
socket=new Socket("127.0.0.1",8080);
User user=new User();
out=new ObjectOutputStream(socket.getOutputStream());
out.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}finally {
if(out!=null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
serialVersionUID 的作用
Java的序列化機(jī)制是通過判斷類的serialVersionUID來驗證版本一致性的医增。在進(jìn)行反序列化時,JVM會把傳來的字節(jié)流中的serialVersionUID與本地相應(yīng)實體類的serialVersionUID進(jìn)行比較老虫,如果相同就認(rèn)為是一致的叶骨,可以進(jìn)行反序列化,否則就會出現(xiàn)序列化版本不一致的異常祈匙,即是InvalidCastException忽刽。
如果沒有為指定的class配置serialVersionUID,那么java編譯器會自動給這個class進(jìn)行一個摘要算法夺欲,類似于指紋算法跪帝,只要這個文件有任何改動,得到的UID就會截然不同些阅,可以保證在這么多類中伞剑,這個編號是唯一的。
serialVersionUID有兩種顯示的生成方式:
一是默認(rèn)的1L市埋,比如:private static final long serialVersionUID = 1L;
二是根據(jù)類名黎泣、接口名、成員方法及屬性等來生成一個64位的哈希字段缤谎。
當(dāng)實現(xiàn)java.io.Serializable接口的類沒有顯式地定義一個serialVersionUID變量的時候抒倚,Java序列化機(jī)制會根據(jù)編譯的Class自動生成一個serialVersionUID當(dāng)作序列化版本來比較用,這種情況下坷澡,如果Class文件(類名托呕,方法名等)沒有發(fā)生變化(增加空格,換行,增加注釋等等)镣陕,就算再編譯多次谴餐,serialVersionUID也不會變化。
靜態(tài)變量序列化
在User中添加一個全局的靜態(tài)變量num 呆抑, 在執(zhí)行序列化以后修改num的值為10岂嗓, 然后通過反序列化以后得到的對象去輸出num的值。
public class User implements Serializable{
private String name;
private int age;
public static int num=5;//設(shè)置靜態(tài)變量num值為5
...
...
...
}
public class Test {
public static void main(String[] args) {
ISerializer serializer=new JavaSerializer();
User user=new User();
user.setName("taofut");
user.setAge(27);
byte[] bytes=serializer.serializer(user);
//序列化以后鹊碍,將num值改為10
User.num=10;
User user1=serializer.deSerializer(bytes,User.class);
System.out.println(user1+"--"+User.num);
}
//執(zhí)行結(jié)果:User{name='taofut', age=27}--10
}
最后的輸出是10厌殉,我們可能會覺得,10是在序列化之后修改的侈咕,按理說反序列化應(yīng)該輸出的是5才對公罕。理論上打印的num是從讀取的對象里獲得的,應(yīng)該是保存時的狀態(tài)才對耀销。之所以打印10的原因在于序列化時楼眷,并不保存靜態(tài)變量,這其實比較容易理解熊尉,序列化保存的是對象的狀態(tài)罐柳,靜態(tài)變量屬于類的狀態(tài),因此序列化并不保存靜態(tài)變量狰住。
父類的序列化
一個子類實現(xiàn)了Serializable接口张吉,而它的父類卻沒有實現(xiàn)Serializable接口,在子類中設(shè)置父類的成員變量的值催植,接著序列化該子類對象肮蛹。再反序列化出來以后輸出父類屬性的值。結(jié)果應(yīng)該是什么创南?
public class SuperUser {
String sex;
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
public class Test {
public static void main(String[] args) {
ISerializer serializer=new JavaSerializer();
User user=new User();
user.setName("taofut");
user.setAge(27);
user.setSex("男");
byte[] bytes=serializer.serializer(user);
User.num=10;
User user1=serializer.deSerializer(bytes,User.class);
System.out.println(user1+"--"+user1.getSex());
}
//執(zhí)行結(jié)果:User{name='taofut', age=27}--null
}
最終結(jié)果發(fā)現(xiàn)伦忠,父類的sex字段的值為null,也就是說父類沒有實現(xiàn)序列化扰藕。
結(jié)論:
1》 當(dāng)一個父類沒有實現(xiàn)序列化時缓苛,子類繼承該父類并且實現(xiàn)了序列化。在反序列化該子類后邓深,是沒辦法獲取到父類的屬性值的未桥。
2》當(dāng)一個父類實現(xiàn)序列化,子類自動實現(xiàn)序列化芥备,不需要再顯示實現(xiàn)Serializable接口冬耿。
3》當(dāng)一個對象的實例變量引用了其他對象,序列化該對象時也會把引用對象進(jìn)行序列化萌壳,但是前提是該引用對象必須實現(xiàn)序列化接口亦镶。
Transient關(guān)鍵字
Transient關(guān)鍵字的作用是控制變量的序列化日月,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中缤骨,在被反序列化后爱咬,transient變量的值被設(shè)為初始值,如 int型的是0绊起,對象型的是null精拟。
public class UserTwo extends SuperUser implements Serializable{
private String name;
private int age;
public static int num=5;
private transient String address;
...
...
...
}
public class Test {
public static void main(String[] args) {
ISerializer serializer=new JavaSerializer();
UserTwo user=new UserTwo();
user.setName("taofut");
user.setAge(27);
user.setSex("男");
user.setAddress("浙江省");//該字段被transient修飾過
byte[] bytes=serializer.serializer(user);
UserTwo user1=serializer.deSerializer(bytes,UserTwo.class);
System.out.println(user1+"--"+user1.getAddress());
}
//執(zhí)行結(jié)果:User{name='taofut', age=27}--null
}
繞開transient機(jī)制的辦法
public class UserTwo extends SuperUser implements Serializable{
private String name;
private int age;
public static int num=5;
private transient String address;
//序列化對象
private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {
objectOutputStream.defaultWriteObject();
objectOutputStream.writeObject(address);
}
//反序列化
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
objectInputStream.defaultReadObject();
address=(String)objectInputStream.readObject();
}
...
...
...
}
public class Test {
public static void main(String[] args) {
ISerializer serializer=new JavaSerializer();
UserTwo user=new UserTwo();
user.setName("taofut");
user.setAge(27);
user.setSex("男");
user.setAddress("浙江省");
byte[] bytes=serializer.serializer(user);
UserTwo user1=serializer.deSerializer(bytes,UserTwo.class);
System.out.println(user1+"--"+user1.getAddress());
}
//執(zhí)行結(jié)果:User{name='taofut', age=27}--浙江省
}
以上代碼可能會產(chǎn)生一個疑問:writeObject和readObject這兩個私有的方法,既不屬于 Object虱歪、也不是Serializable蜂绎,為什么能夠在序列化的時候被調(diào)用呢? 原因是笋鄙,ObjectOutputStream使用了反射來尋找是否聲明了這兩個方法师枣。因為ObjectOutputStream使用getPrivateMethod,(反射可以繞開權(quán)限限制)所以這些方法必須聲明為priate萧落,以至于供ObjectOutputStream來使用践美。
對希望采用自定義序列化的字段用transient修飾,然后在先調(diào)用writeObject和readObject方法中對transient修飾的字段進(jìn)行序列化铐尚,并在方法最開始調(diào)用defaultReadObject和defaultReadObject方法拨脉,對其他字段采用默認(rèn)序列化方式。這樣的好處是方便兼容宣增。
被transient修飾的成員,只是不能被默認(rèn)的序列化方法序列化(從源碼中也可以看到)矛缨,但卻可以被自定義的序列化方法序列化爹脾。
序列化的存儲規(guī)則
public class StoreRuleDemo {
public static void main(String[] args) throws IOException{
ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(new File("user")));
User user=new User();
user.setName("taofut");
user.setAge(27);
user.setSex("男");
outputStream.flush();
outputStream.writeObject(user);
System.out.println(new File("user").length());
outputStream.writeObject(user);
outputStream.flush();
outputStream.close();
System.out.println(new File("user").length());
}
//執(zhí)行結(jié)果:
//89
//94
}
我們發(fā)現(xiàn),同一對象兩次寫入文件箕昭,打印出寫入一次對象后的存儲大小和寫入兩次后的存儲大小灵妨,第二次寫入對象時文件只增加了5個字節(jié)。
這是因為落竹,Java 序列化機(jī)制為了節(jié)省磁盤空間泌霍,具有特定的存儲規(guī)則,當(dāng)寫入文件為同一對象時述召,并不會再將對象的內(nèi)容進(jìn)行存儲朱转,而只是再次存儲一份引用,上面增加的5個字節(jié)的存儲空間就是新增引用和一些控制信息的空間积暖。反序列化時藤为,恢復(fù)引用關(guān)系,該存儲規(guī)則極大的節(jié)省了存儲空間夺刑。
序列化實現(xiàn)深克隆
1》淺克隆機(jī)制:被復(fù)制對象的所有變量都含有與原來的對象相同的值缅疟,而所有的對其他對象的引用仍然指向原來的對象分别。
public class CloneDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Email email=new Email();
email.setContent("今天晚上6點開會");
Person p1=new Person("taofut");
p1.setEmail(email);
Person p2=p1.clone();
p2.setName("fut");
p2.getEmail().setContent("今天晚上6點半開會");
System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
System.out.println(p2.getName()+"->"+p2.getEmail().getContent());
}
//執(zhí)行結(jié)果:
//taofut->今天晚上6點半開會
//fut->今天晚上6點半開會
}
以上案例很好的說明了,淺克隆不能復(fù)制新的引用存淫,Email引用還是指向的同一個對象耘斩,這就導(dǎo)致了都是”今天晚上6點半開會”的結(jié)果出現(xiàn)。
2》深克隆機(jī)制:被復(fù)制對象的所有變量都含有與原來的對象相同的值桅咆,除去那些引用其他對象的變量煌往。那些引用其他對象的變量將指向被復(fù)制過的新對象,而不再是原有的那些被引用的對象轧邪。換言之刽脖,深拷貝把要復(fù)制的對象所引用的對象都復(fù)制了一遍。
public class Email implements Serializable{
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
public class Person implements Cloneable,Serializable{
private String name;
private Email email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Email getEmail() {
return email;
}
public void setEmail(Email email) {
this.email = email;
}
@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
public Person(String name) {
this.name = name;
}
//深克隆方法
public Person deepClone() throws IOException,ClassNotFoundException{
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream outputStream=
new ObjectOutputStream(bos);
outputStream.writeObject(this);
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream inputStream=new ObjectInputStream(bis);
return (Person)inputStream.readObject();
}
}
public class DeepDemo {
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
Email email=new Email();
email.setContent("今天晚上6點開會");
Person p1=new Person("taofut");
p1.setEmail(email);
Person p2=p1.deepClone();
p2.setName("fut");
p2.getEmail().setContent("今天晚上6點半開會");
System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
System.out.println(p2.getName()+"->"+p2.getEmail().getContent());
}
//執(zhí)行結(jié)果:
//taofut->今天晚上6點開會
//fut->今天晚上6點半開會
}
由于Java本身提供的序列化機(jī)制存在兩個問題
- 序列化的數(shù)據(jù)比較大忌愚,傳輸效率低
- 其他語言無法識別和對接
所以出現(xiàn)了一些序列化框架,種類包括:
XML 序列化,JSON 序列化框架 ,Hessian 序列化框架 ,Avro 序列化 ,kyro 序列化框架 ,Protobuf 序列化框架