龙空技术网

大厂面试系列-关于SerialVersionUID为什么不能随便改变

从程序员到架构师 565

前言:

此时各位老铁们对“java中serialversionuid生成”大体比较着重,兄弟们都想要知道一些“java中serialversionuid生成”的相关知识。那么小编同时在网上网罗了一些有关“java中serialversionuid生成””的相关文章,希望小伙伴们能喜欢,大家一起来学习一下吧!

关于SerialVersionUID这个字段,它到底是有什么作用呢?一般人只知道这个字段与Java的序列化有关,并且不能被随意的改变,但是并不知道serialVersionUID 字段为什么不能被随便的改变,这篇文章我们就来带着大家一起研究一下serialVersionUID字段为什么不能被改变。

简单说明

在之前的文章中我们介绍了关于Java序列化的相关内容,其中介绍了Java通过继承 Externalizable接口和 java.io.Serializable接口的方式来进行序列化操作。同时我们还说了如果一个类是可序列化的,那么其子类也是可以进行序列化的。

在之前的文章中我们也提到过java.io.Serializable接口其实是一个空接口。源码如下

public interface Serializable {}

会发现Serializable接口没有任何的方法或者是属性,它的唯一作用就是用来标记可以被序列化。如果没有继承这个接口的话就会抛出一个 NotSerializableException 的异常。

那么为什么只有实现了Serializable接口才可以被序列化呢?

在通过Externalizable接口实现序列化操作的时候,在这个接口中提供了两个方法 writeExternal 和readExternal 。而在我们介绍的时候如果使用Externalizable接口进行序列化操作就必须要实现这两个方法。由此我们可以推断,其实在整个的序列化过程中是经过了一个代码转换的过程。在整个的转换过程中需要对对象的可序列化操作进行判断。

在进行序列化操作的过程中,判断是否是字符串、是否是枚举、是否是Serializable等操作。而这个判断开启的标志就是Serializable接口。

在我们学习Java初级的时候知道transient关键字。是用来控制变量是否可以被序列化操作,如果在这个变量前加上了这个关键字可以阻止这个属性被序列化到文件中,而在反序列化操作之后这个值也会被赋值为初始值。

在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。

如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

所以在对一些需要特殊处理的操作的时候,可以考虑使用transient关键字或者是自己可以重写writeObject 和 readObject 两个方法。

在这里我们介绍了各种序列化操作,那么既然完成了序列化需要有这么多的操作,那么我们如何在这些操作中来判断是那一次的操作呢?这个时候就用到了serialVersionUID字段。

serialVersionUID字段

在之前的介绍中我们了解到序列化操作其实就是一个对象状态转换了存储过程转换的操作。也就是说Java对象原本是被保存在JVM中的堆内存里,但是由于JVM生命周期结束之后对象就不存在了,为了让对象进行持久化操作我们通过序列化的方式将其存入到磁盘中,并且在需要的时候可以再次通过反序列化的方式进行加载使用。

Java序列化是提供了一种JVM生命周期中能把对象进行永久存储的方案,而在JVM中是否允许被反序列化操作不仅取决于我们指定的类路径和包路径是否正确,其中一个更重要的操作是两个类所对应的serialVersionUID字段是否一致,当两个serialVersionUID一致的时候就认定为是同一个类就可以被进行反序列化操作,而如果两个serialVersionUID不一致则表示两个类在操作过程中发生了改变,也就是说不能认定为同一个类。

也许会有人问如果serialVersionUID发生了变化,在反序列化的时候会有什么样的影响呢?

首先我们通过一个小例子来看一下序列化操作中serialVersionUID变化对类产生的影响。

第一步、先来创建一个用于序列化测试的测试类,代码如下,其中 serialVersionUID对应的值是 1L。

public class SerializableTest implements Serializable {    private static final long serialVersionUID = 1L;    private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}

第二步、建立一个测试类对该对象进行序列化操作

