简介: 介绍一下关于serialVersionUID 。这个字段到底有什么用?如果不设置会怎么样?为什么《Java开发手册》中有那样的规定?

作者 | Hollis

序列化是一种对象持久化的手段。普遍应用在网络传输、RMI等场景中。类通过实现 java.io.Serializable 接口以启用其序列化功能。

在我的博客中,其实已经有多篇文章介绍过序列化了,对序列化的基础知识不够了解的朋友可以参考以下几篇文章:

《Java对象的序列化与反序列化》、 《深入分析Java的序列化与反序列化》、 《单例与序列化的那些事儿》

在这几篇文章中,我分别介绍过了序列化涉及到的类和接口、如何自定义序列化策略、transient关键字和序列化的关系等,还通过学习ArrayList对序列化的实现源码深入学习了序列化。并且还拓展分析了一下序列化对单例的影响等。

但是,还有一个知识点并未展开介绍,那就是关于serialVersionUID 。这个字段到底有什么用?如果不设置会怎么样?为什么《Java开发手册》中有以下规定:

背景知识

Serializable 和 Externalizable

类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法进行序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。

如果读者看过Serializable的源码,就会发现,他只是一个空的接口,里面什么东西都没有。Serializable接口没有方法或字段,仅用于标识可序列化的语义。但是,如果一个类没有实现这个接口,想要被序列化的话,就会抛出java.io.NotSerializableException异常。

它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?

原因是在执行序列化的过程中,会执行到以下代码:

if (obj instanceof String) {    writeString((String) obj, unshared);} else if (cl.isArray()) {    writeArray(obj, desc, unshared);} else if (obj instanceof Enum) {    writeEnum((Enum>) obj, desc, unshared);} else if (obj instanceof Serializable) {    writeOrdinaryObject(obj, desc, unshared);} else {    if (extendedDebugInfo) {        throw new NotSerializableException(            cl.getName() + "" + debugInfoStack.toString());    } else {        throw new NotSerializableException(cl.getName());    }}

在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果都不是则直接抛出NotSerializableException。

Java中还提供了Externalizable接口,也可以实现它来提供序列化能力。

Externalizable继承自Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。

当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。否则所有变量的值都会变成默认值。

transient

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

自定义序列化策略

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

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

用户自定义的writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

所以,对于一些特殊字段需要定义序列化的策略的时候,可以考虑使用transient修饰,并自己重写writeObject 和 readObject 方法,如java.util.ArrayList中就有这样的实现。

我们随便找几个Java中实现了序列化接口的类,如String、Integer等,我们可以发现一个细节,那就是这些类除了实现了Serializable外,还定义了一个serialVersionUID

那么,到底什么是serialVersionUID呢?为什么要设置这样一个字段呢?

什么是serialVersionUID

序列化是将对象的状态信息转换为可存储或传输的形式的过程。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。

而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是我们在代码中定义的serialVersionUID。

如果serialVersionUID变了会怎样

我们举个例子吧,看看如果serialVersionUID被修改了会发生什么?

public class SerializableDemo1 {    public static void main(String[] args) {        //Initializes The Object        User1 user = new User1();        user.setName("hollis");        //Write Obj to File        ObjectOutputStream oos = null;        try {            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));            oos.writeObject(user);        } catch (IOException e) {            e.printStackTrace();        } finally {            IOUtils.closeQuietly(oos);        }    }}class User1 implements Serializable {    private static final long serialVersionUID = 1L;    private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    } }

我们先执行以上代码,把一个User1对象写入到文件中。然后我们修改一下User1类,把serialVersionUID的值改为2L。

class User1 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));            User1 newUser = (User1) ois.readObject();            System.out.println(newUser);        } 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.hollis.User1; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

可以发现,以上代码抛出了一个java.io.InvalidClassException,并且指出serialVersionUID不一致。

这是因为,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

这也是《Java开发手册》中规定,在兼容性升级中,在修改类的时候,不要修改serialVersionUID的原因。除非是完全不兼容的两个版本。所以,serialVersionUID其实是验证版本一致性的。

