Java之对象序列化

素颜马尾好姑娘i 2023-06-08 04:34 105阅读 0赞

文章目录

      • 一、简介
      • 二、对象流实现序列化
          1. 成员变量是基本类型
        • 2.成员变量是引用类型
          1. 序列化多个对象
      • 三、自定义序列化(一)
          1. transient关键字
          1. 目标类提供writeObject、readObject方法
          1. 目标类提供writeReplace方法
          1. 目标类提供readResolve()方法
      • 四、自定义序列化(二)
          • 实现Externalizable接口
      • 五、小结
      • 六、序列化版本

一、简介

对象序列化的目的是将对象保存到磁盘, 或者允许在网络中传输。相对的, 反序列化就是根据磁盘中保存的文件恢复对象。

要让某个类的对象能够序列化, 需要实现下面2个接口之一:
-Serializable
-Externalizable

二、对象流实现序列化

通过实现Serializable接口让一个类的对象可序列化的步骤十分简单, 仅仅是在声明一个类时声明实现该接口即可, 无需实现任何方法

1. 成员变量是基本类型

  1. // 用于测试的对象
  2. class Animal implements Serializable {
  3. private String name;
  4. private double weight;
  5. public Animal(String name, double weight) {
  6. this.name = name;
  7. this.weight = weight;
  8. }
  9. @Override
  10. public String toString() {
  11. return "Animal{" +
  12. "name='" + name + '\'' +
  13. ", weight=" + weight +
  14. '}';
  15. }
  16. }

将一个对象输出到磁盘, 可使用对象流 java.io.ObjectOutputStream

  1. String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
  2. try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
  3. Animal cat = new Animal("cat", 10.2);
  4. out.writeObject(cat);
  5. }

将一个对象从磁盘恢复可使用对象流 java.io.ObjectInputStream

  1. String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
  2. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
  3. Animal cat = (Animal)ois.readObject();
  4. System.out.println(cat.toString());
  5. }
  6. // 输出:
  7. Animal{name='cat', weight=10.2}

实现Serializable接口的对象反序列化通过构造器初始化对象?
当从输入流读取对象时, 并没有看到程序调用构造器, 因此反序列化并不需要通过构造器来初始化对象。

  1. 代码:略.

2.成员变量是引用类型

若一个类的成员变量是引用类型, 那么该成员变量也必须是可序列化的

  1. // 用于测试的类1
  2. class Animal implements Serializable {
  3. private String name;
  4. private double weight;
  5. private Friend friend;
  6. public Animal(String name, double weight, Friend friend) {
  7. this.name = name;
  8. this.weight = weight;
  9. this.friend = friend;
  10. }
  11. @Override
  12. public String toString() {
  13. return "Animal{" +
  14. "name='" + name + '\'' +
  15. ", weight=" + weight +
  16. ", friend=" + friend +
  17. '}';
  18. }
  19. }
  20. // 用于测试的类2
  21. class Friend implements Serializable{
  22. private String name;
  23. public Friend(String name) {
  24. this.name = name;
  25. }
  26. @Override
  27. public String toString() {
  28. return "Friend{" +
  29. "name='" + name + '\'' +
  30. '}';
  31. }
  32. }
  33. String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
  34. try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
  35. // 创建对象
  36. Friend friend = new Friend("Ber");
  37. Animal cat = new Animal("cat", 10.2, friend);
  38. // 将对象写入输出流
  39. out.writeObject(cat);
  40. }

从输入流中恢复对象的代码没有变化

  1. // 输出结果
  2. Animal{name='cat', weight=10.2, friend=Friend{name='Ber'}}

3. 序列化多个对象

当保存多个对象并且恢复时, 读取的顺序与保存的顺序需要一致, 而且可以看到, 用于读取对象的方法readObject()并没有参数。

  1. // 多个对象序列化
  2. String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
  3. try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
  4. // 创建对象
  5. Friend friend = new Friend("Ber");
  6. Animal cat = new Animal("cat", 10.2, friend);
  7. // 将对象1写入输出流
  8. out.writeObject(cat);
  9. // 将对象2写入输出流
  10. out.writeObject(friend);
  11. }
  12. // 反序列化
  13. String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
  14. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
  15. // 从输入流获取对象1
  16. Animal cat = (Animal)ois.readObject();
  17. // 从输入流获取对象2
  18. Friend friend = (Friend) ois.readObject();
  19. System.out.println(cat.toString());
  20. System.out.println(friend.toString());
  21. }
  22. // 输出:
  23. Animal{name='cat', weight=10.2, friend=Friend{name='Ber'}}
  24. Friend{name='Ber'}

