JDK1.8中的HashMap
在链表存储上进行了优化,当链表结点过多时,查询效率会降低,所以引入了红黑树:在一条链表的长度超过8时,将其转换为红黑树存储,提高查询效率。存储结点使用了尾插法。
HashMap总体与JDK1.7的相似,所以只列出不同之处。
Put方法
依旧是同样的思路,对于key的hashcode进行hash()处理为hash值,再与数组长度-1进行&操作取得下标。但是数组从Entry[]换为了Node[],总体数据结构相同。存储的方式也发生了改变:
当链表长度小于等于8,采用尾插法将结点插入至链表。
当连表长度大于8(第9个结点插入后),将链表转换为红黑树。
链表转为红黑树过程
首先,创建红黑树实际上是一个很复杂的过程,所以,HashMap会优先选择扩容,也就是说,当在转换时数组长度小于64,则会对数组进行一个扩容操作,从而散列化数组达到提速的目的。在转换时数组长度等于64,才会对链表进行红黑树转换。
链表如何转换为红黑树
红黑树结点为TreeNode,继承了Entry,同时拥有parent,left,right,next,prev,hash属性。
在转换时,首先会将整个链表转换为TreeNode双向链表。将prev,next绑定好,再对TreeNode链表进行树化。之后,遍历整个TreeNode双向链表,开始用红黑树的思路进行结点插入。
由于结点大小决定结点存放的位置,遍历树时结点的大小判断遵从如下原则:
- 先对比两个结点的hash值,如果hash值相同,则进入2
- 获取结点key是否实现了Comparable接口,如果是,则通过compareTo方法对比两个结点的大小。若对比结果为相等,或者key并没有实现Comparable接口,则进入3
- 对比两个结点的类名是否相同,如果相同则进入4
- 使用System.identityHashCode()方法处理并对比A结点的key的hashcode是否小于等于B结点的key的hashcode;System.identityHashCode()方法是防止开发者重写了类的hashcode()方法导致无法获得类初始化时的hashcode值。它会返回类的hashcode值。
确定位置后,将结点插入,按红黑树的要求使用变色和旋转修改树结构。
扩容
扩容方面与1.7类似,但是添加了树和链表的转换,并且对于链表的转移也使用了全新的机制。
转移链表:
由于扩容的结果为旧容量的2倍,所以,在旧数组n位置上存储的结点只能在新数组的n位置或者n+旧数组容量这两个位置上存放。所以,再判断新数组位置时使用了:key.hash&旧数组容量 + 旧数组下标。并且,先遍历一遍链表,找到所有在原位置的数组(低位),以及所以在原位置+旧数组下标(高位)的链表。并直接将两条链表转移。不再一个一个地去转移。
转移红黑树:
如果结点为TreeNode类型,则此处存储的是红黑树,由于TreeNode类型继承了Node和Entry结点,所以,它不仅仅是树,内部也存在一条TreeNode链表;通过同样的方式找到低位链表和高位链表。在转移低位链表时,先判断高位链表是否为null,如果为null,则表明这个树在新数组都只在一个位置,所以直接将红黑树转移至新数组。如果不满足,则判断链表长度是否小于等于6,如果满足,则利用TreeNode的链表结构将红黑树重新转化为链表存储。如果不满足,则将TreeNode链表重新树化。因为结点的改变会破坏原先的树结构,转移时只是重新组了一条链表,所以需要重新转化为红黑树。
Remove()
remove较为特殊,在remove时,如果是链表,则与JDK1.7大同小异。
如果是红黑树:remove结束之后,先判断是否满足如下条件
- 根节点是否为空 ||
- 根节点是否有左孩子 ||
- 根节点是否有右孩子 ||
- 根节点的左孩子是否有左孩子
如果返回的是true,则将红黑树转换为链表,否则,对树结构进行更新。
在remove时,遍历结点的思路与put一致。