如果读者感兴趣,可以把各个版本的JDK代码都拿出来看一下,那些向下兼容的类的serialVersionUID是没有变化过的。比如String类的serialVersionUID一直都是-6849794470754667710L。

但是,作者认为,这个规范其实还可以再严格一些,那就是规定:

如果一个类实现了Serializable接口,就必须手动添加一个private static final long serialVersionUID变量,并且设置初始值。

为什么要明确定一个serialVersionUID

如果我们没有在类中明确的定义一个serialVersionUID的话,看看会发生什么。

尝试修改上面的demo代码,先使用以下类定义一个对象,该类中不定义serialVersionUID,将其写入文件。

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

然后我们修改User1类,向其中增加一个属性。在尝试将其从文件中读取出来,并进行反序列化。

class User1 implements Serializable {    private String name;    private int age;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    } }

执行结果: java.io.InvalidClassException: com.hollis.User1; local class incompatible: stream classdesc serialVersionUID = -2986778152837257883, local class serialVersionUID = 7961728318907695402

同样,抛出了InvalidClassException,并且指出两个serialVersionUID不同,分别是-2986778152837257883和7961728318907695402。

从这里可以看出,系统自己添加了一个serialVersionUID。

所以,一旦类实现了Serializable,就建议明确的定义一个serialVersionUID。不然在修改类的时候,就会发生异常。

serialVersionUID有两种显示的生成方式:一是默认的1L,比如:private static final long serialVersionUID = 1L;二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:private static final long serialVersionUID = xxxxL;

后面这种方式,可以借助IDE生成,后面会介绍。

背后原理

知其然,要知其所以然,我们再来看看源码,分析一下为什么serialVersionUID改变的时候会抛异常?在没有明确定义的情况下,默认的serialVersionUID是怎么来的?

为了简化代码量,反序列化的调用链如下:

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

在initNonProxy中 ,关键代码如下:

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

深入看一下getSerialVersionUID方法:

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

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

这也就找到了以上两个问题的根源,其实是代码中做了严格的校验。

IDEA提示

为了确保我们不会忘记定义serialVersionUID,可以调节一下Intellij IDEA的配置,在实现Serializable接口后,如果没定义serialVersionUID的话,IDEA(eclipse一样)会进行提示:

并且可以一键生成一个:

当然,这个配置并不是默认生效的,需要手动到IDEA中设置一下:

在图中标号3的地方(Serializable class without serialVersionUID的配置),打上勾,保存即可。

总结

serialVersionUID是用来验证版本一致性的。所以在做兼容性升级的时候,不要改变类中serialVersionUID的值。

如果一个类实现了Serializable接口,一定要记得定义serialVersionUID,否则会发生异常。可以在IDE中通过设置,让他帮忙提示,并且可以一键快速生成一个serialVersionUID。

之所以会发生异常,是因为反序列化过程中做了校验,并且如果没有明确定义的话,会根据类的属性自动生成一个。

来源 | HollisChuang's Blog

