Java序列化
一、目的
序列化机制允许将实现序列化的Java对象转化为字节序列,这些字节序列可以进行持久化存储或通过网络传输至其他网络节点,同时还可以通过反序列化以恢复其原有状态,这样就使得对象脱离程序独立存在。
二、常见用途
- (持久化存储)例如一个用于记录用户信息的类,当用户退出程序后再次运行需要保留上一次的信息,这时通过将上次的用户信息序列化后存储至磁盘在用户再次运行时就可以通过反序列化进行提取。
- (网络传输)将bean信息序列化后通过网络传输至其他网络节点。
- (持久化存储)服务器钝化,服务器将目前长时间不用的对象序列化后存储在磁盘中,当需要时先在内存中找若没有则可通过反序列化磁盘中的对象,这样可节省服务器资源。
三、Java序列化实现
在Java中如果某个类需要实现序列化,则需要实现Serializable接口或者Externalizable接口之一即可,其中Serializable只是个标识接口,不用实现任何方法它只是告诉JVM该类可序列化。而Externalizable则需要实现writeExternal和readExternal方法用于自定义序列化规则。
3.1 序列化
创建一个ObjectOutputStream流后调用ObjectOutputStream对象的writeObject输出序列化对象,执行下列代码后会生成一个object.txt文件,而这个文件就是SerializableDemo序列化后的字节序列。
/* 个人信息类 */
public class Personal implements Serializable {
private String name;
private String sex;
private int age;
public Personal(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return this.name + ", " + this.sex + ", " + this.age;
}
}
/* 序列化 */
public class SerializableDemoTest {
@Test
public void testOutput() throws Exception{
Personal demo = new Personal("Mike", "men", 25);
// FileOutputStream流是指文件字节输出流,用于输出原始字节流如图像数据等,其继承OutputStream类
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.txt"));
objectOutputStream.writeObject(demo);
}
}
3.2 反序列化
创建一个ObjectInputStream流后调用ObjectInputStream对象的readObject读取对象的字节序列以恢复其原有状态。
@Test
public void testInput() throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("object.txt"));
Personal person = (Personal)objectInputStream.readObject();
System.out.println(person.toString());
}
序列化时注意点:
- 如果可序列化的成员不是基本数据类型且也不是String类型(实现了Serializable接口),则需要这个引用类型也可序列化(实现Serializable或Externalizable接口之一),否则会导致此类也不可序列化。
- 反序列化顺序要和序列化顺序保持一致。
四、序列化注意点
4.1 序列化算法
- 所有保存到磁盘的对象都有一个序列化编码。
- 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
- 如果此对象已经序列化过,则直接输出编号即可。
- 根据上面的特性可以得知,同一个对象多次进行序列化不会真正的多次序列化。
public class SerializableDemoTest {
@Test
public void testOutput() throws Exception{
Personal demo = new Personal("Mike", "men", 25);
Student student1 = new Student(demo, 1);
Student student2 = new Student(demo, 2);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.txt"));
objectOutputStream.writeObject(student1);
objectOutputStream.writeObject(student2);
}
@Test
public void testInput() throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("object.txt"));
Student student1 = (Student)objectInputStream.readObject();
Student student2 = (Student)objectInputStream.readObject();
System.out.println(student1 == student2);
System.out.println(student1.getPersonal() == student2.getPersonal());
}
}
输出如下结果:
- 同时也存在当对象已经序列化过但内容发生了更改后再次序列化并不会真正的序列化而是输出其编号。
public class SerializableDemoTest {
@Test
public void test() throws Exception{
Personal demo = new Personal("Mike", "men", 25);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.txt"));
objectOutputStream.writeObject(demo);
System.out.println(demo.toString());
demo.setName("Tom");
objectOutputStream.writeObject(demo);
System.out.println(demo.toString());
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("object.txt"));
Personal personal1 = (Personal)objectInputStream.readObject();
Personal personal2 = (Personal)objectInputStream.readObject();
System.out.println(personal1.getName());
System.out.println(personal2.getName());
}
输出如下结果:
4.1 序列化版本号
java序列化提供了一个 serialVersionUID
的序列化版本号来验证版本是否一致,在进行反序列化时通过读取字节流中的serialVersionUID
和本地类中的serialVersionUID
进行比较,若相同则可进行反序列化若不同则会报错。同时若版本号相同即使更改了序列化属性,对象也可以被反序列化回来。
场景描述:一个类在A端序列化后传递至B端进行反序列化。
1. 当版本号一致,A端新增字段
反序列化成功,但A端新增的字段将会被忽略。
2. 当版本号一致,A端减少字段
反序列化成功,但B端相对于A端多出的字段将会被赋予默认值,其中引用默认值:null, 基本类型默认值:0, boolean默认值:false
。
3. 当版本号一致,B端新增字段
反序列化成功,但B端新增的字段将会被赋予默认值。
4. 当版本号一致,B端减少字段
反序列化成功,B端减少的字段将会被忽略。
5. 当版本号不一致,其他相同
反序列化失败。
6. 版本号一致,A端和B端实例变量类型不同
反序列化失败,这时候需要更改serialVersionUID。
4.1 其他注意点
- 当服务器客户端进行序列化和反序列化时要注意双方序列化类除类名相同外包名也需相同否则会报错。
- 序列化并不保存静态变量。
五、自定义序列化
5.1 简单的自定义序列化
有时候一些属性我们不想序列化,这时我们可以通过transient
关键字来指定不需要序列化的字段。使用transient
关键字指定的属性在进行序列化时将会被忽略,反序列化时这些被transient
指定的字段会赋予初始值。
public class Personal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
transient private String sex;
private int age;
public Personal(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
}
// 序列化赋值:Personal demo = new Personal("Mike", "men", 25);
// 反序列输出:Mike, null, 25
5.2 更灵活的自定义序列化
除使用transient
关键字来自定义需要序列化的字段外,还可以通过重写writeObject
与readObject
方法,选择需要序列化的字段,除此之外还可以在writeObject
和readObject
中自定义序列化和反序列化的规则。
public class Personal implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String sex;
private int age;
public Personal(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
private void writeObject(ObjectOutputStream out) throws IOException {
// 将名字反转写入二进制流,还可使用其他处理方法,例如加密处理等
out.writeObject(new StringBuffer(this.name).reverse());
out.writeInt(age);
out.writeObject(new StringBuffer(this.sex).reverse());
}
private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException{
// 此处未对name字段进行反转解密
this.name = ((StringBuffer)in.readObject()).toString();
this.age = in.readInt();
// 将读出的字符串反转或解密恢复回来
this.sex = ((StringBuffer)in.readObject()).reverse().toString();
}
@Override
public String toString() {
return this.name + ", " + this.sex + ", " + this.age;
}
}
// 序列化赋值:Personal demo = new Personal("Mike", "men", 25);
// 反序列输出:ekiM, men, 25
5.3 进一步的自定义序列化
序列化时在调用writeObject
方法之前会先调用writeReplace
方法,此方法可将任意对象代替目标序列化对象;同样在调用readObject
方法之前也会调用readRePlace
方法,此方法可以替代反序列化出的对象(反序列化出对象将会被丢弃)。
5.4 强制的自定义序列化
实现Externalizable
接口必须需实现writeExternal
、readExternal
方法以强制自定义序列化。
public class Personal implements Externalizable {
private static final long serialVersionUID = 1L;
private String name;
private String sex;
private int age;
public Personal() {}
public Personal(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 将名字反转写入二进制流,还可使用其他处理方法,例如加密处理等
out.writeObject(new StringBuffer(this.name).reverse());
out.writeInt(age);
out.writeObject(new StringBuffer(this.sex).reverse());
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = ((StringBuffer)in.readObject()).toString();
this.age = in.readInt();
// 将读出的字符串反转或解密恢复回来
this.sex = ((StringBuffer)in.readObject()).reverse().toString();
}
@Override
public String toString() {
return this.name + ", " + this.sex + ", " + this.age;
}
}
// 序列化赋值:Personal demo = new Personal("Mike", "men", 25);
// 反序列输出:ekiM, men, 25
注意:实现Externalizable接口除必须实现writeExternal()、readExternal()还需提供无参的构造函数,因为在反序列化时需要通过反射来创建相应对象(没有无参构造函数反序列时会报
java.io.InvalidClassException --- no valid constructor
异常)。
5.1 其他 -Transient和父类的序列化
- 特性介绍:
-
当一个子类实现了序列化而父类没有实现序列化,子类在序列化时父类不会跟着序列化。
-
Java对象是先有父类再有子类,因此在反序列化时会调用父类的无参构造函数。
-
父类的无参构造函数如果没有对参数进行初始化那么会被赋初始值。
特性使用:因此我们除了使用transient
关键字来指定我们不需要序列化的字段外,还可以利用此特性将我们不需要序列化的字段放在父类中,用子类去实现序列化接口进行序列化(父类不实现序列化接口)。
end>>>