序列化serialVersionUID

722 阅读7分钟

序列化

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化,关于对象创建有很多方法,创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。

对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换

java 中,序列化常用于RMI(远端方法调用)及网络传输中 相关类:

java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream

Serializable

虽然Serializable接口中并没有定义任何属性和方法,但是如果一个类想要具备序列化能力也比必须要实现它。其实,主要是因为序列化在真正的执行过程中会使用instanceof判断一个类是否实现类Serializable,如果未实现则直接抛出异常。

一般都是通过实现Serializable方法来实现序列化 如果一个类的父类,需要将父类的变量持久化起来,父类也需要几次Serializable接口

public class User{
  private String userName;
  private int userAge;
  get set......
  
  @overwirte
  public String toString(){
    return "User{"+
          "name'"+name+'\''+
          ",age="+age+
          '}';
  }
}

main实现方法

public class SeriableDemo{
  public static void main(String[] args){
    User user = new User();
    user.setname("fourous");
    user.setage("12");
    user.toString();
    
        //Write Obj to File
       try (FileOutputStream fos = new FileOutputStream("tempFile"); ObjectOutputStream oos = new ObjectOutputStream(
           fos)) {
           oos.writeObject(user);
       } catch (IOException e) {
           e.printStackTrace();
       }

       //Read Obj from File
       File file = new File("tempFile");
       try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
           User1 newUser = (User1)ois.readObject();
           System.out.println(newUser);
       } catch (IOException | ClassNotFoundException e) {
           e.printStackTrace();
       }
   }
  }
}

Externalizable

Externalizable和Serializable接口的区别就是Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造,如果实现了Externalizable接口的类中没有无参数的构造函数,在运行时会抛出异常:java.io.InvalidClassException。如果一个Java类没有定义任何构造函数,编译器会帮我们自动添加一个无参的构造方法,可是,如果我们在类中定义了一个有参数的构造方法了,编译器便不会再帮我们创建无参构造方法,这点需要注意

public class User{
  private String userName;
  private int userAge;
  get set......
  //这里是和Serializable区别地方,需要重写这两个方法
  public void writeExternal(ObjectOutput out) throws IOException {
       out.writeObject(name);
       out.writeInt(age);
   }
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
       name = (String) in.readObject();
       age = in.readInt();
   }

  @overwirte
  public String toString(){
    return "User{"+
          "name'"+name+'\''+
          ",age="+age+
          '}';
  }
}

ObjectOutput 和 ObjectInput接口

上面的writeExternal方法和readExternal方法分别接收ObjectOutput和ObjectInput类型参数。这两个类作用如下。

ObjectInput 扩展自 DataInput 接口以包含对象的读操作

DataInput 接口用于从二进制流中读取字节,并根据所有 Java 基本类型数据进行重构。同时还提供根据 UTF-8 修改版格式的数据重构 String 的工具。 对于此接口中的所有数据读取例程来说,如果在读取所需字节数之前已经到达文件末尾 (end of file),则将抛出 EOFException(IOException 的一种)。如果因为到达文件末尾以外的其他原因无法读取字节,则将抛出 IOException 而不是 EOFException。尤其是,在输入流已关闭的情况下,将抛出 IOException

ObjectOutput 扩展 DataOutput 接口以包含对象的写入操作

DataOutput 接口用于将数据从任意 Java 基本类型转换为一系列字节,并将这些字节写入二进制流。同时还提供了一个将 String 转换成 UTF-8 修改版格式并写入所得到的系列字节的工具。 对于此接口中写入字节的所有方法,如果由于某种原因无法写入某个字节,则抛出 IOException。

ObjectOutputStream 和ObjectInputStream

一般使用ObjectOutputStream的writeObject方法把一个对象进行持久化。再使用ObjectInputStream的readObject从持久化存储中把对象读取出来

transient

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null

serialVersionUID

关于serialVersionUID,阿里巴巴开发手册中就有提及

Serializable的源码,就会发现,他只是一个空的接口,里面什么东西都没有。Serializable接口没有方法或字段,仅用于标识可序列化的语义。但是,如果一个类没有实现这个接口,想要被序列化的话,就会抛出java.io.NotSerializableException异常,在序列化之前通过instanceof判断类是否为Enum、Array和 Serializable类型,如果都不是则直接抛出NotSerializableException。 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是我们在代码中定义的serialVersionUID 如果在一个类里面定义一个serialVersionUID,当这个UID修改再反序列化后,会跳出异常InvalidCastException,这是因为,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的 serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。这也是《阿里巴巴Java开发手册》中规定,在兼容性升级中,在修改类的时候,不要修改serialVersionUID的原因。 除非是完全不兼容的两个版本。所以,serialVersionUID其实是验证版本一致性的。如果读者感兴趣,可以把各个版本的JDK代码都拿出来看一下,那些向下兼容的类的serialVersionUID是没有变化过的。比如String类的 serialVersionUID一直都是-6849794470754667710L,其实在平时开发中,如果一个类实现了Serializable接口,就必须手动添加一个 private static final long serialVersionUID变量,并且设置初始值。 对于没有直接定义这个值的,在系统运行起来,会默认给出这个值,但是当修改这个类增加这个字段值以后,就会出现报错情况发生,所以建议每次增加这个值。 对于SerialVersionUID生成方式

  • 默认的1L
  • 通过类名、接口名、成员方法及属性等来生成一个64位的哈希字段,这个可以通过IDE自动生成

** 怎么保证修改抛出异常的原理 ** 在没有指定情况下,生成SerialVersionUID的流程

> ObjectInputStream.readObject -> readObject0 -> readOrdinaryObject -> readClassDesc -> readNonProxyDesc -> ObjectStreamClass.initNonProxy

在反序列化过程中,对serialVersionUID做了严格比较,如果发现不相等,则直接抛出异常

public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?    
if (suid == null) {        
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}  

在没有定义serialVersionUID的时候,会调用 computeDefaultSUID 方法,生成一个默认的serialVersionUID