Java 基础-- 序列化与反序列化
java 中的序列化

java中的序列化( Serialization )可以让我们把一个对象转换为流( stream ),这样我们才能把它通过网络发送出去、以文件的方式存储到本地、或者存到数据库里,顾名思义,反序列化( Deserialization ) 就是将流再转换为 java 对象 序列化看起来非常简单,但是在实际应用中,也会面临一些问题,本文的后半部分会详细讨论。
本文代码下载 导入 eclipse 或者 idea 中可直接运行
Serializable 接口
在 java 中,一个类想要被序列化,必需要实现 java.io.Serializable 接口,具体的序列化操作被封装在 ObjectInputStream 和 ObjectOutputStream 中,所以我们不用关注具体 序列化 & 反序列化 的细节,只用这两个类,就能完成我们的工作。
下面是一个简单的例子。
User.java
package com.serializable;
import java.io.Serializable;
public class User implements Serializable {
// private static final long serialVersionUID = -7470090944414208496L;
private String name;
private int id;
// private double salary;
transient private String gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
/*
* public double getSalary() { return salary; } public void setSalary(double
* salary) { this.salary = salary; }
*/
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "User{name=" + name + ",id=" + id + ",gender=" + gender + "}";
}
}
User 类是一个简单的 JavaBean ,如果类中的某个变量不希望被序列化,可以使用 transient 关键字。另外,静态变量也不会被序列化。
下面我们将user对象序列化,保存到文件中,然后读取这个文件,再将其反序列化。
SerializationUtil.java
package com.serializable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializationUtil {
// 将文件反序列化为对象
public static Object deserialize(String fileName) throws IOException,
ClassNotFoundException {
FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close();
return obj;
}
// 将对象序列化并存到文件中
public static void serialize(Object obj, String fileName)
throws IOException {
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
fos.close();
}
// 测试
public static void main(String[] args) {
String fileName = "User.ser";
User user = new User();
user.setId(123456);
user.setName("张三");
user.setGender("男");
// 序列化
try {
SerializationUtil.serialize(user, fileName);
} catch (IOException e) {
e.printStackTrace();
return;
}
// 反序列化
User userNew = null;
try {
userNew = (User) SerializationUtil.deserialize(fileName);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
System.out.println("user Object::" + user);
System.out.println("userNew Object::" + userNew);
}
}
运行 Main 方法,因为变量 gender 前加了关键字 transient ,所以没有被序列化到文件中。
输出结果:
user Object::User{name=张三,id=123456,gender=男}
userNew Object::User{name=张三,id=123456,gender=null}
变量 serialVersionUID
细心的读者可能会发现,在 User 类的定义中有一个被注释掉的变量 serialVersionUID ,实际上,如果类中没有定义 serialVersionUID ,编译器也会为你的类自动生成一个 serialVersionUID ,serialVersionUID 的值是根据类中的变量、方法名、包名等相关信息计算出来的,如果你对类的定义做了小小的改动,比如在类中新增一个变量,serialVersionUID 的值也会随之改变。在我将变量 serialVersionUID 注释掉后,我的 IDE 还显示了以下warning信息:
The serializable class User does not declare a static final serialVersionUID field of type long
因此,强烈建议在需要序列化的类中,定义 serialVersionUID 这个变量,否则,可能会造成反序列化失败。还以 user 为例, 上一步已经将 user 对象序列化为 文件 user.ser,然后我们在 User.java 上做一些改动,将变量 salary 还有它的 gettser-setter方法取消注释。然后我们再来尝试将上一步生成的 user.ser 反序列化为 user对象。
DeserializationTest.java
package com.serializable;
import java.io.IOException;
public class DeserializationTest {
public static void main(String[] args) {
String fileName = "user.ser";
User userNew = null;
try {
userNew = (User) SerializationUtil.deserialize(fileName);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
System.out.println("user Object " + userNew);
}
} 运行 main 方法,输出结果:
java.io.InvalidClassException: com.User; local class incompatible: stream classdesc serialVersionUID = -1009812510484884867, local class serialVersionUID = 2307692425311469795
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.SerializationUtil.deserialize(SerializationUtil.java:16)
at com.DeserializationTest.main(DeserializationTest.java:12)
user Object::null
反序列化失败,原因是序列化 user 对象时 编译器计算出来一个 serialVersionUID (假设值为A),中间User类经历了一次改动,在反序列化的时候,User 类的 serialVersionUID 又变为另一个值(假设值为B),程序一检查发现,之前的 A 和现在的 B 不相等( 因为User类被改动了 ),立刻抛出异常。
为了避免这个问题,我们在类中显式的声明这个变量。这样无论类的定义如何变化,serialVersionUID 的值都不会变。
如果您时间有限,或者只是做一些简单的测试,看到这里就够了。
与 Serialization 接口相关的一些方法
上面讲到,在 Java 中,序列化无需过多的操作,我们需要做的就是让类继承 Serialization 接口,然后调用 ObjectInputStream 和 ObjectOutputStream 类来进行相关操作,整个序列化的细节都被封装起来了。但是为了处理一些更细节的东西, Serialization 也提供了以下四个方法。
- readObject(ObjectInputStream ois)
- writeObject(ObjectOutputStream oos)
- Object writeReplace()
- Object readResolve()
下面来用具体的例子,来说明它们的作用,通常为了安全性,以上几个方法都应该被声明为私有方法。
自定义的序列化
readObject() 和 writeObject() 这两个方法允许用户自定义序列化和反序列化的过程。在序列化过程中,jvm 会试图调用对象类里的 writeObject 和 readObject ,如果类中没有定义 这两个方法,则默认调用 defaultWriteObject 方法来执行序列化。来看一个例子:
Account.java
package com.customize;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String userName;
private String password;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private void writeObject(ObjectOutputStream aOutputStream)
throws IOException {
aOutputStream.writeUTF(userName + "***");
aOutputStream.writeUTF(password);
}
private void readObject(ObjectInputStream aInputStream)
throws ClassNotFoundException, IOException {
userName = aInputStream.readUTF();
if (!userName.endsWith("***")) {
throw new IOException("数据错误");
}
userName = userName.substring(0, userName.length() - 3);
password = aInputStream.readUTF();
}
public String toString() {
return "Account{userName=" + userName + ",password=" + password + "}";
}
} 注意,writeObject 中变量写入的顺序,必须与 readObject中 读取的顺序一致。
TestCustomSerialization.java
package com.customize;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TestCustomize {
public static void main(String[] args) throws IOException,
ClassNotFoundException {
Account account = new Account();
account.setUserName("张三");
account.setPassword("123");
// 序列化
FileOutputStream fileOut = new FileOutputStream("account.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(account);
out.close();
fileOut.close();
// 反序列化
Account accountNew = null;
FileInputStream fileIn = new FileInputStream("account.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
accountNew = (Account) in.readObject();
in.close();
fileIn.close();
System.out.println(accountNew);
}
}
运行main方法,输出结果:
Account{userName=张三,password=123}
注意 Account 类中的 writeObject 和 readObject 方法,变量 userName 序列化之前,在后面加了个后缀"***", 反序列化后,检查了一下 userName 是不是带有这个后缀,如果没有,说明数据不完整,需要抛出异常。
序列化与继承
假设一个子类继承了某个第三方的类,子类需要被序列化,但是它的父类并没有实现序列化接口,那么子类能够执行序列化吗?当然可以,假设 SubClass 继承了SuperClass。
SuperClass.java
package com.inheritance;
public class SuperClass {
private int id;
private String value;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
SubClass.java
package com.inheritance;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SubClass extends SuperClass implements Serializable,ObjectInputValidation {
private static final long serialVersionUID = -1322322333926390329L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "SubClass{id=" + getId() + ",value=" + getValue() + ",name="+ getName() + "}";
}
private void readObject(ObjectInputStream ois)
throws ClassNotFoundException, IOException {
ois.defaultReadObject();
// 注意set和write的顺序要一致
setId(ois.readInt());
setValue((String) ois.readObject());
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
// 注意set和write的顺序要一致
oos.writeInt(getId());
oos.writeObject(getValue());
}
@Override
public void validateObject() throws InvalidObjectException {
// 验证对象
if (getId() <= 0)
throw new InvalidObjectException("ID 必须大于0");
}
} 需要注意的有两点
- 在序列化中,writeObject 调用了 defaultWriteObject 来处理父类相关的信息,与 writeObject() 类似,在反序列化中 readObject() 中调用了 defaultReadObject()。
- SubClass 实现了 ObjectInputValidation 接口,并重写了 validateObject() 方法,在 validateObject() 中,用户可以加入自己的业务逻辑,来验证反序列化后对象的完整性。
package com.inheritance;
import java.io.IOException;
import com.serializable.SerializationUtil;
public class TestInheritance {
public static void main(String[] args) {
String fileName = "subclass.ser";
SubClass subClass = new SubClass();
subClass.setId(10);
subClass.setValue("123");
subClass.setName("张三");
try {
SerializationUtil.serialize(subClass, fileName);
} catch (IOException e) {
e.printStackTrace();
return;
}
try {
SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
System.out.println("SubClass = " + subNew);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}
}
运行main 方法,输出结果:
SubClass = SubClass{id=10,value=123,name=张三}
java 序列化代理模式 ( Serialization Proxy Pattern )
序列化代理模式用到了两个关键的方法:
writeReplace()
该方法返回一个 Object ,如果类中定义了 writeReplace() ,序列化进程就会将 writeReplace() 的返回值作为序列化的对象。也就是说,用 writeReplace() 返回的对象,来代替本来要序列化的对象。(所以这个方法名带了个replace)。readResolve()
与 writeReplace 类似,readResolve() 也返回一个 Object,如果类中定义了 readResolve() ,反序列化进程会调用 readResolve() ,最终由 readResolve() 将序列化后的对象返回给调用者。
上面的描述可能过于抽象,下面结合一个例子来说明它们的用法。假设Data类为一个普通的类,我们为Data加入一个代理类 DataProxy。
package com.proxypattern;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Data implements Serializable {
private static final long serialVersionUID = 2287363337312358459L;
private String data;
public Data(String d) {
this.data = d;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
@Override
public String toString() {
return "Data{data=" + data + "}";
}
// 代理类
private static class DataProxy implements Serializable {
private static final long serialVersionUID = 1233455273185436744L;
private String dataProxy;
private static final String PREFIX = "ABC";
private static final String SUFFIX = "DEFG";
public DataProxy(Data d) {
// 数据混淆加密
this.dataProxy = PREFIX + d.data + SUFFIX;
}
private Object readResolve() throws InvalidObjectException {
if (dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)) {
return new Data(dataProxy.substring(3, dataProxy.length() - 4));
} else
throw new InvalidObjectException("数据被损坏");
}
}
// 用 DataProxy 代替 Data
private Object writeReplace() {
return new DataProxy(this);
}
private void readObject(ObjectInputStream ois)
throws InvalidObjectException {
throw new InvalidObjectException("不允许直接反序列化,只能通过反序列化代理实例化");
}
}
Data 和 DataProxy 都实现了 Serializable 接口。
DataProxy 是一个内部私有静态类,所以只有 Data 类可以访问它。
DataProxy 的构造函数接收一个 Data 对象。
Data 类定义了 writeReplace() 方法,该方法返回一个 DataProxy 的实例. 所以在序列化Data对象的时,实际上被序列化的是 DataProxy 对象。
在反序列化时,实际上是先将 DataProxy 反序列化,然后自动调用 readResolve() ,由 readResolve() 将 Data 对象返回给调用者。
readObject 接口拒绝直接序列化,让序列化只能通过代理实现。
使用代理模式可以大大减少序列化过程中的安全问题,防止数据被篡改,虽然在 readObject 中也可以做一些简单的验证,但是作用很有限。所以如果您的类是可序列化的,强烈建议您使用这种模式。
package com.proxypattern;
import java.io.IOException;
import com.serializable.SerializationUtil;
public class TestProxyPattern {
public static void main(String[] args) {
String fileName = "data.ser";
Data data = new Data("张三");
try {
SerializationUtil.serialize(data, fileName);
} catch (IOException e) {
e.printStackTrace();
}
try {
Data newData = (Data) SerializationUtil.deserialize(fileName);
System.out.println(newData);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}}
运行main方法,输出结果:
> Data{data=张三}
## 接口 Externalizable
Externalizable 其实功能和 Serializable 差不多,网络上也有很多关于 Serializable 和 Externalizable 的讲解和对比,但是笔者认为Externalizable已经过时,在著名问答网站上,有这么一段回答,翻译成中文大概意思是:
>通过实现 java.io.Serializable 你的类就自动成为了可序列化的,用户无需再进行其他操作,java 会运用反射机制,自动组装和拆解对象,在早期的java版本,反射是非常慢的,所以需一旦遇上大量的体积庞大对象,就会造成性能问题。为了解这个问题,Externalizable应运而生。但是自动java1.3以后,反射机制比以前快的多,所以也就不再有那么多的性能问题,再加上 Externalizable 使用起来比较麻烦,所以Externalizable几乎已经没什么用处了。总的来说,Externalizable 是java1.1 遗留的产物,现在已经没有必要再使用了。
[查看原文出处](https://stackoverflow.com/questions/817853/what-is-the-difference-between-serializable-and-externalizable-in-java)
本着科普的精神,我们简单的说一下Externalizable的用法
Externalizable 继承自 Serializable,也是用于序列化的接口,在使用上稍有不同——让类继承 *java.io.Externalizable* 接口,然后重写 writeExternal() 和 readExternal() 方法,writeExternal() 用于序列化, *readExternal()* 用于反序列化。以Dog类为例:
**Dog.java**
```java
package com.externalizable;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Dog implements Externalizable {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(id);
out.writeObject(name + "123");
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
id = in.readInt();
name = (String) in.readObject();
if (!name.endsWith("123"))
throw new IOException("数据错误");
name = name.substring(0, name.length() - 3);
}
@Override
public String toString() {
return "Dog{id=" + id + ",name=" + name + "}";
}
}ExternalizationTest.java
package com.externalizable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ExternalizationTest {
public static void main(String[] args) {
String fileName = "dog.ser";
Dog dog = new Dog();
dog.setId(123);
dog.setName("doggie");
try {
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(dog);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
FileInputStream fis;
try {
fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
Dog dogNew = (Dog) ois.readObject();
ois.close();
System.out.println("读取Dog对象: " + dogNew);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
} 注意,使用 Externalizable 必须重写 writeExternal() 和 readExternal()
运行main方法,输出结果:
读取Dog对象: Dog{id=123,name=doggie}