当一个对象被多次序列化时, 是否会输出多个对象?

当多次对同一个对象序列化时, 只有首次的序列化将表示对象的字节序列写到输出流, 后面在进行序列化时, 被写入输出流的只是序列化编号, 从而保证是从字节序列恢复的是同一个对象。

  1. // 序列化代码片段
  2. try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
  3. // 创建对象
  4. Friend friend = new Friend("Ber");
  5. // 将对象写入输出流
  6. out.writeObject(friend);
  7. // 将对象再次写入输出流
  8. out.writeObject(friend);
  9. }
  10. // 反序列化代码片段
  11. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
  12. // 从输入流获取对象1
  13. Friend friend = (Friend) ois.readObject();
  14. // 从输入流获取对象2
  15. Friend friend2 = (Friend) ois.readObject();
  16. System.out.println(friend == friend2);
  17. }
  18. // 输出:
  19. true

当一个对象被列化后, 修改该对象属性并再次序列化, 那么反序列化的结果是什么?
反序列化的结果都是首次序列化的属性值, 而这也符合了多次序列化对象的规则: 只有对象的首次序列化才会将表示对象的字节流写入输出流, 后面的调用只会写入序列化编号。

  1. // 对象序列化片段
  2. // 创建对象
  3. Friend friend = new Friend("Ber");
  4. // 将对象写入输出流
  5. out.writeObject(friend);
  6. //修改对象属性值并写入输出流
  7. friend.setName("Hos");
  8. out.writeObject(friend);
  9. // 对象反序列化片段
  10. // 从输入流获取对象
  11. Friend friend = (Friend) ois.readObject();
  12. Friend friend2 = (Friend) ois.readObject();
  13. System.out.println(friend.toString());
  14. System.out.println(friend2.toString());
  15. // 输出:
  16. Friend{name='Ber'}
  17. Friend{name='Ber'}

三、自定义序列化(一)

1. transient关键字

在默认情况下, 将对象序列化时会将其所有的实例变量进行实例化。 若成员变量是引用类型, 则引用的对象也会被实例化。 假设想要忽略对某个对实例变量的序列化, 可使用transient关键字。

  1. // 用于测试的类
  2. class Friend implements Serializable{
  3. private String name;
  4. private transient int age;
  5. public Friend(String name, int age) {
  6. this.name = name;
  7. this.age = age;
  8. }
  9. @Override
  10. public String toString() {
  11. return "Friend{" +
  12. "name='" + name + '\'' +
  13. ", age=" + age +
  14. '}';
  15. }
  16. }
  17. // 对象序列化片段
  18. try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
  19. // 创建对象
  20. Friend friend = new Friend("Ber", 20);
  21. // 将对象写入输出流
  22. out.writeObject(friend);
  23. }
  24. / / 反序列化
  25. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
  26. // 从输入流获取对象
  27. Friend friend = (Friend) ois.readObject();
  28. System.out.println(friend.toString());
  29. }
  30. 输出:
  31. Friend{name='Ber', age=0}

可以看到, 被transient修饰的成员变量age在反序列化后并不是序列化时的值。

2. 目标类提供writeObject、readObject方法






















method desc
private void readObject(ObjectInputStream in) 从输入流中读取对象
private void writeObject(ObjectOutputStream out) 将对象写到输出流
private void readObjectNoData() 当序列化流不完整, 正确的初始化反序列化对象
  1. class Friend implements Serializable{
  2. private String name;
  3. private transient int age;
  4. public Friend(String name, int age) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. private void writeObject(ObjectOutputStream out) throws IOException {
  9. out.writeObject(new StringBuffer(name).reverse());
  10. out.writeInt(age);
  11. }
  12. private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
  13. this.name = ((StringBuffer)in.readObject()).reverse().toString();
  14. this.age = in.readInt();
  15. }
  16. }

