乐趣区

关于序列化:序列化和反序列化

一、序列化的含意、意义及应用场景

  • 序列化:将对象写入到 IO 流中
  • 反序列化:从 IO 流中复原对象
  • 意义:序列化机制容许将实现序列化的 Java 对象转换位字节序列,这些字节序列能够保留在磁盘上,或通过网络传输,以达到当前复原成原来的对象。序列化机制使得对象能够脱离程序的运行而独立存在。
  • 应用场景:所有可在网络上传输的对象都必须是可序列化的,比方 RMI(remote method invoke, 即近程办法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有须要保留到磁盘的 java 对象都必须是可序列化的。通常倡议:程序创立的每个 JavaBean 类都实现 Serializeable 接口。

二、序列化实现的形式

果须要将某个对象保留到磁盘上或者通过网络传输,那么这个类应该实现 Serializable 接口或者 Externalizable 接口之一。

1、Serializable

1.1 一般序列化

Serializable 接口是一个标记接口,不必实现任何办法。一旦实现了此接口,该类的对象就是可序列化的。

  1. 序列化步骤:
  • 步骤一:创立一个 ObjectOutputStream 输入流;
  • 步骤二:调用 ObjectOutputStream 对象的 writeObject 输入可序列化对象。

    public class Person implements Serializable {
      private String name;
      private int age;
      // 我不提供无参结构器
      public Person(String name, int age) {
          this.name = name;
          this.age = age;
      }
      @Override
      public String toString() {
          return "Person{" +
                  "name='" + name + '''+", age="+ age +'}';
      }
    }
    public class WriteObject {public static void main(String[] args) {
          try (// 创立一个 ObjectOutputStream 输入流
               ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
              // 将对象序列化到文件 s
              Person person = new Person("9 龙", 23);
              oos.writeObject(person);
          } catch (Exception e) {e.printStackTrace();
          }
      }
    }
    复制代码
  1. 反序列化步骤:
  • 步骤一:创立一个 ObjectInputStream 输出流;
  • 步骤二:调用 ObjectInputStream 对象的 readObject()失去序列化的对象。

    咱们将下面序列化到 person.txt 的 person 对象反序列化回来

    public class Person implements Serializable {
      private String name;
      private int age;
      // 我不提供无参结构器
      public Person(String name, int age) {System.out.println("反序列化,你调用我了吗?");
          this.name = name;
          this.age = age;
      }
      @Override
      public String toString() {
          return "Person{" +
                  "name='" + name + '''+", age="+ age +'}';
      }
    }
    public class ReadObject {public static void main(String[] args) {
          try (// 创立一个 ObjectInputStream 输出流
               ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"))) {Person brady = (Person) ois.readObject();
              System.out.println(brady);
          } catch (Exception e) {e.printStackTrace();
          }
      }
    }
    // 输入后果
    //Person{name='9 龙', age=23}
    复制代码

    waht???? 输入通知咱们,反序列化并不会调用构造方法。反序列的对象是由 JVM 本人生成的对象,不通过构造方法生成。

1.2 成员是援用的序列化

如果一个可序列化的类的成员不是根本类型,也不是 String 类型,那这个援用类型也必须是可序列化的;否则,会导致此类不能序列化。

看例子,咱们新增一个 Teacher 类。将 Person 去掉实现 Serializable 接口代码。

public class Person{// 省略相干属性与办法}
public class Teacher implements Serializable {
    private String name;
    private Person person;
    public Teacher(String name, Person person) {
        this.name = name;
        this.person = person;
    }
     public static void main(String[] args) throws Exception {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {Person person = new Person("路飞", 20);
            Teacher teacher = new Teacher("雷利", person);
            oos.writeObject(teacher);
        }
    }
}
复制代码

咱们看到程序间接报错,因为 Person 类的对象是不可序列化的,这导致了 Teacher 的对象不可序列化

1.3 同一对象序列化屡次的机制

同一对象序列化屡次,会将这个对象序列化屡次吗?答案是 否定 的。

public class WriteTeacher {public static void main(String[] args) throws Exception {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {Person person = new Person("路飞", 20);
            Teacher t1 = new Teacher("雷利", person);
            Teacher t2 = new Teacher("红发香克斯", person);
            // 顺次将 4 个对象写入输出流
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(person);
            oos.writeObject(t2);
        }
    }
}
复制代码

顺次将 t1、t2、person、t2 对象序列化到文件 teacher.txt 文件中。

留神:反序列化的程序与序列化时的程序统一

public class ReadTeacher {public static void main(String[] args) {try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacher.txt"))) {Teacher t1 = (Teacher) ois.readObject();
            Teacher t2 = (Teacher) ois.readObject();
            Person p = (Person) ois.readObject();
            Teacher t3 = (Teacher) ois.readObject();
            System.out.println(t1 == t2);
            System.out.println(t1.getPerson() == p);
            System.out.println(t2.getPerson() == p);
            System.out.println(t2 == t3);
            System.out.println(t1.getPerson() == t2.getPerson());
        } catch (Exception e) {e.printStackTrace();
        }
    }
}
// 输入后果
//false
//true
//true
//true
//true
复制代码

从输入后果能够看出,Java 序列化同一对象,并不会将此对象序列化屡次失去多个对象。

  • Java 序列化算法
  1. 所有保留到磁盘的对象都有一个序列化编码号
  2. 当程序试图序列化一个对象时,会先查看此对象是否曾经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输入。
  3. 如果此对象曾经序列化过,则间接输入编号即可。

    图示上述序列化过程。

1.4 java 序列化算法潜在的问题

因为 java 序利化算法不会反复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保留序列化编号。

public class WriteObject {public static void main(String[] args) {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
             ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {
            // 第一次序列化 person
            Person person = new Person("9 龙", 23);
            oos.writeObject(person);
            System.out.println(person);
            // 批改 name
            person.setName("海贼王");
            System.out.println(person);
            // 第二次序列化 person
            oos.writeObject(person);
            // 顺次反序列化出 p1、p2
            Person p1 = (Person) ios.readObject();
            Person p2 = (Person) ios.readObject();
            System.out.println(p1 == p2);
            System.out.println(p1.getName().equals(p2.getName()));
        } catch (Exception e) {e.printStackTrace();
        }
    }
}
// 输入后果
//Person{name='9 龙', age=23}
//Person{name='海贼王', age=23}
//true
//true
复制代码

1.5 可选的自定义序列化

  1. 有些时候,咱们有这样的需要,某些属性不须要序列化。应用 transient 关键字抉择不须要序列化的字段。

    public class Person implements Serializable {
       // 不须要序列化名字与年龄
       private transient String name;
       private transient int age;
       private int height;
       private transient boolean singlehood;
       public Person(String name, int age) {
           this.name = name;
           this.age = age;
       }
       // 省略 get,set 办法
    }
    public class TransientTest {public static void main(String[] args) throws Exception {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {Person person = new Person("9 龙", 23);
               person.setHeight(185);
               System.out.println(person);
               oos.writeObject(person);
               Person p1 = (Person)ios.readObject();
               System.out.println(p1);
           }
       }
    }
    // 输入后果
    //Person{name='9 龙', age=23', singlehood=true', height=185cm}
    //Person{name='null', age=0', singlehood=false', height=185cm}
    复制代码

    从输入咱们看到,应用 transient 润饰的属性,java 序列化时,会疏忽掉此字段,所以反序列化出的对象,被 transient 润饰的属性是默认值。对于援用类型,值是 null;根本类型,值是 0;boolean 类型,值是 false。

  2. 应用 transient 尽管简略,但将此属性齐全隔离在了序列化之外。java 提供了 可选的自定义序列化。能够进行管制序列化的形式,或者对序列化数据进行编码加密等。

    private void writeObject(java.io.ObjectOutputStream out) throws IOException;private void readObject(java.io.ObjectIutputStream in) throws IOException,ClassNotFoundException;
    private void readObjectNoData() throws ObjectStreamException;
    复制代码

    通过重写 writeObject 与 readObject 办法,能够本人抉择哪些属性须要序列化,哪些属性不须要。如果 writeObject 应用某种规定序列化,则相应的 readObject 须要相同的规定反序列化,以便能正确反序列化出对象。这里展现对名字进行反转加密。

    public class Person implements Serializable {
       private String name;
       private int age;
       // 省略构造方法,get 及 set 办法
       private void writeObject(ObjectOutputStream out) throws IOException {
           // 将名字反转写入二进制流
           out.writeObject(new StringBuffer(this.name).reverse());
           out.writeInt(age);
       }
       private void readObject(ObjectInputStream ins) throws IOException,ClassNotFoundException{
           // 将读出的字符串反转复原回来
           this.name = ((StringBuffer)ins.readObject()).reverse().toString();
           this.age = ins.readInt();}
    }
    复制代码

    当序列化流不残缺时,readObjectNoData()办法能够用来正确地初始化反序列化的对象。例如,应用不同类接管反序列化对象,或者序列化流被篡改时,零碎都会调用 readObjectNoData()办法来初始化反序列化的对象。

  3. 更彻底的自定义序列化

    ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
    ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

    • writeReplace:在序列化时,会先调用此办法,再调用 writeObject 办法。此办法可将任意对象代替指标序列化对象

      public class Person implements Serializable {
        private String name;
        private int age;
        // 省略构造方法,get 及 set 办法
        private Object writeReplace() throws ObjectStreamException {ArrayList<Object> list = new ArrayList<>(2);
            list.add(this.name);
            list.add(this.age);
            return list;
        }
         public static void main(String[] args) throws Exception {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                 ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {Person person = new Person("9 龙", 23);
                oos.writeObject(person);
                ArrayList list = (ArrayList)ios.readObject();
                System.out.println(list);
            }
        }
      }
      // 输入后果
      //[9 龙, 23]
      复制代码
    • readResolve:反序列化时替换反序列化出的对象,反序列化进去的对象被立刻抛弃。此办法在 readeObject 后调用。

      public class Person implements Serializable {
          private String name;
          private int age;
          // 省略构造方法,get 及 set 办法
           private Object readResolve() throws ObjectStreamException{return new ("brady", 23);
          }
          public static void main(String[] args) throws Exception {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
                   ObjectInputStream ios = new ObjectInputStream(new FileInputStream("person.txt"))) {Person person = new Person("9 龙", 23);
                  oos.writeObject(person);
                  HashMap map = (HashMap)ios.readObject();
                  System.out.println(map);
              }
          }
      }
      // 输入后果
      //{brady=23}
      复制代码

      readResolve 罕用来反序列单例类,保障单例类的唯一性。

      留神:readResolve 与 writeReplace 的拜访修饰符能够是 private、protected、public,如果父类重写了这两个办法,子类都须要依据本身需要重写,这显然不是一个好的设计。通常倡议对于 final 润饰的类重写 readResolve 办法没有问题;否则,重写 readResolve 应用 private 润饰。

