Java对象的序列化
Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。
使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
简而言之,就是让对象想基本数据类型和字符串类型一样,通过输入输出字节流ObjectInputStream 和 ObjectOutputStream进行写和读操作。
Java序列化的应用场景
当你想把的内存中的对象状态保存到一个文件中或者数据库中时候
当你想用套接字在网络上传送对象的时候
当你想通过RMI传输对象的时候
如何对Java对象进行序列化与反序列化
在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。
import java.io.*;
import java.util.*;
class User implements Serializable {
private String name;
private int age;
private Date birthday;
private transient String gender;
private static int test =1;
private static final long serialVersionUID = -6849794470754667710L;
public User() {
System.out.println("none-arg constructor");
}
public User(String name, Integer age,Date birthday,String gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.birthday = birthday;
this.gender = gender;
}
public void setTest(int Newtest) {
this.test = Newtest;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", birthday=" + birthday +
", testStatic="+test+
'}'+" "+super.toString();
}
}
public class SerializableDemo {
public static void main(String[] args) throws Exception {
//Initializes The Object
User user = new User("qiuyu",23,new Date(),"male");
System.out.println(user);
user.setTest(10);
System.out.println(user);
//Write Obj to File
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempFile"));
out.writeObject(user);
out.close();
//Read Obj from File
ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
User newUser = (User) in.readObject();
System.out.println(newUser);
in.close();
}
}
此时的输出为:
arg constructor
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=10} User@326de728
User{name='qiuyu', age=23, gender=null, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=10} User@4883b407
此时必须注意的是,当重新读取被保存的User对象时,并没有调用User的任何构造器,看起来就像是直接使用字节将User对象还原出来的。
当User对象被保存到tempfile文件中之后,我们可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的CLASSPATH中包含有User.class,否则会抛出ClassNotFoundException。
Q:之前不是说序列化不保存静态变量么,为什么这里的静态变量进行了传递,都变成了10呢?
A:因为此时User.class已经被加载进了内存中,且将static变量test从1更改为了10。当我们恢复对象时,会直接获取当前static的变量test的值,所以为10。
Q:但如果我们的恢复操作在另一个文件中进行,结果会怎么样呢?
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class Recovering {
public static void main(String[] args) throws Exception{
//Read Obj from File
ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
User newUser = (User) in.readObject();
System.out.println(newUser);
in.close();
}
}
输出结果为:
User{name='qiuyu', age=23, gender=null, birthday=Tue Nov 14 20:38:57 GMT+08:00 2017, testStatic=1} User@6442b0a6
A:因为在运行此代码时,User.class会被加载进内存,然后执行初始化,将被初始化为1,因此test变量会被恢复为1。注意区分上面两种情况,不要因为是在本地进行测试,就认为static会被序列化,同时要了解Java运行时,内存加载的机制。
基本知识点
Serializable接口
对于任何需要被序列化的对象,都必须要实现接口Serializable,它只是一个标识接口,本身没有任何成员,只是用来标识说明当前的实现类的对象可以被序列化.
如果父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
如果被写对象的类型是String,数组,Enum,Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。
对象的读写
Java类中对象的序列化工作是通过 ObjectOutputStream 和 ObjectInputStream 来完成的。
使用readObject()、writeObject()方法对对象进行读写操作;
对于基本类型,可以使用readInt()、writeInt() , readDouble()、writeDouble()等类似的接口进行读写。
序列化机制
如果仅仅只是让某个类实现Serializable接口,而没有其它任何处理的话,则就是使用默认序列化机制。使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化。
在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程。为此需要为某个字段声明为transient,那么序列化机制就会忽略被transient修饰的字段。transient的引用变量会以null返回,基本数据类型会以相应的默认值返回。(例如:引用类型没有实现Serializable,或者动态数据只可以在执行时求出而不能或不必存储)
writeObject()与readObject()
对于上被声明为transient的字段gender,除了将transient关键字去掉之外,是否还有其它方法能使它再次可被序列化?方法之一就是在User类中添加两个方法:writeObject()与readObject(),如下所示:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeUTF(gender);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
gender = in.readUTF();
}
在writeObject()方法中会先调用ObjectOutputStream中的defaultWriteObject()方法,该方法会执行默认的序列化机制,此时会忽略掉gender字段。
然后再调用writeUtf()方法显示地将gender字段写入到ObjectOutputStream中。readObject()的作用则是针对对象的读取,其原理与writeObject()方法相同。
Q: writeObject()与readObject()都是private方法,那么它们是如何被调用的呢?
A: 毫无疑问,是使用反射。(注意这不是继承接口的方法,因为接口类的方法都是public的,而这里的方法是private的)
Externalizable接口
无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。JDK中提供了另一个序列化接口Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。
import java.io.*;
import java.util.*;
class UserExtern implements Externalizable {
private String name;
private int age;
private Date birthday;
private transient String gender;
private static int test =1;
private static final long serialVersionUID = -6849794470754667710L;
public UserExtern() {
System.out.println("none-arg constructor");
}
public UserExtern(String name, Integer age,Date birthday,String gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.birthday = birthday;
this.gender = gender;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeUTF(gender);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
gender = in.readUTF();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
out.writeObject(birthday);
out.writeUTF(gender);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
birthday = (Date) in.readObject();
gender = in.readUTF();
}
public void setTest(int Newtest) {
this.test = Newtest;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
", birthday=" + birthday +
", testStatic="+test+
'}'+" "+super.toString();
}
}
public class ExternalizableDemo {
public static void main(String[] args) throws Exception {
UserExtern userExtern = new UserExtern("qiuyu",23,new Date(),"male");
System.out.println(userExtern);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("external"));
out.writeObject(userExtern);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("external"));
UserExtern userExtern1 = (UserExtern)in.readObject();
System.out.println(userExtern1)
}
}
输出结果为:
arg constructor
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:34:25 GMT+08:00 2017, testStatic=1} UserExtern@25618e91
none-arg constructor
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:34:25 GMT+08:00 2017, testStatic=1} UserExtern@604ed9f0
Externalizable继承于Serializable,当使用该接口时,序列化的细节需要由程序员去完成writeExternal()与readExternal()方法的具体细节,以及哪些状态需要进行序列化。
另外,若使用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此序列化过程中UserExtern的无参构造器会被调用。由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。
注意事项
读取对象的顺序必须与写入的顺序相同
如果有不能被序列化的对象,执行期间就会抛出NotSerializableException异常
序列化时,只对对象的状态进行保存,而不管对象的方法
静态变量不会被序列化,因为所有的对象共享同一份静态变量的值
如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存还原,而且会是递归的方式(对象网)。(序列化程序会将对象版图上的所有东西储存下来,这样才能让该对象恢复到原来的状态)
如果子类实现Serializable接口而父类未实现时,父类不会被序列化,但此时父类必须有个无参构造方法,否则会抛InvalidClassException异常;因为反序列化时会恢复原有子对象的状态,而父类的成员变量也是原有子对象的一部分。由于父类没有实现序列化接口,即使没有显示调用,也会默认执行父类的无参构造函数使变量初始化;
深入理解
序列化ID的问题
serialVersionUID适用于JAVA的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。
在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。
序列化存储规则
Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用;
序列化到同一个文件时,如第二次修改了相同对象属性值再次保存时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象,第二次进行的修改将无效。
public static void main(String[] args) throws Exception {
//Initializes The Object
User user = new User("qiuyu",23,new Date(),"male");
user.setTest(10);
System.out.println(user);
//Write Obj to File
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempFile"));
out.writeObject(user);
user.setAge(25);
System.out.println(user);
out.writeObject(user);
out.close();
//Read Obj from File
ObjectInputStream in = new ObjectInputStream(new FileInputStream("tempFile"));
User newUser = (User) in.readObject();
User newUser1 = (User) in.readObject();
System.out.println(newUser);
System.out.println(newUser1);
in.close();
}
输出结果: 注意观察age的值
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@326de728
User{name='qiuyu', age=25, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@326de728
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@4883b407
User{name='qiuyu', age=23, gender=male, birthday=Tue Nov 14 22:47:22 GMT+08:00 2017, testStatic=10} User@4883b407
多次序列化的问题
在一次的序列化的过程中,ObjectOutputStream 会在文件开始的地方写入一个 Header的信息到文件中。于是在多次序列化的过程中就会继续在文件末尾(本次序列化的开头)写入 Header 的信息,这时如果进行反序列化的对象的时候会报错:java.io.StreamCorruptedException: invalid type code: AC