問題背景
今天在解決一個對象的持久化問題時,需要用到序列化技術。一開始,我想用 fastjson,但是麻煩的是這個框架是基於 getter 方法來序列化對象的,可是我序列化的對象不是一個標准的 Java Bean 對象,沒有 getter/setter 方法。而我的需求是根據字段(類成員變量)來序列化對象。然后我就想到了使用 Java 序列化技術,並且配合使用 transient 屏蔽不需要參與序列化的字段(屬性)。更多 transient 關鍵字的信息可以參考這篇文章:transient 變量修飾符
但是問題來了,先上代碼:
// User 類是我要序列化保存,並且再用反序列化恢復的類
public class User implements Serializable {
private String userName;
private transient List<String> tags;
public User(String userName) {
this.userName = userName;
this.tags = new ArrayList<String>();
}
public User(String userName, List<String> tags) {
this.userName = userName;
this.tags = tags;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", tags=" + tags +
'}';
}
}
我的測試類是
public class JavaSerializationTest {
@Test
public void test1() {
User user = new User("kendoziyu", Arrays.asList("cool", "smart"));
byte[] data = SerializationUtils.serialize(user);
User copyUser = SerializationUtils.deserialize(data);
System.out.println(copyUser);
User expiredUser = new User("kendoziyu");
System.out.println(expiredUser);
}
}
結果:

問題所在:
我希望反序列化之后 tags 對象是一個空集合,但不是一個 null!
后來我也找到了替代方案:
Externalizable 是 Serializable 的子類接口,Externalizable 是一個手動序列化接口,而 Serializable 是一個自動序列化接口。
public class NewUser implements Externalizable {
private String userName;
private List<String> tags;
public NewUser() {
tags = new ArrayList<String>();
}
public NewUser(String userName) {
this.userName = userName;
this.tags = new ArrayList<String>();
}
public NewUser(String userName, List<String> tags) {
this.userName = userName;
this.tags = tags;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", tags=" + tags +
'}';
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(this.userName);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.userName = (String) in.readObject();
}
}
- 此時不需要加入 transient 關鍵字了,取而代之的是,在 writeExternal 方法中,手動編碼控制需要序列化哪些字段。
- 這個方法需要一個默認的構造函數,否則會拋出異常 org.apache.commons.lang3.SerializationException: java.io.InvalidClassException: test.java.serialization.NewUser; no valid constructor
- 當然有了默認構造函數之后,我們就可以在默認構造函數中,初始化我們的集合
產生疑惑
問題雖然解決了,但是同樣是 Serializable 接口,為什么一個需要默認構造函數,一個不需要?
分析源碼
java原生序列化中反序列化時,是如何創建對象的 這篇文檔中有貼源碼,我這里就不貼源碼了:
- 假如,反序列化的目標類實現了 Externalizable 接口,那么就通過反射得到就是該目標類對應的默認構造函數:
// cl 在這里就是我們的 User.class,這段代碼在 ObjectStreamClass 的靜態方法 getExternalizableConstructor 中
Constructor<?> cons = cl.getDeclaredConstructor((Class<?>[]) null);
- 假如,反序列化的目標類實現了 Serializable 接口,那么就會獲取 Object 的默認構造函數
// 這段代碼是從 ObjectStreamClass 的靜態方法 getSerializableConstructor 中節選的,它會一直調用 getSuperClass 找父類
// 最終找到了 Object,並且獲取了 Object 的默認構造函數
Class<?> initCl = cl;
while (Serializable.class.isAssignableFrom(initCl)) {
Class<?> prev = initCl;
if ((initCl = initCl.getSuperclass()) == null ||
(!disableSerialConstructorChecks && !superHasAccessibleConstructor(prev))) {
return null;
}
}
Constructor<?> cons = initCl.getDeclaredConstructor((Class<?>[]) null);
...
獲取到了構造函數對象 Constructor,就可以通過反射創建 Java 對象了:
// 實例化的調用
cons.newInstance();
但是有一個比較納悶的問題,那就是用 Object 的構造函數為啥還能創建子類對象??
我們可以看一下 Constructor#newInstance 方法:
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
...
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
實例化,還是要靠 ConstructorAccessor 實例。而此時該對象實例,是在 MethodAccessorGenerator#generateSerializationConstructor 方法中通過 asm 操作字節碼返回的全新SerializationConstructorAccessorImpl 對象,通過這個對象就可以創建我們想要的 User 對象了。但是這種方式反序列化創建對象時,是不會調用我們源代碼 User 中的默認構造函數的。
附錄
引入工具包
序列化的方法,在 Apache 中已經有了實現,參考 org.apache.commons.lang3.SerializationUtils 。你們可以使用以下 maven 配置引入依賴:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
序列化工具類 SerializationUtils
下面這個類來自於 org.apache.commons.lang.SerializationUtils,是序列化的工具類。如果不想導入包,可以直接用這段代碼:
public class SerializationUtils {
public SerializationUtils() {
}
public static Object clone(Serializable object) {
return deserialize(serialize(object));
}
public static void serialize(Serializable obj, OutputStream outputStream) {
if (outputStream == null) {
throw new IllegalArgumentException("The OutputStream must not be null");
} else {
ObjectOutputStream out = null;
try {
out = new ObjectOutputStream(outputStream);
out.writeObject(obj);
} catch (IOException var11) {
throw new SerializationException(var11);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException var10) {
}
}
}
}
public static byte[] serialize(Serializable obj) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
serialize(obj, baos);
return baos.toByteArray();
}
public static Object deserialize(InputStream inputStream) {
if (inputStream == null) {
throw new IllegalArgumentException("The InputStream must not be null");
} else {
ObjectInputStream in = null;
Object var2;
try {
in = new ObjectInputStream(inputStream);
var2 = in.readObject();
} catch (ClassNotFoundException var12) {
throw new SerializationException(var12);
} catch (IOException var13) {
throw new SerializationException(var13);
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException var11) {
}
}
return var2;
}
}
public static Object deserialize(byte[] objectData) {
if (objectData == null) {
throw new IllegalArgumentException("The byte[] must not be null");
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
return deserialize((InputStream)bais);
}
}
}
