Java transient 关键字解密!!
HashMap引起的故事
某同学简称小M,最近在复习Java基础准备面试,众所周知,Java的HashMap相关的面试题非常多,比如HashMap的底层结构,扩容,loadfactor,线程安全问题,不同jdk版本的实现,树化等等。所以小M准备打开HashMap的源码一探究竟。首先他看到了HashMapd底层结构,大家常常说HashMap是一个数组链表。在源码中:
就是数组指的就是这个table,链表指的是这个table中的每个元素都是一个链表的头节点(或者树的根节点)。但是细心的小M发现,这个结构被一个关键字:transient修饰了。这让他有点疑惑,要说static,final,public,private这些关键字它很熟悉,但是transient关键字还是第一次见。这是啥玩意?
先查资料
遇到不懂的问题,首先查资料。首先看看这个词啥意思,毕竟,中文意思小M都不知道。从google翻译可以知道,这个词是短暂的意思。那么用来修饰table难道说这个table是短暂的?这个短暂指的是什么短暂?小M能想到的最多就是生命周期是短暂的,比如在GC的时候会优先回收这块内存,但是这好像和我们平时的认知不太相符,毕竟我们平时用HashMap从来没有出现过因为GC导致的问题。那别瞎猜了,看看官方文档怎么说。
从oracle的关于java se的官方文档中,找到:https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.3.1.3 。其中关于transient key word的描述是这样的:
这段话翻译一下就是: 被transient标记的变量说明他们不是这个对象需要持久化(persitent)的一部分.并且下面举了一个例子,有个Point的类,里面只有x,y会被持久化,rho和theta不会被持久化。因为他们被transient修饰了。这里有一个问题:什么叫做持久化?在例子的后面他解释了,持久化指的就是序列化相关的service。那么到这里我们可以总结出来一个信息就是:transient修饰的变量不会被持久化。
Java 序列化和transient
到这里小M就更有疑问了,transient有那么神奇嘛?被他修饰之后在序列化时就会被跳过,那么HashMap的table数组为啥不用被序列化?table可是存数据的地方,如果他不被序列化,那HashMap做序列化时,数据咋搞?带着这些疑问,小M做了接下来的实验。
###transient 修饰字段的序列化
既然官方有例子,那么就把官方的例子跑一下。新建一个Point类,实现Serializable接口(要想支持对象序列化和反序列,必须实现这个接口)。添加构造方法和get set方法,并且重新一些toString方法:
toString:
接着新建一个主函数,new 一个point对象序列化,然后再反序列化.
执行一下看看结果:
果然,反序列化之后,rho和theta两个字段是没有值的。
HashMap和transient
HashMap既然使用了这个关键字修饰table,说明table是不用背序列化的,那HashMap序列化之后的值岂不是为空了,这不符合尝试,简单做个实验。
还是刚才的程序,发现序列化和反序列化出来的结果是一样的,这说明HashMap自己重写了序列化和反序列的逻辑,不依赖Java默认的序列化逻辑。那么这两个函数在哪呢?
其实很简单,我们在序列化和反序列化的时候,调用的是writeObject和readObject,当一个类想自己实现序列化和反序列化时,重写这两个方法即可。
writeObject:
readObject:
这里我们简单看一个就行,因为read和write是两个相反的操作,怎么写进去的,就怎么读出来,这很好理解。看一下writeObject。writeObject首先获取了capacity(),这个就是table的大小。
然后是defaultWriteObject:
defaultwriteObject的主要逻辑在 defaultWriteFields:
这个函数的逻辑也不复杂,首先反射调用序列化类里面的方法,然后遍历table里面的所有字段,一个一个的writeObject。所以到这里就基本明白了HashMap是怎么序列化的:HashMap在序列化时,遍历map对象,然后按照一个一个kv pair的序列化。那么它为什么要这样做呢?这和直接序列化table有啥区别?
HashMap为啥这样做
要回答这个问题,其实要思考一下HashMap是怎么添加元素的。也就是put方法。
可以看到put方法其实是调用了putVal方法,里面第一个参数,是计算了key的hash值。
这个函数在key非空的时候,直接返回一个key的hashcode相关的值。(ps:至于为啥这样算感兴趣的伙伴可以看看,非常的有意思)。而这个hash值,会决定这个元素放在table的哪个位置。这里说明了一个非常重要的点:hashcode决定了当前这个kv pair在table中的位置。那么hashcode是怎么计算的呢?
打开源码发现,hashcode是native的方法,说明这个函数其实是和当前操作系统相关的。native修饰的方法是java里面的一种特殊调用:JNI(Java native invoke)。这种方法其实在JVM里面有一个对应的C语言实现的方法,不同的平台实现的不一致。所以到这里我们就能回到为什么hashmap在序列化的时候table是不能序列化的:因为table里面的元素以及他们的位置其实只能代表当前环境下的结果,在序列化的时候很有可能会经过网络传输到另外一台设备上,这时候他的hashcode实现和当前设备不一定一致,所以当前这个table在另外一个设备上里面元素排序是不一样的,那么这种情况下,只能通过把具体的值序列出出来,然后再反序列化的时候重新一个个put到table里面。那么这时候我们再看readObject就可以发现:
确实是这样做的。到这里所有的疑问就解答了。
总结
看来,搞清楚疑问最好的方法就是,动手实现一遍! 创作不易,记得关注哦!!一起学习Java私聊加群!
#Java求职##Java##学习路径#