java序列化机制详解

java序列化机制详解

java序列化是将java对象保存在文件或者通过网络传输的机制,通过实现接口Serializable或者Externalizable标识该类的对象可以序列化和反序列化。

如果希望保存java对象的状态并在以后的某个时刻在内存中重建该对象,我们可以通过java序列化的机制实现。

Serializable关键字

标识类对象是可序列化的只要实现Serializable接口即可,该接口不包括任何字段和方法,它只是一个空的接口。如以下代码片断,Student类实现了该接口并自动获得了序列化的能力。

//实现了Serializable接口的类自动拥有序列化的能力
public class Student implements Serializable {
}

对象的状态是由成员变量决定的,所以序列化保存的是成员变量的值。static修饰的是类变量,序列化的时候static变量不会保存,另外,我们还可以通过java提供的transient关键字来显示标识变量不需要序列化。因此java序列化的是除了static和transient修饰的其它成员变量。

ObjectOutputSteram和ObjectInputStream

如何使用java的序列化机制呢?

java提供了ObjectOutputStream和ObjectInputStream两个对象输入输出流来实现序列化。假设Student类需要序列化,类的定义如下:

import java.io.Serializable;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午7:30 */
public class Student implements Serializable { private Integer age; private String name; public Student(Integer age, String name) { this.age = age; this.name = name; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } }

下面代码是将Student对象序列化的例子

该例中将对象序列化到了文件student.out中,通过ObjectOutputStream将对象jack写到文件输出流中。接着通过ObjectInputStream将jack对象反序列化并强制转换为Student类。(注意此时需要捕获ClassNotFoundException类,假如此时JVM找不到Student Class对象,将会抛出该异常。)

import java.io.*;

