原帖地址:原帖
個人網站地址:個人網站
簡書對markdown的支持太完美了,我竟然可以直接Ctrl C/V過來。
定義
Java序列化是指把Java對象轉換為字節序列的過程;
Java反序列化是指把字節序列恢復為Java對象的過程。
應用場景
當兩個進程進行遠程通信時,可以相互發送各種類型的數據,包括文本、圖片、音頻、視頻等, 而這些數據都會以二進制序列的形式在網絡上傳送。那么當兩個Java進程進行通信時,如何實現進程間的對象傳送呢?這就需要Java序列化與反序列化了。一方面,發送方需要把這個Java對象轉換為字節序列,然后在網絡上傳送;另一方面,接收方需要從字節序列中恢復出Java對象。數據傳輸便是序列化與反序列化的主要應用場景之一。當然也可用于數據的存儲與讀取。
實現方式
java的序列化有兩種方式:
- 實現序列化接口Serializable,這個使用的比較多。Serializable接口是一個空的接口,它的主要作用就是標識這個類的對象是可序列化的。
- 實現接口Externalizable。Exterinable繼承了Serializable,是Serializable的一個擴展,對于哪些屬性可以序列化,哪些可以反序列化可以做詳細地約束。
相關工具類:
-
java.io.ObjectOutputStream:表示對象輸出流
它是OutputStream類的一個子類,對應的ObjectOutputStream.WriteObject(Object object)就要求參數object實現Serializable接口。
使用方式如下:
import java.io.FileOutputStream; import java.io.ObjectOutputStream; String fileName = "test.txt"; //文件名 LoginInfo info = new LoginInfo("chen","123"); //某個待序列化對象,LoginInfo類須實現Serializable接口 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName)); //創建輸出流 oos.writeObject(info); //序列化 oos.close(); //關閉流
-
java.io.ObjectInputStream:表示對象輸入流
相應地,它的readObject(Object object)方法從輸入流中讀取字節序列,再把它們反序列化成為一個對象,并返回。
使用方式如下:
import java.io.FileInputStream; import java.io.ObjectInputStream; String fileName = "test.txt"; ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); LoginInfo info2 = (LoginInfo)ois.readObject(); ois.close();
方式一:實現Serializable接口
Serializable源碼
/*
* Copyright (c) 1996, 2005, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package java.io;
/**
* Serializability of a class is enabled by the class implementing the
* java.io.Serializable interface. Classes that do not implement this
* interface will not have any of their state serialized or
* deserialized. All subtypes of a serializable class are themselves
* serializable. The serialization interface has no methods or fields
* and serves only to identify the semantics of being serializable.
*
* @author unascribed
* @see java.io.ObjectOutputStream
* @see java.io.ObjectInputStream
* @see java.io.ObjectOutput
* @see java.io.ObjectInput
* @see java.io.Externalizable
* @since JDK1.1
*/
public interface Serializable {
}
中間還省略了很多注釋,再忽略上面留下的注釋,發現這個接口是空的!沒有字段,沒有方法,只是標識一個類的對象是否可序列化(實現了這個接口,就表示可以序列化)。
serialVersionUID的作用
實現Serializable接口前后并沒有增加新方法,只是多了一個serialVersionUID,其實這個字段也不是必須的。若不寫serialVersionUID。程序也能運行成功,不過eclipe會顯示警告。
其實,Java的序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常(InvalidCastException)。
所以最好有這個字段,那么要如何添加呢?鼠標移動到警告處,eclipse在給出警告的同時也給出了解決方法:
- 添加默認值,即1L;
- 添加自動產生的ID值,我的是8685376332791485990L;(推薦)
- 通過@SuppressWarnings 批注取消特定代碼段(即,類或方法)中的警告。
java實現
package serialize;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Date;
public class LoginInfo implements Serializable{
private static final long serialVersionUID = 8685376332791485990L;
private String username;
private String password;
private Date logindate;
public LoginInfo(){
System.out.println("non-parameter constructor");
}
public LoginInfo(String username, String password){
System.out.println("parameter constructor");
this.username = username;
this.password = password;
this.logindate = new Date();
}
public String toString(){
return "username="+username+",password="+password+",logindate="+logindate;
}
public static void main(String[] args) throws Exception{
LoginInfo info = new LoginInfo("chen","123");
System.out.println(info);
String fileName = "info_serializable.txt";
System.out.println("Serialize object");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName));
oos.writeObject(info);
oos.close();
System.out.println("Deserialize object");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName));
LoginInfo info2 = (LoginInfo)ois.readObject();
ois.close();
System.out.println(info2);
}
}
運行結果:
parameter constructor
username=chen,password=123,logindate=Sun Feb 19 09:33:38 CST 2017
Serialize object
Deserialize object
username=chen,password=123,logindate=Sun Feb 19 09:33:38 CST 2017
之后會在項目目錄下生成文件info_serializable.txt,雖然存成了txt格式,但并不能使用普通文本格式打開,會出現亂碼。
transicent的作用
如果,不希望某些字段被序列化,如LoginInfo中的username,只需在對應字段的定義時使用關鍵詞transient:
// private String password;
private transient String password;
再次運行程序,結果如下:
parameter constructor
username=chen,password=123,logindate=Sun Feb 19 09:50:34 CST 2017
Serialize object
Deserialize object
username=chen,password=null,logindate=Sun Feb 19 09:50:34 CST 2017
password反序列化后的結果為null,達到了保護密碼的目的。
方式二:實現Externalizable接口
Externalizable源碼
/*
* Copyright (c) 1996, 2004, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package java.io;
import java.io.ObjectOutput;
import java.io.ObjectInput;
/**
* @author unascribed
* @see java.io.ObjectOutputStream
* @see java.io.ObjectInputStream
* @see java.io.ObjectOutput
* @see java.io.ObjectInput
* @see java.io.Serializable
* @since JDK1.1
*/
public interface Externalizable extends java.io.Serializable {
/**
* @param out the stream to write the object to
* @exception IOException Includes any I/O exceptions that may occur
*/
void writeExternal(ObjectOutput out) throws IOException;
/**
* @param in the stream to read data from in order to restore the object
* @exception IOException if I/O errors occur
* @exception ClassNotFoundException If the class for an object being
* restored cannot be found.
*/
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
Externalizable繼承了Serializable接口,并添加了兩個新的方法,分別表示在哪些字段能被序列化和哪些字段能夠反序列化。
java實現
erialVersionUID和之前一樣,最好添加。(但不添加的話,eclipse竟然連警告都沒有!!無奈,只能自己隨便寫個數了。)
package serialize;
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.util.Date;
public class LoginInfo2 implements Externalizable{
private static final long serialVersionUID = 4297291454171868241L;
private String username;
private String password;
private Date logindate;
public LoginInfo2(){
System.out.println("non-parameter constructor");
}
public LoginInfo2(String username, String password){
System.out.println("parameter constructor");
this.username = username;
this.password = password;
this.logindate = new Date();
}
public String toString(){
return "username="+username+
",password="+password+
",logindate="+logindate;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(logindate);
out.writeUTF(username);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
logindate = (Date)in.readObject();
username = (String)in.readUTF();
}
public static void main(String[] args) throws Exception{
LoginInfo2 info = new LoginInfo2("chen","123");
System.out.println(info);
String fileName = "info_externalizable.txt";
System.out.println("Serialize object");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName));
oos.writeObject(info);
oos.close();
System.out.println("Deserialize object");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName));
LoginInfo2 info2 = (LoginInfo2)ois.readObject();
ois.close();
System.out.println(info2);
}
}
運行結果:
parameter constructor
username=chen,password=123,logindate=Sun Feb 19 10:53:57 CST 2017
Serialize object
Deserialize object
non-parameter constructor
username=chen,password=null,logindate=Sun Feb 19 10:53:57 CST 2017
實現了和Serializable+transient同樣的目的。
無參構造函數
仔細分析運行結果,發現這種方式反序列化的時候竟然調用了無參構造函數。對于恢復Serializable對象,完全以它存儲的二進制為基礎來構造,而不調用構造函數。而對于一個Externalizable對象,public的無參構造函數將會被調用。如果沒有public的無參構造函數,運行時會報異常(Invalid Class Exception : LoginInfo2 ; no valid constructor...),之后會調用readExternal 讀取數據。源碼的注釋中也做了如下說明:
Object Serialization uses the Serializable and Externalizable interfaces. Object persistence mechanisms can use them as well. Each object to be stored is tested for the Externalizable interface.
- If the object supports Externalizable, the writeExternal method is called. If the object does not support Externalizable and does implement Serializable, the object is saved using ObjectOutputStream.
- When an Externalizable object is reconstructed, an instance is created using the public no-arg constructor, then the readExternal method called. Serializable objects are restored by reading them from an ObjectInputStream.
transcient還有效果嗎
對于externalizable實現方式的代碼做如下修改:
// private Date logindate;
private transient Date logindate;
運行結果與代碼改動之前一樣,說明transcient在externalizable實現的類中失效了
總結
-
兩個流:objectOutputStream、ObjectInputStream
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName)); oos.writeObject(info); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); LoginInfo info2 = (LoginInfo)ois.readObject(); ois.close();
-
serialVersionUID的作用
Java的序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本一致性的,所以實現Serializable或Externalizable接口都最好有serialVersionUID這一字段,沒有會報警告。
-
Serializable與Externalizable的關系
Serializable是個空接口,用于指示某類的對象是否可以序列化。Externalizable接口繼承了Serializable接口,添加了writeExternal、readExternal方法。
-
transient關鍵字
在Serializable接口實現的類中,transient修飾的字段不參與序列化過程;在Externalizable接口實現的類中,transient無效。即:transient只能與Serializable搭配使用。
public class LoginInfo implements Serializable{ private static final long serialVersionUID = 8685376332791485990L; private String username; private transient String password; //不參與序列化過程 ...... }
-
writeExternal、readExternal 與無參構造函數
Externalizable實現的類對象,序列化與反序列化由writeExternal與readExternal兩個函數控制(內部操作的字段要對應),而且反序列時會先調用無參構造函數創建實例對象,再通過readExternal讀取數據。所以Externalizable實現的類若想反序列化,必須有無參構造函數。而恢復Serializable對象,完全以之前存儲的二進制為基礎來構造,不調用構造函數。