Java的序列化与反序列化
一、简介
对于接触过Java的应该都知道对象是怎么回事,它是类的一个实例也可以说是类的一种状态。那我们知道类的存储方式就是一个class文件,那要如何去存储一个对象呢?这就是序列化的存在作用。首先得知道计算机中存储的基本单位字节(Byte),所以得先把对象转为基本单位再存储到计算机上,那么序列化机制就是负责完成这个转换工作。
序列化与反序列研究的是两个相互的过程,也就是java对象和字节间的转换过程。
1.1、定义
- 序列化就是指把Java对象转换成字节序列的过程;
- 反序列化就是指把字节序列转换成Java对象的过程。
1.2、作用
序列化:
- 序列化将对象转换为字节序列,在传递和保存对象时确保对象的完整性和可传递性;
- 序列化把对象转换成有序的字节流,以便在网络中传输以及保存到本地文件;
- 序列化后转换成的字节流将保存对象的状态以及相关的描述信息;
- 序列化机制的核心作用就是对象状态的保存与重建;
- 通过序列化机制实现进程间的通信。
反序列化:
- 反序列化从网络上或者本地文件获得字节流后,根据字节流中保存的对象状态及描述的信息来重建对象。
二、如何实现对象的序列化和反序列化
2.1、必备条件
只有实现了Serializable
或者Externalizable
接口的类的对象才能被序列化成字节序列,如果没有实现相关接口,在序列化时会抛出异常:java.io.NotSerializableException
。
2.2、JDK中序列化和反序列化的API
- 在序列化中使用到的API是对象输出流:
java.io.ObjectOutputStream
该类的writeObject(Object obj)
方法将将传入的obj对象进行序列化,把得到的字节序列写入到目标输出流中进行输出。 - 在反序列化中使用到的API是对象输入流:
java.io.ObjectInputStream
该类的readObject()方法从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回。
2.3、对象可序列的三种实现
2.3.1、仅仅实现Serializable接口
创建一个实现了Serializable接口的User 类,实例化一个对象,并将改状态的对象序列化保存到本地;再通过反序列化将文件中的对象转换为对象,这一系列操作该对象的状态得以持久化保存。
User类:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private String password;
public User(int id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
/*
*省略get\set方法
*/
}
测试序列化和反序列化:
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("E:/user.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User user = new User(1,"Albert","123456");
oos.writeObject(user);
oos.flush();
oos.close();
System.out.println("序列化完成!");
FileInputStream fis = new FileInputStream("E:/user.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
User oldUser = (User) ois.readObject();
System.out.println("反序列化完成!id="+oldUser.getId()+" name="+oldUser.getName()+" password="+oldUser.getPassword());
} catch (Exception e) {
e.printStackTrace();
}
2.3.2、实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out)方法。
此时该类实例化的对象都将采用以下定义的方式进行序列化与反序列化。
User类:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private String password;
public User(int id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
/*
*省略get\set方法
*/
public void readObject(ObjectInputStream in) throws Exception{
id = in.readInt();
//name = (String)in.readObject();
password = (String)in.readObject();
}
public void writeObject(ObjectOutputStream out)throws IOException {
out.writeInt(id);
out.writeObject(name);
out.writeObject(password);
}
}
测试序列化和反序列化:
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("E:/user.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User user = new User(1, "Albert", "123456");
user.writeObject(oos);
System.out.println("序列化完成!");
FileInputStream fis = new FileInputStream("E:/user.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
User oldUser = new User();
oldUser.readObject(ois);
System.out.println("反序列化完成!id=" + oldUser.getId() + " name=" + oldUser.getName() + " password=" + oldUser.getPassword());
} catch (Exception e) {
e.printStackTrace();
}
}
2.3.3、实现了Externalnalizable接口
实现Externalnalizable接口的类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法;使用对应的方法进行序列化或反序列化。可以在两个方法中配置需要序列化的属性以及需要反序列化的属性。
User类:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private int id;
private String name;
private String password;
public User(int id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
/*
*省略get\set方法
*/
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(id);
out.writeObject(name);
out.writeObject(password);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
id = in.readInt();
name = (String)in.readObject();
//password = (String)in.readObject(); 反序列时不要密码
}
}
测试序列化和反序列化:
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("E:/user.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User user = new User(1, "Albert", "123456");
oos.writeObject(user);
oos.flush();
oos.close();
System.out.println("序列化完成!");
FileInputStream fis = new FileInputStream("E:/user.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
User oldUser = (User) ois.readObject();
System.out.println("反序列化完成!id=" + oldUser.getId() + " name=" + oldUser.getName() + " password=" + oldUser.getPassword());
} catch (Exception e) {
e.printStackTrace();
}
}
三、其他
3.1、序列化和反序列化的注意点
1、序列化时,只对对象的状态进行保存,而不管对象的方法;
2、当一个父类实现序列化接口时,子类将自动实现序列化,而不需要再次显式实现Serializable接口;
3、当一个对象的实例变量引用其他对象,序列化该对象时也会把引用对象进行序列化,而且被引用的类也不需要显式实现 Serializable接口,这是能用序列化解决深拷贝的重要原因;
4、声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。
5、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化 对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:
- 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
- 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
3.2、Serializable和Externalizable的区别
Externalizable是Serializable的子接口,
- Serializable不强制要求定义方法readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out)。
- Serializable序列化是将对象转换成相应的二进制,再进行传输或存储操作。
- Serializable反序列化时是以二进制为基础构造出某个状态的对象,不会涉及到对象的构造函数。
- Externalizable接口必须实现writeExternal(ObjectOutput out) 和void readExternal(ObjectInput in)两个方法,这两个方法分别在对象序列化与反序列化时自动调用。
- Externalizable在反序列化时会调用默认构造函数来创建对象,然后利用void readExternal(ObjectInput in)中的对应属性进行初始化。
3.3、SerialVersionUID
SerialVersionUID用于序列化机制中的版本验证,在将对象序列化来保存对象时,会将这个属性一并保存到二进制文件中。到需要反序列化时,JVM会读取字节流中的SerialVersionUID值与本地实体类的SerialVersionUID值进行比较,如果一致就可以进行反序列化,否则会抛出版本不一致的异常:InvalidCastException。
如果没有显式的定义该属性值,Java序列机制就会根据编译的Class自动生成一个SerialVersionUID值。(在对象没有发生改变时该值都是不变的)
生成方式:
- 默认的1L: private static final long serialVersionUID = 1L;
- 根据类名、接口名、成员组成等来生成一个64位的哈希值;
SerialVersionUID不建议随便改动,随便修改SerialVersionUID值可能会导致反序列化失败,在对象定义好后最好保持SerialVersionUID的值不变。
3.4、序列化与单例模式
通过序列化的实现过程得知,被序列化的对象肯定不是单例的;通过反序列获得的对象是一个新的对象,这也就是序列化会破坏对象的单例性。
那么该如何确保单例性不被破坏呢?
在单例对象类中定义readResolve方法,使用双重校验锁方式实现单例。
private volatile static User singleUser;
public static User getSingleUser(){
if(singleUser == null){
synchronized (User.class){
if (singleUser==null){
singleUser = new User();
}
}
}
return singleUser;
}
private Object readResolve(){
return singleUser;
}