/**
 * 

文件描述: 对象序列化Demo

* * @Author luanmousheng * @Date 17/7/1 下午7:34 */
public class SerializableDemo { public static void main(String[] args) { try { /** * 对象输出流,具体输出流是文件输出 */ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out")); Student jack = new Student(23, "jack"); /** * 通过ObjectOutputStream将对象写入到文件输出流 */ oos.writeObject(jack); oos.close(); /** * 对象输入流,具体输入是文件输入流 */ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out")); /** * 通过ObjectInputStream将对象从文件输入流读入 */ Student stu = (Student) ois.readObject(); ois.close(); System.out.println(stu); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

输出:

Student{age=23, name='jack'}

若Student类未实现Serializable接口,将会抛出运行时异常 java.io.NotSerializableException。

transient关键字

若将Student的age字段设置为transient的,反序化后age字段将是空值:

import java.io.Serializable;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午7:40 */
public class Student implements Serializable{ //transient修饰的字段默认不会被序列化 private transient Integer age; private String name; public Student(Integer age, String name) { this.age = age; this.name = name; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } }

Demo运行后的输出为:

Student{age=null, name='jack'}

可以看到输出后的age为null,transient修饰的字段将不会被序列化。

static修饰的成员变量

前面我们说过java序列化包括的是除了static和transient修饰的其它成员变量。假设将Student的name字段设置为static,看下Demo输出结果:

import java.io.Serializable;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午7:55 */
public class Student implements Serializable{ private Integer age; private static String name = "student"; public Student(Integer age, String name) { this.age = age; this.name = name; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } }

Demo输出结果为:

Student{age=23, name='jack'}

看到反序列化后name的值是”jack”而不是”student”,这是为何?

这正是因为不会序列化static修饰的变量而导致的结果。序列化后的文件其实name字段值为空,反序列化后去方法区查找静态变量name的值,这个name值正是之前Demo类通过Student的构造函数将name值设置为”jack”的值,所以最终反序列化后的name值为”jack”。

也就是说这个”name”值不是通过反序列化得到的,而是通过去方法区拿到的全局的值。

如果读者对上例有疑惑,可以继续看下面的例子:

import java.io.*;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午20:30 */
public class Student implements Serializable{ private Integer age; private static String name = "student"; public Student(Integer age, String name) { this.age = age; this.name = name; } //增加了设置静态变量的方法 public static void setName(String name) { Student.name = name; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } }
import java.io.*;

/**
 * 

文件描述:

* * @Author luanmousheng * @Date 17/7/1 下午20:52 */
public class SerializableDemo { public static void main(String[] args) { try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out")); Student jack = new Student(23, "jack"); oos.writeObject(jack); oos.close(); //反序列化前将静态变量设置为"tom" Student.setName("tom"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out")); Student stu = (Student) ois.readObject(); ois.close(); System.out.println(stu); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

Demo输出结果为:

Student{age=23, name='tom'}

输出结果的name值为”tom”,而不是写入时的”jack”。因为在反序列化之前已经将静态变量name的值设置为”tom”,反序列化后会去取这个静态变量的值。:smile:

writeObject()和readObject()方法

如果没有为类添加这两个方法,序列化和反序列化是通过ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法进行的。

如果为类添加writeObject()和readObject()方法,将不会再调用ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法,我们可以更加精确的操作对象的序列化。

例如如果将Student类的age字段设置为transient了,此时我们又想去序列化该字段,那么我们通过这两个方法来实现:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午21:12 */
public class Student implements Serializable{ private transient Integer age; private String name; public Student(Integer age, String name) { this.age = age; this.name = name; } private void writeObject(ObjectOutputStream oos) throws IOException { //默认的序列化 oos.defaultWriteObject(); oos.writeInt(age); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { //默认的反序列化 ois.defaultReadObject(); this.age = ois.readInt(); } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } }

此时Demo的输出结果将会输出age的值:

Student{age=23, name='jack'}

注意,这两个方法都是private的。方法中首先调用了序列化的默认方法,之后才是自定义的逻辑。还要注意的是序列化的顺序和反序列化的顺序必须一致。否则会读出未知的结果。例如将Student的age和name都设置为transient,在writeObject和readObject方法中调整写入和读出的顺序:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午21:29 */
public class Student implements Serializable{ private transient Integer age; private transient String name; public Student(Integer age, String name) { this.age = age; this.name = name; } private void writeObject(ObjectOutputStream oos) throws IOException { //默认的序列化 oos.defaultWriteObject(); oos.writeInt(age); oos.writeUTF(name); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { //默认的序列化 ois.defaultReadObject(); //此处读入的顺序和写入的顺序不一致 this.name = ois.readUTF(); this.age = ois.readInt(); } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } }

Demo输出结果让人莫名其妙:

Student{age=1507332, name=''}

此处如果将oos.writeUTF方法改为oos.writeObject、ois.readUTF方法改为ois.readObject,会抛出异常 OptionalDataException

Externalizable关键字

实现了该接口的类基于之前Serializable的序列化机制就会失效,Externalizable接口已经实现了Serializable。看下面的例子:

import java.io.*;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午21:53 */
public class Student implements Externalizable{ private Integer age; private String name; public Student(Integer age, String name) { this.age = age; this.name = name; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } @Override public void writeExternal(ObjectOutput out) throws IOException { } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { } }

将Student改为上面的代码,运行Demo类的输出结果为:

java.io.InvalidClassException: com.lms.serializable.Student; no valid constructor

异常抛出,提示没有可用的构造器,其实就是没有默认的构造函数。实现了Externalizable接口的类对象,反序列化的时候会先通过无参的构造函数new一个对象,然后将各个字段的值赋值给该对象,Student没有默认的构造函数,所以上例抛出异常。

为Student添加默认构造函数后的Demo输出结果为:

Student{age=null, name='null'}

可以看出,所有字段为空,这是因为实现的Externalizable接口的对象序列化需要开发者自己去保证,也就是需要实现writeExternal和readExternal方法,实现了writeExternal和readExternal方法后的Student类如下:

import java.io.*;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午22:14 */
public class Student implements Externalizable{ private Integer age; private String name; public Student() { } public Student(Integer age, String name) { this.age = age; this.name = name; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(age); out.writeObject(name); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { age = in.readInt(); name = (String) in.readObject(); } }

Demo输出结果为:

Student{age=23, name='jack'}

可以看出正确输出了结果。

单例的序列化

如果一个类是单例,反序列化后是同一个对象吗?我们将Student改为单例模式的(Student用单例模式确实有点牵强),看下例:

import java.io.*;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午22:40 */
public class Student implements Serializable{ private Integer age; private String name; private static class Instance { private static Student jack = new Student(23, "jack"); } private Student(Integer age, String name) { this.age = age; this.name = name; } public static Student getInstance() { return Instance.jack; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } }

Demo类修改为:

import java.io.*;

/**
 * 

文件描述:Demo类

* * @Author luanmousheng * @Date 17/7/1 下午23:00 */
public class SerializableDemo { public static void main(String[] args) { try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out")); //取单例 Student jack = Student.getInstance(); oos.writeObject(jack); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out")); Student stu = (Student) ois.readObject(); ois.close(); System.out.println(stu); //看反序列化后是否是同一个对象 System.out.println(jack == stu); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

Demo输出结果为:

Student{age=23, name='jack'}
false

可以看出,反序列化后不是同一个对象。那要怎么解决这个问题呢?可以在单例类Student中加个私有方法readResolve()方法。

import java.io.*;

/**
 * 

文件描述: 需要序列化的类需要实现Serializable接口

* * @Author luanmousheng * @Date 17/7/1 下午23:20 */
public class Student implements Serializable{ private Integer age; private String name; private static class Instance { private static Student jack = new Student(23, "jack"); } private Student(Integer age, String name) { this.age = age; this.name = name; } public static Student getInstance() { return Instance.jack; } //增加readResolve方法,直接返回单例 private Object readResolve() { return Instance.jack; } @Override public String toString() { return "Student{" + "age=" + age + ", name='" + name + '\'' + '}'; } }

Demo输出结果为:

Student{age=23, name='jack'}
true

可以看到,反序列化后的对象和序列化之前的对象是同一个对象。

无论是实现Serializable或者Externalizable接口的对象,反序列化的时候都会调用readResolve方法,因此这里的readResolve方法直接返回了单例,而不是返回反序列化后的对象,保证了单例的逻辑。

序列化ID

实现Serializable接口的类需要生成对应的序列化id(serialVersionUID)。如:

