本文主要從以下方面記錄:
1、Java序列化和反序列化是什么?
2、為什么需要序列化與反序列化?
3、怎么實現Java序列化和反序列化?
4、幾個序列化注意事項
一、Java序列化和反序列化是什么?
通俗的來講,序列化過程就是將對象轉成二進制流存入內存或者文件,反序列化從內存或文件中讀取二進制流轉換成對象。
二、為什么需要序列化與反序列化?
其實這個問題也就是它們的應用場景有哪些?這樣就容易回答多了。比如文件(文本、圖片等)進行傳輸,這些文件都是通過二進制序列的形式進行傳輸的(序列化過程),而接收方則要讀取這些二進制數據進行相對應的轉換(反序列化過程)。除了這個它主要用於網絡傳輸(進程之間的通信等)。
三、怎么實現Java序列化和反序列化?
要想實現序列化有個必要條件就是要實現Serializable接口或Externalizable接口。大部分可能只知道有Serializable接口沒有關注Externalizable接口,那么你看了本文之后就應該知道了,后面再介紹它們的區別。
有了上面個必要條件后還需要借助jdk中有兩個類:java.io.ObjectOutputStream和java.io.ObjectInputStream,它們分別負責序列化和反序列化。我們可以看下這兩個類的說明就知道是這兩個類負責相對應的功能。
/** * An ObjectOutputStream writes primitive data types and graphs of Java objects * to an OutputStream. The objects can be read (reconstituted) using an * ObjectInputStream. Persistent storage of objects can be accomplished by * using a file for the stream. If the stream is a network socket stream, the * objects can be reconstituted on another host or in another process. * * <p>Only objects that support the java.io.Serializable interface can be * written to streams. The class of each serializable object is encoded * including the class name and signature of the class, the values of the * object's fields and arrays, and the closure of any other objects referenced * from the initial objects. * ... */ public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants{}
/** * An ObjectInputStream deserializes primitive data and objects previously * written using an ObjectOutputStream. * * <p>ObjectOutputStream and ObjectInputStream can provide an application with * persistent storage for graphs of objects when used with a FileOutputStream * and FileInputStream respectively. ObjectInputStream is used to recover * those objects previously serialized. Other uses include passing objects * between hosts using a socket stream or for marshaling and unmarshaling * arguments and parameters in a remote communication system. * ... */ public class ObjectInputStream extends InputStream implements ObjectInput, ObjectStreamConstants{ }
下面我們以學生對象為例,將對象序列化保存至文件中,再從文件中反序列化轉換成對象。
import java.io.Serializable; /** * @Description: 學生類 已實現序列化接口 * @author yuanfy * @date 2018年1月11日 上午11:36:37 * @version 1.0 */ public class Student implements Serializable{ private static final long serialVersionUID = 6415983562512521049L; private String name; private int age; private String sex; public Student(String name, int age, String sex) { this.name = name; this.age = age; this.sex = sex; } 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 String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + ", sex=" + sex + "]"; } }
上面已經定義Java對象, 下面進行序列化測試。
@Test public void testSerializable() throws FileNotFoundException, IOException{ Student s = new Student("james", 31, "man"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("E:\\Student.txt"))); oos.writeObject(s); oos.close(); }
如果電腦E盤存在的話運行肯定通過(windows系統),單元測試通過后(說明序列化過程已完成)查看Student.txt文件中的內容,如下:
上面是正常的情況, 如果Student類沒有實現Serializable接口呢?那么序列化時會存在什么樣的問題。我們把Student類去掉Serializable接口的實現,然后再進行序列化測試。這時你會發現單元測試后不通過,先看看報錯原因:下面截圖中的錯誤信息提示Student類沒有被序列化。
那么為什么java.io.ObjectOutputStream類會拋出這樣的異常呢,它是怎么識別有沒有實現Serializable接口的。接下來,我們來看看他的源碼:writeObject方法中調用了writeObject()方法,所以主要邏輯在它里面。
/** * Underlying writeObject/writeUnshared implementation. */ private void writeObject0(Object obj, boolean unshared) throws IOException { boolean oldMode = bout.setBlockDataMode(false); depth++; try { //前面部分代碼省略 // remaining cases if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) {//判斷是否是Serializable的子類。 writeOrdinaryObject(obj, desc, unshared); } else { //除了String類型、數組類型和枚舉類型,其他對象如果沒有實現Serializable接口,都會拋出NotSerializableException異常 if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } } finally { depth--; bout.setBlockDataMode(oldMode); } }
從中可以看出,除了String類型、數組類型和枚舉類型,其他對象如果沒有實現Serializable接口,都會拋出NotSerializableException異常。
接下來進行反序列化測試:
@Test public void testDeserialize() throws FileNotFoundException, IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("E:\\Student.txt"))); Student s = (Student)ois.readObject(); System.out.println(s); ois.close(); }
在進行正常的序列化測試后接着測試反序列化測試,正常來說是不會報錯的。看看測試結果就明了了。
同樣上面是正常的情況。接下來異常的情況就能體現出Student類中serialVersionUID變量的作用了:它就是驗證序列化與反序列化的唯一性。在序列化后修改Student類中的serialVersionUID= 61234L,然后再測試反序列化,測試結果如下:
從錯誤提示中可以看出,會從解析出來的serialVersionUID和要轉換的class中serialVersionUID做對比,判斷是否相等。java.io.ObjectStreamClass關鍵源碼如下:
/** * Initializes class descriptor representing a non-proxy class. */ void initNonProxy(ObjectStreamClass model, Class<?> cl, ClassNotFoundException resolveEx, ObjectStreamClass superDesc) throws InvalidClassException { long suid = Long.valueOf(model.getSerialVersionUID()); ObjectStreamClass osc = null; if (cl != null) { osc = lookup(cl, true); if (osc.isProxy) { throw new InvalidClassException( "cannot bind non-proxy descriptor to a proxy class"); } if (model.isEnum != osc.isEnum) { throw new InvalidClassException(model.isEnum ? "cannot bind enum descriptor to a non-enum class" : "cannot bind non-enum descriptor to an enum class"); } //這里會判斷serialVersionUID是否一致 if (model.serializable == osc.serializable && !cl.isArray() && suid != osc.getSerialVersionUID()) { throw new InvalidClassException(osc.name, "local class incompatible: " + "stream classdesc serialVersionUID = " + suid + ", local class serialVersionUID = " + osc.getSerialVersionUID()); } //后面代碼略 } }
四、幾個序列化注意事項
1、對繼承父類的子類序列化
public class Person { public int age; public int weight; public Person(int age, int weight) { this.age = age; this.weight = weight; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } }
import java.io.Serializable; public class Women extends Person implements Serializable{ private static final long serialVersionUID = -1259423203325949704L; private String name; public Women(String name, int age, int weight) { super(age, weight); this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + ", weight=" + weight + "]"; } }
單元測試代碼如下,先猜錯下會不會運行通過,結果又是啥?
@Test public void test2() throws FileNotFoundException, IOException, ClassNotFoundException{ Women w = new Women("Scarlett Johansson", 34, 50); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("E:\\Women.txt"))); oos.writeObject(w); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("E:\\Women.txt"))); Women s = (Women)ois.readObject(); System.out.println(s); ois.close(); }
測試結果如下,居然報錯。該錯誤提示需要提供無參構造函數(During deserialization, the fields of non-serializable classes will be initialized using the public or protected no-arg constructor of the class. A no-arg constructor must be accessible to the subclass that is serializable. The fields of serializable subclasses will be restored from the stream.),具體詳情解釋見這here.
當我們提供無參構造函數后再進行單元測試得到結果:Student [name=Scarlett Johansson, age=0, weight=0]。所以可以得出結論:父類中的字段不參與序列化,只是將其初始化而已。
2、transient 關鍵字
使用transient關鍵字來修飾變量,然后進行序列化(在實現serializable接口情況下)效果其實和父類的序列化一樣,它所修飾的變量不參與序列化。這里就不舉例說明了,自己可以寫個案例測試下。
另外transient關鍵字只能修飾變量, 不能修飾類和方法。
3、static關鍵字
使用static關鍵字來修飾變量,不管有沒有transient修飾,同樣不參與序列化(在實現serializable接口情況下)。
4、Externalizable接口
Externalizable接口extends Serializable接口,而且在其基礎上增加了兩個方法:writeExternal()和readExternal()。這兩個方法會在序列化和反序列化還原的過程中被自動調用,以便執行一些特殊的操作。
下面看案例:
import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; /** * @Description: 學生類 已實現序列化接口 * @author yuanfy * @date 2018年1月11日 上午11:36:37 * @version 1.0 */ public class Student implements Externalizable{ private static final long serialVersionUID = 61234L; private String name; private transient int age; private static String sex; //如果覆蓋了無參構造函數就一定要顯示聲明無參構造函數,跟父類序列化的案例一樣。 public Student(){} public Student(String name, int age, String sex) { this.name = name; this.age = age; this.sex = sex; } 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 String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + ", sex=" + sex + "]"; } @Override public void writeExternal(ObjectOutput out) throws IOException { // TODO Auto-generated method stub out.writeUTF(name); out.writeInt(age); out.writeObject(sex); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.name = in.readUTF(); this.age = in.readInt(); this.sex = (String)in.readObject(); } }
@Test public void test3() throws FileNotFoundException, IOException, ClassNotFoundException{ Student s1 = new Student("james", 31, "man"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("E:\\Student.txt"))); oos.writeObject(s1); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("E:\\Student.txt"))); Student s2 = (Student)ois.readObject(); System.out.println(s2); ois.close(); }
上面是正常完整案例,其中要注意的是,要序列化的類要提供無參構造函數,否則反序列化會報錯(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.)詳解here。得出結論如下:
與Serizable對象不同,使用Externalizabled,就意味着沒有任何東西可以自動序列化, 為了正常的運行,我們需要在writeExtenal()方法中將自對象的重要信息寫入,從而手動的完成序列化。對於一個Externalizabled對象,對象的默認構造函數都會被調用(包括哪些在定義時已經初始化的字段),然后調用readExternal(),在此方法中必須手動的恢復數據。這就說明在Externalizable接口下不管是transient或static修飾的變量,如果沒有指定寫入,就不會序列化。