2、Externalizable:强制自定义序列化

通过实现 Externalizable 接口,必须实现 writeExternal、readExternal 办法。

public interface Externalizable extends java.io.Serializable {void writeExternal(ObjectOutput out) throws IOException;
     void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
复制代码
public class ExPerson implements Externalizable {
    private String name;
    private int age;
    // 留神,必须加上 pulic 无参结构器
    public ExPerson() {}
    public ExPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // 将 name 反转后写入二进制流
        StringBuffer reverse = new StringBuffer(name).reverse();
        System.out.println(reverse.toString());
        out.writeObject(reverse);
        out.writeInt(age);
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 将读取的字符串反转后赋值给 name 实例变量
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        System.out.println(name);
        this.age = in.readInt();}
    public static void main(String[] args) throws IOException, ClassNotFoundException {try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"))) {oos.writeObject(new ExPerson("brady", 23));
            ExPerson ep = (ExPerson) ois.readObject();
            System.out.println(ep);
        }
    }
}
// 输入后果
//ydarb
//brady
//ExPerson{name='brady', age=23}
复制代码

留神:Externalizable 接口不同于 Serializable 接口,实现此接口必须实现接口中的两个办法实现自定义序列化,这是强制性的;特别之处是必须提供 pulic 的无参结构器,因为在反序列化的时候须要反射创建对象。