    private static final long serialVersionUID = -5881678404557689785L;

如果没有显示为类添加该字段(IDE可以设置成自动生成),java将会根据字节码文件动态生成序列化ID。每次重新编译生成的序列化ID都不一样。所以前面的Student例子都不是完美的,没有生成序列化ID。

如果显示为类添加该字段,那么不管编译多少次,只要没有修改该字段的值,该字段就不会变。

为何需要序列化ID?

如果一个类X在多个客户端使用,其中一个客户端A收到来自客户端B的类X对象x。客户端A通过网络收到对象x的二进制序列化文件后会进行反序列化操作。首先客户端A会取出对象x的序列化ID,并将该ID和客户端A本地类X Class对象的序列化ID进行比较,如果不相等那就会提示序列化失败,如果相等那就可以正常进行序列化。本人所在的公司现在使用的RPC框架、阿里开源的dubbo所使用的网络传输对象就是实现了Serializable接口。

父类和子类实现序列化的问题

  • 父类实现了序列化接口,子类自动拥有序列化功能,不需要显示的实现序列化接口
  • 父类没有实现序列化接口,子类实现了序列化接口。序列化子类对象时不会去序列化父类的对象。因为必须有父对象才有子对象,反序列化时会调用父类的无参默认构造函数,因此父类必须要有无参默认构造函数,反序列化后父类对象字段值都是其类型默认值(基本类型是0值,引用类型是null)

序列化多次相同对象的问题

如果序列化两个相同的对象,java做了特殊处理,看下例:

import java.io.*;

/**
 * 

文件描述: Demo类

* * @Author luanmousheng * @Date 17/7/1 下午23:40 */
public class SerializableDemo { public static void main(String[] args) { try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out")); Student jack = new Student(23, "jack"); //第一次序列化对象 oos.writeObject(jack); oos.flush(); //第一次序列化对象后的文件大小 System.out.println(new File("student.out").length()); //第二次序列化同一个对象 oos.writeObject(jack); //第二次序列化对象后的文件大小 System.out.println(new File("student.out").length()); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out")); //第一次反序列化对象 Student stu1 = (Student) ois.readObject(); //第二次反序列化对象 Student stu2 = (Student) ois.readObject(); ois.close(); //比较是否同一个对象 System.out.println(stu1 == stu2); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

Demo输出结果为:

189
194
true

可以看到两次反序列化后的对象是同一个对象。

第一次序列化后的文件大小为189,第二次序列化后的文件大小为194,只增加了5字节,这显示是java做了优化导致的。两个相同对象当然不需要冗余存储,这增加的5字节是一些引用信息,这些引用指向同一个对象,反序列化后重建引用关系。

另外需要注意的是,如果在第二次序列化同一个对象之前,改变了这个对象某些字段的值,因为虚拟机发现之前已经将该对象序列化过了,这时只会存储写的引用,不会实际去写,所以第二次序列化之前对这些字段的改变实际上是没有效果的。

例如:

import java.io.*;

/**
 * 

文件描述:Demo类

* * @Author luanmousheng * @Date 17/7/1 下午23:54 */
public class SerializableDemo { public static void main(String[] args) { try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out")); Student jack = new Student(23, "jack"); //第一次序列化对象 oos.writeObject(jack); oos.flush(); //第一次序列化对象后的文件大小 System.out.println(new File("student.out").length()); //第二次序列化之前将name值改为"lucy" jack.setName("lucy"); //第二次序列化同一个对象 oos.writeObject(jack); //第二次序列化对象后的文件大小 System.out.println(new File("student.out").length()); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out")); //第一次反序列化对象 Student stu1 = (Student) ois.readObject(); //第二次反序列化对象 Student stu2 = (Student) ois.readObject(); ois.close(); //比较是否同一个对象 System.out.println(stu1); System.out.println(stu2); System.out.println(stu1 == stu2); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }

输出结果为:

189
194
Student{age=23, name='jack'}
Student{age=23, name='jack'}
true

可以发现输出的两个对象值完全相同,第二次反序列化之前将值改为”lucy”是无效的。

你可能感兴趣的:(java)