1 什么是序列化

序列化是一种对象持久化的手段。普遍应用在网络传输的场景中。对于 Java 而言,序列化主要用于对对象进行持久化操作。一般情况下,Java 对象的生命周期短于 JVM 的生命周期,随着 JVM 停止运行,Java 对象也随之消亡。但是,为了能在 JVM 停止运行后对指定对象进行持久化,并在将来的某个时刻重新读取被保存的对象,或跨 JVM 传输对象,持久化能实现上述需求。
Java 在持久化对象时,会将对象的状态保存为一组字节。在反序列化时,将字节组装成对象。
值得注意的是,序列化保存的的是对象的状态,即它的成员变量,而不会保存类的静态变量和被transient修饰的变量。

2 Java序列化的相关特性

  • 只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。
  • 通过 ObjectOutputStream 和 ObjectInputStream 实现对象的序列化及反序列化。
  • 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。
  • 序列化并不保存静态变量。
  • 要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。
  • transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
  • 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

    3 Java序列化的方式

  • 实现空接口 Serializable。

    • 这种方式隐式实现序列化,是最简单的序列化方式,
    • 默认情况下,会自动序列化所有非statictransient修饰的成员变量。
  • 实现 Externalizable 接口。
    • Externalizable 继承自Serializable,一旦实现了Externalizable 接口,那么基于Serializable的机制将失效。
    • 这种方式必须实现 writeExternal() 和 readExternal() 方法,这两个方法是自动调用的。
      • writeExternal()方法内可以自己选择哪些部分进行序列化。
      • writeExternal()方法内可以自定义选择序列化被transient修饰的成员变量。
  • 实现 Serializable 接口并添加 writeObject() 和 readObject() 方法。
    • 手动添加writeObject() 和 readObject() 方法,注意不是重写或覆盖。
      • 这两个方法必须被 private 修饰。
      • 第一行应该默认调用 defaultReadObject() 与 defaultWriteObject() 方法,目的是隐式序列化非static非transient变量。
      • 最后调用 readObject() 和 writeObject() 进行显示序列化被transient修饰的成员变量。
      • writeObject()方法内可以自定义选择序列化被transient修饰的成员变量。

