Java序列化
1. 什么是序列化陡鹃?
序列化是將一個對象的狀態(tài)磷蛹,各屬性的值序列化保存起來吮旅,然后在合適的時候通過反序列化獲得。
Java的序列化是將一個對象表示成字節(jié)序列,該字節(jié)序列包括了對象的數(shù)據(jù)庇勃,有關對象的類型信息和存儲在對象中的數(shù)據(jù)類型檬嘀。
說白了,就是將對象保存起來责嚷,就跟保存字符串數(shù)據(jù)一樣鸳兽,用到的時候再取出來。任何實現(xiàn)了Serializable接口的類都可以被序列化罕拂。
2. 實現(xiàn)Serializable接口進行序列化
package com.wangjun.othersOfJava;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializeDemo {
public static void main(String[] args) {
Employee em = new Employee();
em.name = "wangjun";
em.age = 24;
em.ssh = 123456;
// 將對象序列化后保存到文件
try (
FileOutputStream fo = new FileOutputStream("tem.ser");
ObjectOutputStream oo = new ObjectOutputStream(fo))
{
oo.writeObject(em);
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化取出對象
try(
FileInputStream fi = new FileInputStream("tem.ser");
ObjectInputStream oi = new ObjectInputStream(fi))
{
Employee e2 = (Employee) oi.readObject();
System.out.println(e2.name);
System.out.println(e2.age);
System.out.println(e2.ssh);
System.out.println(Employee.local);
e2.test();
} catch (Exception e) {
e.printStackTrace();
}
}
static class Employee implements Serializable {
String name;
int age;
static String local = "earth";
transient int ssh;
public void test() {
System.out.println("this is test method!");
}
}
}
程序的運行結果:
wangjun
24
0
earth
this is test method!
如果有一些字段不想被序列化怎么辦呢揍异?這時候就可以用transient關鍵字修飾,就像上面代碼的ssh
字段爆班,關于transient關鍵字有以下幾個特點:
- 一旦被transient關鍵字修飾衷掷,那變量將不再是對象持久化的一部分,該變量內(nèi)容在序列化后無法獲得訪問柿菩;
- transient只能修飾變量戚嗅,不能修飾方法和類,本地變量(局部變量)也不能被transient修飾枢舶;
- 一個靜態(tài)變量不管是否被transient修飾渡处,都不能被序列化。
從上面的例子看到好像與第三條不符祟辟,其實反序列化取出的local是JVM里面的值医瘫,而不是反序列化出來的【衫В可以加一行代碼驗證一下醇份,在反序列化之前更改一下local的值:
// 反序列化取出對象
Employee.local = "earth2";
try(
...
看一下打印結果
wangjun
24
0
earth2
this is test method!
這說明打印出來的是JVM中對應的local的值earth2,而不是序列化的時候的值earth吼具。
3. 實現(xiàn)Externalizable接口進行序列化
transient只有對實現(xiàn)了Serializable接口方式的序列化有效僚纷,還有一種序列化的方式是實現(xiàn)Externalizable接口,這種實現(xiàn)方式不像實現(xiàn)Serializable接口一樣可以幫你自動序列化拗盒,它需要在writeExternal方法中手動指定需要序列化的變量并且在readExternal手動取出來怖竭,這與是否被transient修飾無關,下面更改一下上面的例子陡蝇,將Employee類改成:
static class Employee implements Externalizable {
String name;
int age;
static String local = "earth";
transient int ssh;
//實現(xiàn)Externalizable接口進行序列化必須顯式聲明無參構造器
public Employee() {
}
public void test() {
System.out.println("this is test method!");
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
//out.writeObject(age);
out.writeObject(ssh);
out.writeObject(local);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
//age = (int) in.readObject();
ssh = (int) in.readObject();
local = (String) in.readObject();
}
}
重新運行痊臭,結果(注意:上述主函數(shù)中還存在對local重新賦值的代碼Employee.local = "earth2";
):
wangjun
0
123456
earth
this is test method!
可以看到能否被序列化跟transient和static修飾都沒有關系,只跟writeExternal和readExternal有關系登夫。
4. Serializable和Externalizable的區(qū)別
- 對Serializable對象反序列化時广匙,由于Serializable對象完全以它存儲的二進制位為基礎來構造,因此并不會調(diào)用任何構造函數(shù)恼策,因此Serializable類無需默認構造函數(shù)鸦致,但是當Serializable類的父類沒有實現(xiàn)Serializable接口時,反序列化過程會調(diào)用父類的默認構造函數(shù),因此該父類必需有默認構造函數(shù)分唾,否則會拋異常抗碰。
- 對Externalizable對象反序列化時,會先調(diào)用類的不帶參數(shù)的構造方法绽乔,這是有別于默認反序列方式的改含。如果把類的不帶參數(shù)的構造方法刪除,或者把該構造方法的訪問權限設置為private迄汛、默認或protected級別捍壤,會拋出
java.io.InvalidException: no valid constructor
異常,因此Externalizable對象必須有默認構造函數(shù)鞍爱,而且必需是public的鹃觉。 - 如果不是特別堅持實現(xiàn)Externalizable接口,那么還有另一種方法睹逃。我們可以實現(xiàn)
Serializable
接口盗扇,并添加writeObject()
和readObject()
的方法。一旦對象被序列化或者重新裝配沉填,就會分別調(diào)用那兩個方法疗隶。也就是說,只要提供了這兩個方法翼闹,就會優(yōu)先使用它們斑鼻,而不考慮默認的序列化機制。
5. SerialVersionUID的作用
上述實現(xiàn)Serializable接口的Employee類中猎荠,會有一個警告:
The serializable class Employee does not declare a static final serialVersionUID field of type long
意思是Employee沒有聲明一個靜態(tài)final的常量serialVersionUID坚弱,那這個serialVersionUID的作用是什么呢?
serialVersionUID是對類進行版本控制的关摇,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一致性的荒叶。在進行反序列化時,JVM會把傳來的字節(jié)流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較输虱,如果相同就認為是一致的些楣,可以進行反序列化,否則就會出現(xiàn)序列化版本不一致的異常宪睹,即是InvalidCastException愁茁。
serialVersionUID有兩種生成方式:
- 一是默認的1L,比如:private static final long serialVersionUID = 1L横堡;
- 二是根據(jù)類名埋市、接口名冠桃、成員方法及屬性等來生成一個64位的哈希字段命贴。
如果程序沒有顯式的聲明serialVersionUID,那么程序將用第二種實現(xiàn)。我們可以做一個實現(xiàn)胸蛛,還是用上述實現(xiàn)Serializable接口的例子污茵。
我們先運行一下程序,生成序列化文件tem.ser葬项,在把“將對象序列化后保存到文件”這一段邏輯注釋掉泞当,對Employee類增加一個test字段:
static class Employee implements Serializable {
String name;
int age;
static String local = "earth";
transient int ssh;
String test;
public void test() {
System.out.println("this is test method!");
}
}
這時候運行的時候會報錯:
java.io.InvalidClassException: com.wangjun.othersOfJava.SerializeDemo$Employee; local class incompatible: stream classdesc serialVersionUID = 4506166831890198488, local class serialVersionUID = 785960679919880606
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at com.wangjun.othersOfJava.SerializeDemo.main(SerializeDemo.java:32)
因為程序發(fā)現(xiàn)取到的序列化文件的serialVersionUID和當前的serialVersionUID不一樣。這個serialVersionUID是根據(jù)類名民珍、接口名襟士、成員方法及屬性等來生成一個64位的哈希字段,因為增加了test字段嚷量,因此生成的serialVersionUID不一樣了陋桂。
接著,我們顯式的聲明serialVersionUID
static class Employee implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
static String local = "earth";
transient int ssh;
public void test() {
System.out.println("this is test method!");
}
}
將剛才注釋的代碼取消注釋蝶溶,運行一遍再注釋掉嗜历,并且新增字段test:
static class Employee implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
static String local = "earth";
transient int ssh;
String test;
public void test() {
System.out.println("this is test method!");
}
}
再次運行發(fā)現(xiàn)沒有報錯,運行OK抖所。這是因為你顯式聲明了serialVersionUID梨州,序列化的serialVersionUID和目前的serialVersionUID一樣,因此會認為是同一個版本的類田轧。
你也可以將serialVersionUID改成2L暴匠,這個時候又會報錯了。
參考:
https://www.cnblogs.com/duanxz/p/3511695.html