序列化和反序列化的代码与3.1相同, 没有变化。

首先, 通过提供writeObject方法, 我们可以决定序列化哪些变量, 以及如何序列化变量。
其次, 提高了在通过网络传输字节化序列时的安全性。

注意:
(1). readObject恢复实例变量的顺序必须与writeObject存储实例变量一致。
(2). readObject恢复实例变量的方式必须与writeObject存储实例变量的方式相对应. 在上例中:
writeObject序列化String类型变量: String —> StringBuffer —> StringBuffer.reverse
readObject反序列化String类型变量: (StringBuffer)Object —> StringBuffer.reverse —> toString

3. 目标类提供writeReplace方法

使用该方法时, 会将原本将要序列化的对象改为对另一个对象的序列化。

Java序列化保护机制保证: 在序列化对象之前, 先调用该对象的writeReplace()方法, 若该方法返回的是另一个Java对象, 则转为序列化该对象。

  1. // 用于测试的类
  2. class Friend implements Serializable {
  3. private String name;
  4. private transient int age;
  5. public Friend(String name, int age) {
  6. this.name = name;
  7. this.age = age;
  8. }
  9. private Object writeReplace() {
  10. ArrayList<Object> list = new ArrayList<>();
  11. list.add(name);
  12. list.add(age);
  13. return list;
  14. }
  15. }
  16. // 序列化对象代码
  17. try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
  18. // 创建对象
  19. Friend friend = new Friend("Ber", 50);
  20. // 将对象写入输出流
  21. out.writeObject(friend);
  22. }
  23. // 反序列化代码
  24. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
  25. // 从输入流获取对象
  26. ArrayList list = (ArrayList)ois.readObject();
  27. System.out.println(list);
  28. }
  29. // 输出:
  30. [Ber, 50]

可以看到, 在将对象序列化代码中无需改变, 但是在反序列化时需要将对象转为与writeReplace方法中定义的类型(ArrayList)一致。因此可以知道, 对原对象的序列化实际上转为了对ArrayList对象的序列化。

4. 目标类提供readResolve()方法

writeReplace方法是在序列化时替换序列化的对象, 那么与之相对的, readResolve()方法的返回值会替换原本反序列化的对象, 因为readResolve()会在调用readObject()时被调用。
在这里插入图片描述

  1. // 用于测试的bean
  2. class Friend implements Serializable {
  3. private String name;
  4. private transient int age;
  5. public Friend(String name, int age) {
  6. this.name = name;
  7. this.age = age;
  8. }
  9. // 提供readResolve方法
  10. private Object readResolve() {
  11. return String.valueOf("I am a String");
  12. }
  13. @Override
  14. public String toString() {
  15. return "Friend{" +
  16. "name='" + name + '\'' +
  17. ", age=" + age +
  18. '}';
  19. }
  20. }
  21. // 序列化对象
  22. try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
  23. // 创建对象
  24. Friend friend = new Friend("Ber", 50);
  25. // 将对象写入输出流
  26. out.writeObject(friend);
  27. }
  28. // 反序列化对象
  29. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
  30. // 从输入流获取对象
  31. Object object = ois.readObject();
  32. System.out.println(object.toString());
  33. }
  34. // 输出:
  35. I am a String

根据例子可知, Friend对象反序列化后返回的对象是readResolve()中定义的String对象。

注意:
与writeReplace()方法相似, readResolve()方法可以使用任意的访问控制符。 假如readResolve()不使用 private 或final 修饰, 那么就会有被子类继承的风险, 也就意味着假如子类继承了这个方法且没有重写, 那么对子类反序列化时将会得到一个父类对象。 但总是让子类重写方法也是一个负担, 若是需要重写, 则最好使用private或final修饰。

四、自定义序列化(二)

实现Externalizable接口

与实现Serializable接口不同的是, 实现Externalizable接口需要做2件事:
(1) 实现 writeExternal(ObjectOutput out) 和 readExternal(ObjectInput in) 方法。
(2) 为该类提供无参数构造器, 无参数构造器在反序列化时被使用到。