3、两种序列化比照

实现 Serializable 接口

实现 Externalizable 接口

零碎主动存储必要的信息

程序员决定存储哪些信息

Java 内建反对,易于实现,只须要实现该接口即可,无需任何代码反对

必须实现接口内的两个办法

性能略差

性能略好

尽管 Externalizable 接口带来了肯定的性能晋升,但变成复杂度也进步了,所以个别通过实现 Serializable 接口进行序列化。

三、序列化版本号 serialVersionUID

咱们晓得,反序列化必须领有 class 文件,但随着我的项目的降级,class 文件也会降级,序列化怎么保障降级前后的兼容性呢?

java 序列化提供了一个 private static final long serialVersionUID 的序列化版本号,只有版本号雷同,即便更改了序列化属性,对象也能够正确被反序列化回来。

public class Person implements Serializable {
    // 序列化版本号
    private static final long serialVersionUID = 1111013L;
    private String name;
    private int age;
    // 省略构造方法及 get,set
}
复制代码

如果反序列化应用的 class 的版本号 与序列化时应用的 不统一 ,反序列化会 报 InvalidClassException 异样。

序列化版本号可自在指定,如果不指定,JVM 会依据类信息本人计算一个版本号,这样随着 class 的降级,就无奈正确反序列化;不指定版本号另一个显著隐患是,不利于 jvm 间的移植,可能 class 文件没有更改,但不同 jvm 可能计算的规定不一样,这样也会导致无奈反序列化。

什么状况下须要批改 serialVersionUID 呢?分三种状况。

  • 如果只是批改了办法,反序列化不容影响,则无需批改版本号;
  • 如果只是批改了动态变量,瞬态变量(transient 润饰的变量),反序列化不受影响,无需批改版本号;
  • 如果批改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不统一,则会反序列化失败,这时候须要更改 serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果缩小了实例变量,反序列化时会疏忽掉缩小的实例变量。

四、总结

  1. 所有须要网络传输的对象都须要实现序列化接口,通过倡议所有的 javaBean 都实现 Serializable 接口。
  2. 对象的类名、实例变量(包含根本类型,数组,对其余对象的援用)都会被序列化;办法、类变量、transient 实例变量都不会被序列化。
  3. 如果想让某个变量不被序列化,应用 transient 润饰。
  4. 序列化对象的援用类型成员变量,也必须是可序列化的,否则,会报错。
  5. 反序列化时必须有序列化对象的 class 文件。
  6. 当通过文件、网络来读取序列化后的对象时,必须依照理论写入的程序读取。
  7. 单例类序列化,须要重写 readResolve()办法;否则会毁坏单例准则。
  8. 同一对象序列化屡次,只有第一次序列化为二进制流,当前都只是保留序列化编号,不会反复序列化。
  9. 倡议所有可序列化的类加上 serialVersionUID 版本号,不便我的项目降级。
作者:9 龙
链接:https://juejin.im/post/6844903848167866375
起源:掘金
退出移动版