mybaits 字段设置null_为什么阿里巴巴禁止开发人员修改serialVersionUID 字段的值相关推荐

  1. 为什么阿里巴巴禁止开发人员使用isSuccess作为变量名(修订版)

    在日常开发中,我们会经常要在类中定义布尔类型的变量,比如在给外部系统提供一个RPC接口的时候,我们一般会定义一个字段表示本次请求是否成功的. 关于这个"本次请求是否成功"的字段的定 ...

  2. 为什么阿里巴巴禁止开发人员使用isSuccess作为变量名

    在日常开发中,我们会经常要在类中定义布尔类型的变量,比如在给外部系统提供一个RPC接口的时候,我们一般会定义一个字段表示本次请求是否成功的. 关于这个"本次请求是否成功"的字段的定 ...

  3. 为什么阿里巴巴禁止开发人员 boolean 类型变量使用 isXXX 来命名?

    >>号外:关注"Java精选"公众号,菜单栏->聚合->干货分享,回复关键词领取视频资料.开源项目. 背景 平时工作中大家经常使用到boolean以及Boo ...

  4. serialversionuid的作用_为什么阿里Java规约要求谨慎修改serialVersionUID字段

    serialVersionUID简要介绍 serialVersionUID是在Java序列化.反序列化对象时起作用的一个字段.Java的序列化机制是通过判断类的serialVersionUID来验证版 ...

  5. python 什么可以作为变量名_为什么强烈禁止开发人员使用isSuccess作为变量名

    在日常开发中,我们会经常要在类中定义布尔类型的变量,比如在给外部系统提供一个RPC接口的时候,我们一般会定义一个字段表示本次请求是否成功的. 关于这个"本次请求是否成功"的字段的定 ...

  6. 为什么强烈禁止开发人员使用isSuccess作为变量名

    在日常开发中,我们会经常要在类中定义布尔类型的变量,比如在给外部系统提供一个RPC接口的时候,我们一般会定义一个字段表示本次请求是否成功的. 关于这个"本次请求是否成功"的字段的定 ...

  7. mybaits 字段设置null_并发编程的艺术:双重检查锁为什么要使用volatile字段?

    双重锁的由来 单例模式中,有一个DCL(双重锁)的实现方式.在Java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才开始初始化. 下面是非线程安全的延迟初始化对象的实 ...

  8. php数据库字段设置长度,javascript - 表单字符长度与数据库字段长度

    html的表单length长度是以字符个数计算的,不管是汉字还是字母,但是数据库又是按字节计算的,汉字占2个字母占1个,这样容易造成写入的时候长度超出的问题. 两个问题: 1.有没有好的方法,能够在前 ...

  9. 解读《阿里巴巴 Java 开发手册》背后的思考

    <阿里巴巴 Java 开发手册>是阿里巴巴集团技术团队的集体智慧结晶和经验总结,经历了多次大规模一线实战的检验及不断的完善,系统化地整理成册,反馈给广大开发者.现代软件行业的高速发展对开发 ...

最新文章

  1. ASP.NET弹出窗口技术之增加网站流量方法
  2. 感谢大家的支持,MVP之后需要总结
  3. 2.9 什么是端到端的深度学习-深度学习第三课《结构化机器学习项目》-Stanford吴恩达教授
  4. 转载--va_list
  5. git仓库的推送问题
  6. 《塑造互联网思维的企业》一一第4章 全球商务向社会化媒体的转变
  7. 给你多少钱,你才会愿意为国家生孩子?
  8. 多线程的创建方式 你会优先选择哪一种_Python多线程入门到放弃
  9. python生成数字_Python生成数字图片代码分享
  10. 使用FragmentTabHost+Fragment+viewpager 实现滑动分页
  11. ffmepg安装yasm之后还是出现nasm/yasm not found or too old. Use --disable-x86asm for
  12. CentOS 7.4 初次手记:第三章 CentOS基础了解
  13. SVN多项目共享导出及故障处理
  14. 【专业学位、学术学位硕士研究生】区别是?如何报考
  15. 分区表中GLO字段对信息收集的影响
  16. [MySQL远程备份策略举例]
  17. 摄氏度和华氏度的换算
  18. 【Try to Hack】Kerberos基础
  19. SQL server数据库实验(三)数据库的嵌套查询和集合查询
  20. cs/bs以及优缺点

热门文章

  1. 第8章:Kubernetes 安全
  2. linux服务器拷贝目录文件夹,linux两台服务器之间文件/文件夹拷贝
  3. GitHub Token的使用
  4. 数据库领域 TOP10 热门课程推荐 | 最棒的课程给最好的你
  5. 技术的想象力:云栖大会第一天发布了什么?
  6. 云原生下,如何实现高可用的MySQL?
  7. 如何解决大规模机器学习的三大痛点?
  8. 游戏关卡中的类型运用:《LOOP》的无限可能
  9. 《Booth 空箱》发售一周年回顾
  10. 为什么说Android才是游戏开发者的乐土?