public class SerializableDemo1 {    public static void main(String[] args) {        //Initializes The Object        SerializableTest serializableTest = new SerializableTest();        serializableTest.setName("Test");        //Write Obj to File        ObjectOutputStream oos = null;        try {            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));            oos.writeObject(serializableTest);        } catch (IOException e) {            e.printStackTrace();        } finally {            IOUtils.closeQuietly(oos);        }    }}

第三步、将上面代码中的serialVersionUID 改成2L。其他内容不变

public class SerializableTest implements Serializable {    private static final long serialVersionUID = 2L;    private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}

第四步、通过反序列化方式进行反序列化操作

public class SerializableDemo2 {    public static void main(String[] args) {        //Read Obj from File        File file = new File("tempFile");        ObjectInputStream ois = null;        try {            ois = new ObjectInputStream(new FileInputStream(file));            SerializableTest serializableTest = (SerializableTest) ois.readObject();            System.out.println(serializableTest);        } catch (IOException e) {            e.printStackTrace();        } catch (ClassNotFoundException e) {            e.printStackTrace();        } finally {            IOUtils.closeQuietly(ois);            try {                FileUtils.forceDelete(file);            } catch (IOException e) {                e.printStackTrace();            }        }    }}

这个时候就会出现一个如下的报错

java.io.InvalidClassException: com.example.demo.controller.SerializableTest; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

会看到代码执行完成之后抛出的异常是java.io.InvalidClassException,并且标记出不一致的serialVersionUID 是什么?

这是因为在进行序列化和反序列化的时候,JVM会将serialVersionUID对应的值与本地磁盘中存储的serialVersionUID进行比较,如果两者一致则认为是同一个对象,也就是可以被反序列化,但是如果serialVersionUID不一致就会出现InvalidClassException异常。

也就是说,serialVersionUID 是用来标记序列化和反序列化之后的对象是否一致的标记。

而在我们开发过程中很少去写这个熟悉,这是因为在编译的过程中由编译器会为我们创建了默认的serialVersionUID,这也就是在我们查看有些class的源码的时候会发现在源码中会有这样一个属性的原因,但是在我们实际开发的过程中并没有编写这个属性。下面我们就来看一下serialVersionUID的校验原理

为什么一定要指定serialVersionUID值呢?

其实从字面意思上可以理解因为在serialVersionUID中出现了一个Version的单词,也就是说serialVersionUID值其实是与对象版本有关。也许有人到这里就有点懵了,什么是对象版本有关?

简单的举个例子。我们有这个一个类。并且该类进行了序列化操作,但是我们并没有明确的指定该类的serialVersionUID是多少,根据上面说的,在JVM会自动添加一个serialVersionUID。

public class SerializableTest implements Serializable {    private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}

后续因为逻辑的变化我们在这个类中增加了一个属性,如下。

public class SerializableTest implements Serializable {      private String id;    private String name;    public String getId() {        return id;    }    public void setId(String id) {        this.id = id;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }}

这个时候我们对其进行反序列化操作的时候,就会出现上面的InvalidClassException异常,这个时候,就会看到在JVM默认添加的serialVersionUID值。从这个操作中我们就可以知道,SerializableTest 发生了变化,就有点类似于从版本1变成了版本2,不管是属性还是方法那个发生了变化,那么这个类都会有一个新的serialVersionUID值。这样我们就会很明确的知道是发生了变化。

如果我们为其指定了一个自定义的serialVersionUID值那么我们通过对serialVersionUID值的变化就可以知道需要适配那个版本的类对象进行序列化操作。就有利于我们对类对象进行标记了。

反序列化的原理详解

在我们反序列化的操作过程中为什么serialVersionUID发生了变化之后就会抛出异常呢?

在反序列化操作过程中类对象经历的如下的一些过程。

ObjectInputStream.readObject()方法readObject0()方法readOrdinaryObject()方法readClassDesc()方法readNonProxyDesc()方法ObjectStreamClass.initNonProxy()方法

按照上述过程进行了调用,实现了反序列化操作,有兴趣的读者可以按照上面方法提供的方法名在源码中进行探索。其中比较重要的也是值得我们注意的就是initNonProxy()方法,在这个方法中会看到我们抛出的InvalidClassException异常的内容。如下。

  if (model.serializable == osc.serializable &&                    !cl.isArray() &&                    suid != osc.getSerialVersionUID()) {                throw new InvalidClassException(osc.name,                        "local class incompatible: " +                                "stream classdesc serialVersionUID = " + suid +                                ", local class serialVersionUID = " +                                osc.getSerialVersionUID());            }

也就是说在反序列化读取的过程中其实是对serialVersionUID进行了比较操作。如果发现不相等的时候就直接抛出了异常。而这里有一个getSerialVersionUID() 方法我们可以进一步的进行查看。代码如下。

   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);                    }                }            );        }        return suid.longValue();    }

会看到如果在对应的序列化类中没有找到相应的serialVersionUID的时候会调用computeDefaultSUID(cl)方法进行生成。对于生成serialVersionUID的操作,这里我们先不做讨论。

总结

其实在我们开发过程中无论是IDEA还是Eclipse都有对应的提示,让我们去创建一个serialVersionUID值,但是实际操作中我们并不会太多的关注这个值,都是有编译器自己进行创建。现在的编译器也是越来智能了,这里纠正的就是在上面描述中我们提到的关于serialVersionUID生成的操作描述其实是有待斟酌的。对于我们默认不写的serialVersionUID值并不是在编译阶段生成的,而是在编译器中默认生成的。

在反序列化操作过程中如果出现serialVersionUID未指定的情况,则是需要调用对应的生成方式在反序列的校验中根据类的属性在进行生成的。这个是我们在上面表述中存在的问题。

标签: #java中serialversionuid生成