具体代码如下:

  1. package org.example;
  2. import java.io.*;
  3. // Serializable接口实现序列化
  4. public class SerializeTest {
  5. public static void main(String[] args) {
  6. // 序列化
  7. try (ObjectOutputStream oos = new ObjectOutputStream(
  8. new FileOutputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")));
  9. ) {
  10. Student s1 = new Student("zhangsan", 1);
  11. oos.writeObject(s1);
  12. Student s2 = new Student("lisi", 2);
  13. oos.writeObject(s2);
  14. } catch (FileNotFoundException e) {
  15. e.printStackTrace();
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }
  19. // 反序列化
  20. try (ObjectInputStream ois = new ObjectInputStream(
  21. new FileInputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")))
  22. ) {
  23. Student o = (Student) ois.readObject();
  24. System.out.println(o);
  25. Student o2 = (Student) ois.readObject();
  26. System.out.println(o2);
  27. } catch (FileNotFoundException e) {
  28. e.printStackTrace();
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. } catch (ClassNotFoundException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. static class Student implements Serializable {
  36. private String name;
  37. private transient int id;
  38. // 省略了全参、空参构造方法、get、set方法、toString方法
  39. }
  40. }
  1. package org.example;
  2. import java.io.*;
  3. // Externalizable接口 实现自定义序列化和反序列化
  4. public class SerializeTest2 {
  5. public static void main(String[] args) {
  6. // 序列化
  7. try (ObjectOutputStream oos = new ObjectOutputStream(
  8. new FileOutputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")));
  9. ) {
  10. Student s1 = new Student("zhangsan", 1, new School("清华", "北京"), 1);
  11. oos.writeObject(s1);
  12. Student s2 = new Student("lisi", 2,new School("北大", "北京"), 2);
  13. oos.writeObject(s2);
  14. } catch (FileNotFoundException e) {
  15. e.printStackTrace();
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }
  19. // 反序列化
  20. try (ObjectInputStream ois = new ObjectInputStream(
  21. new FileInputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")))
  22. ) {
  23. Student o = (Student) ois.readObject();
  24. System.out.println(o);
  25. Student o2 = (Student) ois.readObject();
  26. System.out.println(o2);
  27. } catch (FileNotFoundException e) {
  28. e.printStackTrace();
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. } catch (ClassNotFoundException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. // 实现Externalizable接口
  36. static class Student implements Externalizable {
  37. private String name;
  38. private transient Integer age;
  39. private School school;
  40. private int id;
  41. // 省略了全参、空参构造方法、get、set方法、toString方法
  42. @Override
  43. public void writeExternal(ObjectOutput out) throws IOException {
  44. // 根据变量的类型不同,对需要进行序列化的变量进行writeXXX操作
  45. // 这里没有对id变量进行序列化,其反序列化的结果为类型的默认值
  46. out.writeObject(name);
  47. out.writeInt(age);
  48. out.writeObject(school);
  49. // out.writeInt(id);
  50. }
  51. @Override
  52. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  53. // 获取反序列化后的结果,注意对类型进行转换
  54. name = (String) in.readObject();
  55. age = in.readInt();
  56. school = (School) in.readObject();
  57. // id = in.readInt();
  58. }
  59. }
  60. private static class School implements Externalizable {
  61. private String name;
  62. private String address;
  63. // 省略了全参、空参构造方法、get、set方法、toString方法
  64. @Override
  65. public void writeExternal(ObjectOutput out) throws IOException {
  66. out.writeObject(name);
  67. out.writeObject(address);
  68. }
  69. @Override
  70. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  71. name = (String) in.readObject();
  72. address = (String) in.readObject();
  73. }
  74. }
  75. }
  1. package org.example;
  2. import java.io.*;
  3. // Serializable 接口并添加 writeObject() 和 readObject() 方法
  4. public class SerializeTest3 {
  5. public static void main(String[] args) {
  6. // 序列化
  7. try (ObjectOutputStream oos = new ObjectOutputStream(
  8. new FileOutputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")));
  9. ) {
  10. Student s1 = new Student("zhangsan", 1);
  11. oos.writeObject(s1);
  12. Student s2 = new Student("lisi", 2);
  13. oos.writeObject(s2);
  14. } catch (FileNotFoundException e) {
  15. e.printStackTrace();
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }
  19. // 反序列化
  20. try (ObjectInputStream ois = new ObjectInputStream(
  21. new FileInputStream(new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\Serialize.txt")))
  22. ) {
  23. Student o = (Student) ois.readObject();
  24. System.out.println(o);
  25. Student o2 = (Student) ois.readObject();
  26. System.out.println(o2);
  27. } catch (FileNotFoundException e) {
  28. e.printStackTrace();
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. } catch (ClassNotFoundException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. static class Student implements Serializable {
  36. private String name;
  37. private transient int id;
  38. // 省略了全参、空参构造方法、get、set方法、toString方法
  39. private void writeObject(ObjectOutputStream oos) throws IOException {
  40. oos.defaultWriteObject();
  41. // 显示序列化被transient修饰的变量
  42. oos.writeInt(id);
  43. }
  44. private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
  45. ois.defaultReadObject();
  46. id = ois.readInt();
  47. }
  48. }
  49. }

4 ArrayList中的序列化

首先,ArrayList 中实现了 Serializable 接口,意味着这个集合类可以进行序列化操作。
在ArrayList 底层,保存具体集合元素的 Object[] 类型的数组变量 elementData 被 transient 修饰,但是集合元素经过反序列化后依旧可以复原。原因如下:ArrayList 提供了两个方法:writeObject 和 readObject。

  • 在writeObject方法中:
    • 首先调用ObjectOutputStream 的 defaultWriteObject 方法,写出非transient非static的属性;以及ArrayList的size属性。
    • 然后再把 Object[] 中下标 [0, size-1] 的数据逐一写入。注意这时候长度是数组的实际长度(也就是前面写出的size),而不是ArrayList的数组容量。
  • 在readObject 方法中:
    • 首先调用ObjectInputStream 的 defaultReadObject 方法,读取非transient非static的属性;以及ArrayList的size属性。
    • 其次先读出数组长度,如果数组长度不够则调用ensureCapacityInternal 方法扩容。
    • 最后逐一读出。

      为什么要用transient修饰elementData

      既然要序列化ArrayList,为什么又要用transient修饰elementData?
      ArrayList 由于其动态性,数组容量总是大于等于数组实际数量,当序列化时可能会将大量空值序列化为 null ,造成不必要的浪费,因此用transient修饰elementData,然后在writeObjec进行手动序列化,只序列化实际存储的元素,而不是整个数组。

      5 能序列化的前提

      如果一个类想被序列化,需要实现 Serializable 接口进行自动序列化,或者实现 Externalizable 接口进行手动序列化,否则强行序列化该类的对象,就会抛出 NotSerializableException 异常,这是因为,在序列化操作过程中会对类型进行检查(代码如下),要求被序列化的类必须属于 Enum、Array 和 Serializable 类型其中的任何一种(Externalizable也继承了Serializable)。
      JVM 是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)。后面进行具体的介绍。
      transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
      FileOutputStream 类有一个带有两个参数的重载构造方法——FileOutputStream(String, boolean)。若其第二个参数为 true 且 String 代表的文件存在,那么将把新的内容写到原来文件的末尾而非重写这个文件,故不能用这个版本的构造函数来实现序列化,也就是说必须重写这个文件,否则在读取这个文件反序列化的过程中就会抛出异常,导致只有第一次写到这个文件中的对象可以被反序列化,之后程序就会出错。
      1. if (obj instanceof String) {
      2. writeString((String) obj, unshared);
      3. } else if (cl.isArray()) {
      4. writeArray(obj, desc, unshared);
      5. } else if (obj instanceof Enum) {
      6. writeEnum((Enum<?>) obj, desc, unshared);
      7. } else if (obj instanceof Serializable) {
      8. writeOrdinaryObject(obj, desc, unshared);
      9. } else {
      10. if (extendedDebugInfo) {
      11. throw new NotSerializableException(
      12. cl.getName() + "\n" + debugInfoStack.toString());
      13. } else {
      14. throw new NotSerializableException(cl.getName());
      15. }
      16. }
      17. }

6 Java序列化中的serialVersionUID

serialVersionUID 用于 Java 的序列化机制。简单来说,Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。
在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException 。
序列化操作的时候系统会把当前类的 serialVersionUID 写入到序列化文件中,当反序列化时系统会去检测文件中的 serialVersionUID ,判断它是否与当前类的 serialVersionUID 一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。serialVersionUID 有两种生成机制:

  • 采用默认的private static final long serialVersionUID = 1L。
  • 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
    private static final long serialVersionUID = xxxxL;

当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作序列化版本比较用。这种情况下,如果 Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化的。

7 Java序列化中父子类相关问题

  1. 如果父类已经实现了序列化,其所有子类都自动实现序列化。
  2. 事实上,很多时候父类并没有实现序列化,这个时候其子类要想实现序列化,除了自身实现 Serializable 以外,还必须满足两个条件:
    • 父类有无参的构造方法。
      • 在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。
      • 当我们在反序列后输出父对象的变量值时,它的值是调用父类无参构造函数后的值。
    • 子类要负责父类中成员变量的序列化和反序列化。
      • 还记得前面提到的 writeObjectreadObject方法吗,就是利用这两个方法手动序列化父类的成员变量。
      • 需要注意的是,序列化前父类中定义的变量的值,和序列化后的值是不一样的。
  1. package org.example.exception;
  2. import java.io.*;
  3. // 父类实现了序列化,其所有子类自动实现了序列化
  4. public class ExceptionDemo {
  5. public static void main(String[] args) throws Exception {
  6. File file = new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\exception\\test.txt");
  7. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
  8. Student s1 = new Student("zhangsan", 1);
  9. Student s2 = new Student2("lisi", 2);
  10. oos.writeObject(s1);
  11. oos.writeObject(s2);
  12. System.out.println(s1);
  13. System.out.println(s2);
  14. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
  15. Student s3 = (Student) ois.readObject();
  16. Student2 s4 = (Student2) ois.readObject();
  17. System.out.println(s3);
  18. System.out.println(s4);
  19. }
  20. static class People implements Serializable {
  21. }
  22. // 子类,注意没有实现Serializable接口
  23. static class Student extends People {
  24. public String name;
  25. public int id;
  26. public Student(String name, int id) {
  27. this.name = name;
  28. this.id = id;
  29. }
  30. @Override
  31. public String toString() {
  32. return "Student{" +
  33. "name='" + name + '\'' +
  34. ", id=" + id +
  35. '}';
  36. }
  37. }
  38. // 孙类,注意没有实现Serializable接口
  39. static class Student2 extends Student {
  40. public Student2(String name, int id) {
  41. super(name, id);
  42. }
  43. @Override
  44. public String toString() {
  45. return "Student2{" +
  46. "name='" + name + '\'' +
  47. ", id=" + id +
  48. '}';
  49. }
  50. }
  51. }
  1. package org.example.exception;
  2. import java.io.*;
  3. // 父类没有实现序列化,子类实现序列化
  4. public class ExceptionDemo2 {
  5. public static void main(String[] args) throws Exception {
  6. File file = new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\exception\\test2.txt");
  7. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
  8. Student s1 = new Student("male","zhangsan", 1);
  9. oos.writeObject(s1);
  10. System.out.println(s1);
  11. System.out.println("--------------------");
  12. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
  13. Student s3 = (Student) ois.readObject();
  14. System.out.println(s3);
  15. }
  16. static class People {
  17. public String gender;
  18. public People(String gender) {
  19. this.gender = gender;
  20. }
  21. // 父类必须有空参构造方法
  22. // 反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象
  23. public People() {
  24. System.out.println("父类的无参构造方法");
  25. }
  26. @Override
  27. public String toString() {
  28. return "People{" +
  29. "gender='" + gender + '\'' +
  30. '}';
  31. }
  32. }
  33. // 子类
  34. static class Student extends People implements Serializable {
  35. public String name;
  36. public int id;
  37. public Student(String gender, String name, int id) {
  38. super(gender);
  39. this.name = name;
  40. this.id = id;
  41. }
  42. private void writeObject(ObjectOutputStream out) throws IOException {
  43. // 先序列化子类对象
  44. out.defaultWriteObject();
  45. // 再手动序列化父类的成员变量
  46. out.writeObject(gender);
  47. }
  48. private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
  49. // 先反序列化子类对象
  50. in.defaultReadObject();
  51. // 再手动反序列化父类的成员变量
  52. String gender = (String) in.readObject();
  53. }
  54. @Override
  55. public String toString() {
  56. return "Student{" +
  57. "gender='" + gender + '\'' +
  58. ", name='" + name + '\'' +
  59. ", id=" + id +
  60. '}';
  61. }
  62. }
  63. }

输出结果:

  1. Student{gender='male', name='zhangsan', id=1}
  2. --------------------
  3. 父类的无参构造方法
  4. Student{gender='null', name='zhangsan', id=1}

父类没有实现序列化,子类也没有实现序列化,孙类实现了序列化。

  1. package org.example.exception;
  2. import java.io.*;
  3. // 父类没有实现序列化,子类也没有实现序列化,孙类实现了序列化
  4. public class ExceptionDemo3 {
  5. public static void main(String[] args) throws Exception {
  6. File file = new File("G:\\javaCode\\demo_code\\demo\\src\\org\\example\\exception\\test3.txt");
  7. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
  8. Student s2 = new Student2("male","lisi", 2, "清华");
  9. oos.writeObject(s2);
  10. System.out.println(s2);
  11. System.out.println("--------------------");
  12. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
  13. Student2 s3 = (Student2) ois.readObject();
  14. System.out.println(s3);
  15. }
  16. static class People {
  17. public String gender;
  18. public People(String gender) {
  19. this.gender = gender;
  20. }
  21. // 父类必须有空参构造方法
  22. // 反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象
  23. public People() {
  24. System.out.println("父类的父类的无参构造方法");
  25. }
  26. @Override
  27. public String toString() {
  28. return "People{" +
  29. "gender='" + gender + '\'' +
  30. '}';
  31. }
  32. }
  33. // 子类
  34. static class Student extends People {
  35. public String name;
  36. public int id;
  37. public Student() {
  38. System.out.println("父类的无参构造方法");
  39. }
  40. public Student(String gender, String name, int id) {
  41. super(gender);
  42. this.name = name;
  43. this.id = id;
  44. }
  45. @Override
  46. public String toString() {
  47. return "Student{" +
  48. "gender='" + gender + '\'' +
  49. ", name='" + name + '\'' +
  50. ", id=" + id +
  51. '}';
  52. }
  53. }
  54. // 孙类
  55. static class Student2 extends Student implements Serializable {
  56. String school;
  57. public Student2() {
  58. }
  59. public Student2(String gender, String name, int id, String school) {
  60. super(gender, name, id);
  61. this.school = school;
  62. }
  63. private void writeObject(ObjectOutputStream out) throws IOException {
  64. out.defaultWriteObject();
  65. out.writeObject(gender);
  66. out.writeObject(name);
  67. out.writeInt(id);
  68. }
  69. private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
  70. in.defaultReadObject();
  71. String gender = (String) in.readObject();
  72. String name = (String) in.readObject();
  73. id = in.readInt();
  74. }
  75. @Override
  76. public String toString() {
  77. return "Student2{" +
  78. "gender='" + gender + '\'' +
  79. ", name='" + name + '\'' +
  80. ", id=" + id +
  81. ", school='" + school + '\'' +
  82. '}';
  83. }
  84. }
  85. }

输出结果:

  1. Student2{gender='male', name='lisi', id=2, school='清华'}
  2. --------------------
  3. 父类的父类的无参构造方法
  4. 父类的无参构造方法
  5. Student2{gender='null', name='null', id=2, school='清华'}

参考