除此之外, 序列化和反序列化的代码都是调用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject()方法完成。

  1. class Person implements Externalizable {
  2. private String name;
  3. private int age;
  4. public Person() {
  5. System.out.println("无参构造器调用");
  6. }
  7. public Person(String name, int age) {
  8. this.name = name;
  9. this.age = age;
  10. }
  11. // 实现接口方法
  12. public void writeExternal(ObjectOutput out) throws IOException {
  13. out.writeObject(name);
  14. out.writeInt(age);
  15. }
  16. // 实现接口方法
  17. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  18. this.name = (String) in.readObject();
  19. this.age = in.readInt();
  20. }
  21. @Override
  22. public String toString() {
  23. return "Person{" +
  24. "name='" + name + '\'' +
  25. ", age=" + age +
  26. '}';
  27. }
  28. }
  29. // 序列化代码
  30. try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
  31. // 创建对象
  32. Person person = new Person("Ber", 50);
  33. // 将对象写入输出流
  34. out.writeObject(person);
  35. }
  36. // 反序列化代码
  37. String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
  38. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
  39. // 从输入流获取对象
  40. Person person = (Person) ois.readObject();
  41. System.out.println(person.toString());
  42. }
  43. 输出:
  44. 无参构造器调用
  45. Person{name='Ber', age=50}

Serializable和Externalizable接口不同之处:






















Serializable Externalizable
自动存储对象信息 自定义存储对象的信息
易于实现, 只需声明实现该接口即可 需要实现接口的方法、提供无参构造器
性能略差 性能略好

Externalizable接口虽然能带来性能上的提升, 但是也增加了编程的复杂度, 因此大多数情况下使用实现Serializable的方法。

五、小结

  1. 对象的类名成员变量都会被序列化, 方法、静态变量、transient修饰的成员变量不会被序列化。
  2. 实现Serializable接口的类可以使用transient修饰成员变量, 让该成员变量不会被序列化, 虽然使用static修饰成员变量也能达到这个效果, 但是不推荐这样使用。
  3. 要进行序列化的对象的成员变量也必须是可序列化的, 假如该成员变量是不可序列化的, 需要使用transient修饰, 否则该类对象时不可序列化的。
  4. 反序列化必须要有序列化对象的class文件
  5. 当通过文件、网络来进行反序列化时, 必须按实际写入的顺序读取。

六、序列化版本

当进行对象反序列化时需要提供对象的class文件, 那么当class文件的版本升级时, 如何保证两个class文件的兼容性?
通过在序列化类显式的提供一个private static final long serialVersionUID值。 这个值用于标识java类的序列化版本, 也就是说, 即使一个类升级后, 只要它的serialVersionUID不变, 序列化机制也会把它们当成同一版本。

该值获取方法:
(1) 这个值可以自己定
(2) 使用bin目录下的serialver.exe工具来获取该值. 命令的格式为serialver 类名:
在这里插入图片描述
(3)通过该开发工具生成
IDEA如何自动生成 serialVersionUID 的设置

假如不显式的提供serialVersionUID会发生什么情况?
(1) 该值由JVM根据类的信息进行计算, 一个类修改后计算出的值往往会发生变化, 造成反序列化因为类版本不兼容而失败。
(2) 不利于在不同的JVM进行移植, 不同的JVM可能会有不同的计算策略, 因此一个类虽然没有改变, 但是会因为序列化版本不兼容造成无法反序列化的现象。

假如一个类的修改导致了反序列化失败, 则应该重新分配serialVersionUID的值。

发表评论

表情:
评论列表 (有 0 条评论,105人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java序列对象

    java序列化 Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型

    相关 Java对象序列

    [Java对象序列化][Java]   当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个

    相关 Java 对象序列

    引言 将 [ Java][Java] 对象序列化为二进制文件的 Java 序列化技术是 Java 系列技术中一个较为重要的技术点,在大部分情况下,开发人员只需要了解被序列

    相关 Java对象序列

    Java对象的序列化 Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的

    相关 java对象序列

    概念: Java 提供了一种对象序列化的机制,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件