Java面向對象系列[v1.0.0][自定義序列化]

自定義序列化

當一個類裏包含的某些實例變量是敏感信息,這個時候不希望系統將該實例變量進行序列化,或者某個實例變量的類型是不可序列化的,因此不希望對該實例變量進行遞歸序列化,從而避免報java.io.NotSerializableException

當對某個對象進行序列化時,系統會自動把該對象的所有實例變量依次進行序列化,如果某個實例變量引用到另一個對象,則被引用的對象也會被序列化,如果被引用的對象的實例變量引用了其他對象,則被應用的對象也會被序列化,這種情況稱爲遞歸序列化

transient關鍵字

序列化

通過在實例變量前使用transient關鍵修飾,告訴Java序列化的時候無需理會該實例變量,如下代碼所示:

public class Person
	implements java.io.Serializable
{
	private String name;
	private transient int age;
	// 注意此處沒有提供無參數的構造器!
	public Person(String name, int age)
	{
		System.out.println("有參數的構造器");
		this.name = name;
		this.age = age;
	}
	// 省略name與age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}
}

transient關鍵字只能用於修飾實例變量

反序列化

import java.io.*;

public class TransientTest
{
	public static void main(String[] args)
	{
		try (
			// 創建一個ObjectOutputStream輸出流
			var oos = new ObjectOutputStream(new FileOutputStream("transient.txt"));
			// 創建一個ObjectInputStream輸入流
			var ois = new ObjectInputStream(new FileInputStream("transient.txt")))
		{
			var per = new Person("孫悟空", 500);
			// 系統會per對象轉換字節序列並輸出
			oos.writeObject(per);
			// 從序列化文件中讀取該Person對象
			var p = (Person) ois.readObject();
			// 輸入該Person對象的age實例變量值,輸入0,因爲age被transient關鍵字修飾
			System.out.println(p.getAge());
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

自定義序列化I

使用transient修飾的實例變量被完全隔離在序列化機制以外,反序列化的時候完全無法獲取,除了這種情況外Java還提供了一種自定義序列化機制,通過這種自定義序列化機制可以讓程序控制如何序列化各實例變量,甚至完全不序列化某些實例變量,需要做這種特殊處理的類必須提供如下特殊簽名方法,這些方法用於實現自定義序列化

  • private void writeObject(java.io.ObjectOutputStream out)throws IOException: 負責寫入特定類的實力狀態,從而相應的readObject()方法可以恢復它,通過重寫該方法,程序員可以完全獲得對序列化機制的控制,可以自主決定哪些實例變量需要序列化,需要如何序列化,默認情況下,該方法會調用out.defaultWriteObject來保存Java對象的各實例變量,從而可以實現序列化Java對象
  • private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException: 負責從流中讀取並恢復對象實例變量,通過重寫該方法,可以完全獲得對反序列化機制的控制,可以自主決定需要反序列化哪些實例變量,以及如何進行反序列化,默認情況下,該方法會調用in.defaultReadObject來恢復Java對象的沒有被transient修飾的實例變量。通常情況下readObject()方法與writeObject()方法對應,如果writeObject()方法中對Java對象的實例變量進行了一些處理,則應該在readObject()方法中對其實例變量進行相應的飯處理從而正確恢復該對象
  • private void readObjectNoData()throws ObjectStreamException: 當序列化流不完整時,readObjectNoData()方法可以用來正確地初始化反序列化對象
import java.io.*;

public class Person
	implements java.io.Serializable
{
	private String name;
	private int age;
	// 注意此處沒有提供無參數的構造器!
	public Person(String name, int age)
	{
		System.out.println("有參數的構造器");
		this.name = name;
		this.age = age;
	}
	// 省略name與age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}

	private void writeObject(java.io.ObjectOutputStream out)
		throws IOException
	{
		// 將name實例變量的值反轉後寫入二進制流
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	private void readObject(java.io.ObjectInputStream in)
		throws IOException, ClassNotFoundException
	{
		// 將讀取的字符串反轉後賦給name實例變量
		this.name = ((StringBuffer) in.readObject()).reverse()
			.toString();
		this.age = in.readInt();
	}
}

自定義序列化II

還有一種更徹底的自定義機制,它甚至可以在序列化對象的時候將該對象替換成其他對象,要實現此種替換,必須爲序列化類提供方法ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;只要該方法存在,序列化機制就會調用它

import java.util.*;
import java.io.*;

public class Person
	implements java.io.Serializable
{
	private String name;
	private int age;
	// 注意此處沒有提供無參數的構造器!
	public Person(String name, int age)
	{
		System.out.println("有參數的構造器");
		this.name = name;
		this.age = age;
	}
	// 省略name與age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}

	//	重寫writeReplace方法,程序在序列化該對象之前,先調用該方法
	private Object writeReplace() throws ObjectStreamException
	{
		ArrayList<Object> list = new ArrayList<>();
		list.add(name);
		list.add(age);
		return list;
	}
}

Java的序列化機制保證在序列化某個對象之前,先調用該對象的writeReplace()方法,如果該方法返回另一個Java對象,則系統轉爲序列化另一個對象

import java.io.*;
import java.util.*;

public class ReplaceTest
{
	public static void main(String[] args)
	{
		try (
			// 創建一個ObjectOutputStream輸出流
			var oos = new ObjectOutputStream(new FileOutputStream("replace.txt"));
			// 創建一個ObjectInputStream輸入流
			var ois = new ObjectInputStream(new FileInputStream("replace.txt")))
		{
			var per = new Person("孫悟空", 500);
			// 系統將per對象轉換字節序列並輸出
			oos.writeObject(per);
			// 反序列化讀取得到的是ArrayList
			var list = (ArrayList) ois.readObject();
			System.out.println(list);
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

writeReplace()方法是遞歸的,系統調用writeObject()方法的時候,發現傳入的對象裏有writeReplace()則會執行它,從而序列化另一個對象,然而如果另一個對象裏還有writeReplace()方法,則系統會轉而執行該writeReplace()方法,以此類推,直到不再返回另一個對象爲止
與writeReplace()方法相對應的,序列化機制裏還有一個特殊的方法,他可以實現保護性複製整個對象ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;這個方法會緊接着readObject()之後被調用,該方法的返回值將會代替原來反序列化的對象,而原來readObject()反序列化的對象會被立刻丟棄

import java.io.*;

public class Orientation
	implements java.io.Serializable
{
	public static final Orientation HORIZONTAL = new Orientation(1);
	public static final Orientation VERTICAL = new Orientation(2);
	private int value;
	private Orientation(int value)
	{
		this.value = value;
	}
	// 爲枚舉類增加readResolve()方法
	private Object readResolve() throws ObjectStreamException
	{
		if (value == 1)
		{
			return HORIZONTAL;
		}
		if (value == 2)
		{
			return VERTICAL;
		}
		return null;
	}
}

Orientation類的構造器私有,程序只有兩個Orientation對象,分別通過HORIZONTAL和VERTICAL 兩個常量來引用,如果讓該類實現Serializable接口,則會引發一個問題,例如將一個Orientation.HORIZONTAL值序列化後再讀出

import java.io.*;

public class ResolveTest
{
	public static void main(String[] args)
	{
		try (
			// 創建一個ObjectOutputStream輸入流
			var oos = new ObjectOutputStream(new FileOutputStream("transient.txt"));
			// 創建一個ObjectInputStream輸入流
			var ois = new ObjectInputStream(new FileInputStream("transient.txt")))
		{
			oos.writeObject(Orientation.HORIZONTAL);
			var ori = (Orientation) ois.readObject();
			System.out.println(ori == Orientation.HORIZONTAL);
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
}

這個時候如果直接用ori和Orientation.HORIZONTAL值進行比較,將會返回false,也就是說ori是一個新的Orientation對象,並不等於Orientation類中的任何一個枚舉值,反序列化機制在恢復Java對象時無需調用構造器來初始化Java對象,從這個層面講序列化機制可以用來克隆對象,這個時候readResolve()方法就有了意義
所有的單例類、枚舉類在實現序列化的時候都應該提供readResolve()方法,這樣纔可以保證反序列化的對象依然正常
存在的弊端是writeReplace()和readResolve()方法都可以使用任意的訪問控制符,因此父類的該兩個方法可以被子類繼承,而如果子類沒有重寫該兩個方法,子類反序列化的時候將會得到一個父類的對象,因此建議使用final類重寫readResolve()和writeReplace()方法,否則應儘量使用private修飾該兩個方法

自定義序列化III

Java還可以由開發者決定存儲和恢復哪些對象數據,要實現這種方式必須實現Externalizable接口,該接口定義瞭如下兩個方法:

  • void readExternal(ObjectInput in): 需要序列化的類實現readExternal()方法來實現反序列化,該方法調用DataInput(它是ObjectInput的父接口)的方法來恢復基本類型的實例變量值,調用ObjectInput的readObject()方法來恢復應用類型的實例變量值
  • void writeExternal(ObjectOutput out): 需要序列化的類實現writeExternal()方法來保存對象的狀態,該方法調用DataOutput(它是ObjectOutput的父接口)的方法來保存基本類型的實力變量值,調用ObjectOutput的writeObject()方法來保存引用類型的實例變量值
import java.io.*;

public class Person implements java.io.Externalizable
{
	private String name;
	private int age;
	// 注意必須提供無參數的構造器,否則反序列化時會失敗。
	public Person(){}
	public Person(String name, int age)
	{
		System.out.println("有參數的構造器");
		this.name = name;
		this.age = age;
	}
	// 省略name與age的setter和getter方法

	// name的setter和getter方法
	public void setName(String name)
	{
		this.name = name;
	}
	public String getName()
	{
		return this.name;
	}

	// age的setter和getter方法
	public void setAge(int age)
	{
		this.age = age;
	}
	public int getAge()
	{
		return this.age;
	}

	public void writeExternal(java.io.ObjectOutput out)
		throws IOException
	{
		// 將name實例變量的值反轉後寫入二進制流
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	public void readExternal(java.io.ObjectInput in)
		throws IOException, ClassNotFoundException
	{
		// 將讀取的字符串反轉後賦給name實例變量
		this.name = ((StringBuffer) in.readObject()).reverse().toString();
		this.age = in.readInt();
	}
}
  • Person類實現了java.io.Externalizable接口,該Person類還實現了readExternal()、writeExternal()兩個方法,這兩個方法除方法簽名和readObject()、writeObject()兩個方法的方法簽名不同外,其方法體完全一樣
  • 如果程序需要序列化實現Externalizable接口的對象,一樣調用ObjectOutputStream的writeObject()方法輸出該對象即可;反序列化該對象,則調用ObjectInputStream的readObject()方法
  • 當使用Externalizable機制反序列化對象時,程序會先使用public的無參數構造器創建實例,然後才執行readExternal()方法進行反序列化,因此實現Externalizable的序列化類必須提供public的無參數構造器

注意事項

  • 對象的類名、實例變量(包括基本類型、數組、對其他對象的引用)都會被序列化;方法、類變量(即static修飾的成員變量)、transient實例變量都不會被序列化
  • 實現Serializable接口的類如果需要讓某個實例變量不被序列化,則可以使用transient修飾,而不是加static關鍵字,雖然它也可以達到效果
  • 保證序列化對象的實例變量類型也是可序列化的,否則需要使用transient關鍵字來修飾該實例變量,否則該類是不可序列化的
  • 反序列化對象時必須有序列化對象的class文件
  • 當通過文件、網絡來讀取序列化後的對象時,必須按實際寫入的順序讀取

版本

反序列化對象時必須提供該對象的class文件,而class可能會升級,Java如何保證兩個class文件的兼容性?,序列化機制允許爲序列化類提供一個private static final的serialVersionUID值,用於標識該Java類的序列化版本,如果該Java類升級了,但是標識不變序列化機制仍會當成同一個序列化版本來處理

public class Test
{
	// 爲該類指定一個serialVersionUID類變量值
	private static final long serialVersionUID = 512L;
	...
}

爲了反序列化時確保序列化版本的兼容性,最後在每個要序列化的類中加入private static final long serialVersionUID這個類變量,否則該類變量有JVM根據類的相關信息計算,而java類升級後,JVM計算的值就會發生變化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章