1.什麼是序列化
Java的對象序列化其實就是將一個實現了serializable接口的對象轉換成一個二進制byte數組,這樣日後使用這個對象時候就能將這個對象及其數據通用反序列化轉換回來,重新構建。
在接口開發或者其它容易跨平臺操作時,使用對象序列化就意味着能自動補全操作系統的差異,例如在windows系統上創建一個對象,序列化之後通過網絡傳到linux系統上反序列化回來,那麼這個對象在Linux上仍可以繼續使用!
簡而言之:
● 序列化: 將數據結構或對象轉換成二進制串的過程。
● 反序列化:將在序列化過程中所生成的二進制串轉換成數據結構或者對象的過程。
2.怎麼序列化
在實現序列化是有個前提條件的:只有實現了Serializable或Externalizable接口的類的對象才能被序列化,否則拋出異常。
舉個例子:
public class Student implements Serializable {
private String name = null;
private Integer age = null;
private Gender gender = null;
public Student () {
System.out.println("none-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
@Override
public String toString() {
return "[" + name + ", " + age + ", " + gender + "]";
}
}
新建一個miantest 進行序列化和反序列化
public class maintest{
public static void main(String[] args) throws Exception {
File file = new File("studnet.out");
ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
Studnet studnet = new Studnet ("John", 101, Gender.MALE);
oout.writeObject(studnet );
oout.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
Object newStudent = oin.readObject(); // 沒有強制轉換到Person類型
oin.close();
System.out.println(newStudent);
}
}
此時可以看到在反序列時並沒有調用任何構造器的,直接讀取字節文件。
還有一點需要注意的事當Student對象被保存到Student.out文件中之後,我們可以在其它地方去讀取該文件以還原對象,但必須確保該讀取程序的CLASSPATH中包含有Student.class,否則會拋出ClassNotFoundException。
總結下就是:
假定一個Student類,它的對象需要序列化,可以有如下三種方法:
方法一:若Student類僅僅實現了Serializable接口,則可以按照以下方式進行序列化和反序列化
ObjectOutputStream採用默認的序列化方式,對Student對象的非transient的實例變量進行序列化。
ObjcetInputStream採用默認的反序列化方式,對對Student對象的非transient的實例變量進行反序列化。
方法二:若Student類僅僅實現了Serializable接口,並且還定義了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),則採用以下方式進行序列化與反序列化。(適用於加解密)
ObjectOutputStream調用Student對象的writeObject(ObjectOutputStream out)的方法進行序列化。
ObjectInputStream會調用Student對象的readObject(ObjectInputStream in)的方法進行反序列化。
方法三:若Student類實現了Externalnalizable接口,且Student類必須實現readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,則按照以下方式進行序列化與反序列化。
ObjectOutputStream調用Student對象的writeExternal(ObjectOutput out))的方法進行序列化。
ObjectInputStream會調用Student對象的readExternal(ObjectInput in)的方法進行反序列化。
3.爲什麼序列化
序列化使用於一下場景:
1):當你想把的內存中的對象狀態保存到一個文件中或者數據庫中時候;
2):當你想用套接字在網絡上傳送對象的時候;
3):當你想通過RMI傳輸對象的時候;
4.反序列化會遇到什麼問題,如何解決
1)對敏感字段加密:
情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,纔可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。
private static final long serialVersionUID = 1L;
private String password = "pass";
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private void writeObject(ObjectOutputStream out) {
try {
PutField putFields = out.putFields();
System.out.println("原密碼:" + password);
password = "encryption";//模擬加密
putFields.put("password", password);
System.out.println("加密後的密碼" + password);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
GetField readFields = in.readFields();
Object object = readFields.get("password", "");
System.out.println("要解密的字符串:" + object.toString());
password = "pass";//模擬解密,需要獲得本地的密鑰
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("result.obj"));
out.writeObject(new Test());
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"result.obj"));
Test t = (Test) oin.readObject();
System.out.println("解密後的字符串:" + t.getPassword());
oin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
2)序列化 ID 問題
情境:兩個客戶端 A 和 B 試圖通過網絡傳遞對象數據,A 端將對象 C 序列化爲二進制數據再傳給 B,B 反序列化得到 C。
問題:C 對象的全類路徑假設爲 com.inout.Test,在 A 和 B 端都有這麼一個類文件,功能代碼完全一致。也都實現了 Serializable 接口,但是反序列化時總是提示不成功。
解決:虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清單 1 中,雖然兩個類的功能代碼完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。
package com.inout;
import java.io.Serializable;
public class A implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.inout;
import java.io.Serializable;
public class A implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
序列化 ID 在 Eclipse 下提供了兩種生成策略,一個是固定的 1L,一個是隨機生成一個不重複的 long 類型數據(實際上是使用 JDK 工具生成),在這裏有一個建議,如果沒有特殊需求,就是用默認的 1L 就可以,這樣可以確保代碼一致時反序列化成功。那麼隨機生成的序列化 ID 有什麼作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。
3)部分字段不進行序列化
情景:當對象被序列化時(寫入字節序列到目標文件)時,服務端需要這個狀態,而客戶端並不需要,此時完全沒有必要將此狀態序列化到客戶端。
例如:
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
class ClassLib implements Serializable {
private transient InputStream is;
private int majorVer;
private int minorVer;
ClassLib(InputStream is) throws IOException {
System.out.println("ClassLib(InputStream) called");
this.is = is;
DataInputStream dis;
if (is instanceof DataInputStream)
dis = (DataInputStream) is;
else
dis = new DataInputStream(is);
if (dis.readInt() != 0xcafebabe)
throw new IOException("not a .class file");
minorVer = dis.readShort();
majorVer = dis.readShort();
}
int getMajorVer() {
return majorVer;
}
int getMinorVer() {
return minorVer;
}
void showIS() {
System.out.println(is);
}
}
public class TransDemo {
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("usage: java TransDemo classfile");
return;
}
ClassLib cl = new ClassLib(new FileInputStream(args[0]));
System.out.printf("Minor version number: %d%n", cl.getMinorVer());
System.out.printf("Major version number: %d%n", cl.getMajorVer());
cl.showIS();
try (FileOutputStream fos = new FileOutputStream("x.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(cl);
}
cl = null;
try (FileInputStream fis = new FileInputStream("x.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
System.out.println();
cl = (ClassLib) ois.readObject();
System.out.printf("Minor version number: %d%n", cl.getMinorVer());
System.out.printf("Major version number: %d%n", cl.getMajorVer());
cl.showIS();
} catch (ClassNotFoundException cnfe) {
System.err.println(cnfe.getMessage());
}
}
}
5.相關注意事項
a)序列化時,只對對象的狀態進行保存,而不管對象的方法;
b)當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable接口;
c)當一個對象的實例變量引用其他對象,序列化該對象時也把引用對象進行序列化;
d)並非所有的對象都可以序列化,,至於爲什麼不可以,有很多原因了,比如:
1.安全方面的原因,比如一個對象擁有private,public等field,對於一個要傳輸的對象,比如寫到文件,或者進行rmi傳輸 等等,在序列化進行傳輸的過程中,這個對象的private等域是不受保護的。
2.資源分配方面的原因,比如socket,thread類,如果可以序列化,進行傳輸或者保存,也無法對他們進行重新的資源分 配,而且,也是沒有必要這樣實現。
參考案列:
Java序列化與反序列化