Open MicroKibaco opened 5 years ago
待分析~
HashMap在1.7的基础上引入了 红黑树
,首先针对接受需要的存储键值对(k,v)标记为x,然后再使用hashCode根据键计算hash值,这也是与JDK1.7的主要区别,然后再根据数组计算hash的下标位,如果发生hash冲突?即该位置之前存储过数据,当前节点数据结构是否为红黑树 或者 链表,如果是链表结构,在链表末尾插入数据,如果是红黑树,在红黑树插入数据,整个存储过程结束
1.8的 扩容机制,新元素存储机制提高了效率
注意: HashMap在添加第一个元素时才会创建数组。
put方法分为三种情况:
table尚未初始化,对数据进行初始化
table已经初始化,且通过hash算法找到下标所在的位置数据为空,直接将数据存放到指定位置
table已经初始化,且通过hash算法找到下标所在的位置数据不为空,发生hash冲突(碰撞),发生碰撞后,会执行以下操作:
判断插入的key如果等于当前位置的key的话,将 e 指向该键值对
如果此时桶中数据类型为 treeNode,使用红黑树进行插入
如果是链表,则进行循环判断, 如果链表中包含该节点,跳出循环,如果链表中不包含该节点,则把该节点插入到链表末尾,同时,如果链表长度超过树化阈值(TREEIFY_THRESHOLD)且table容量超过最小树化容量(MIN_TREEIFY_CAPACITY),则进行链表转红黑树(由于table容量越小,越容易发生hash冲突,因此在table容量<MIN_TREEIFY_CAPACITY 的时候,如果链表长度>TREEIFY_THRESHOLD,会优先选择扩容,否则会进行链表转红黑树操作)
根据hash值查找到指定位置的数据
校验指定位置第一个节点的数据是key是否为传入的key,如果是直接返回第一个节点,否则继续查找第二个节点
如果数据是TreeNode(红黑树结构),直接通过红黑树查找节点数据并返回
如果是链表结构,循环查找所有节点,返回数据
如果没有找到符合要求的节点,返回null
resize 方法兼顾两个职责,创建初始存储表格,或者在容量不满足需求的时候,进行扩容。
首先先判断当前table是否进行过初始化,如果没有进行过初始化,此处就解决了调用无参构造方法时候,threshold和initialCapacity 未初始化的问题,如果已经初始化过了,则进行扩容,容量为原来的二倍
扩容后创建新的table,并对所有的数据进行遍历
如果新计算的位置数据为空,则直接插入
如果新计算的位置为链表,则通过hash算法重新计算下标,对链表进行分组
如果是红黑树,则需要进行拆分操作
put():在新增元素时,先通过传入的key进行哈希计算,然后得到该元素在节点数组的位置,如果该数组对应位置没有该元素,则将该节点放到该位置; 如果该位置存在该元素,则遍历该元素与其后面的节点,如果发现有和要添加节点的key一样的节点,直接替换该节点的值。若最终未发现,则将要添加的节点放到数组当前位置,然后next指向该位置的下一个节点; get():先定位到数组元素,再遍历该元素处的链表; 确定数组index:hashcode % table.length取模; 如果两个key拿到的index相等:Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。
TreeMap:通常比HashMap要慢,与使用TreeSet一样,TreeMap时一种创建有序列表的方式。树的行为时:总是保证有序,并且不必进行特殊的排序。如果使用Map,第一选择是HashMap,如果要求Map始终保持有序,可以选择TreeMap; LinkedHashMap:在插入时比HashMap慢一点,因为它维护散列数据结构的同时还要维护链表(以保持插入顺序)。正是由于这个列表,使得其迭代速度更快。
JDK1.7中HashMap的数据结构是数组+链表; JDK1.8引入了红黑树,原因:解决发生哈希碰撞后,链表过长从而导致索引效率慢的问题;应用场景:当链表长度大于8时,该链表转换成红黑树。
参考的博客Java中HashMap的实现原理 HashMap底层是数组加链表的数据结构。
上边两行字怎么那么大。。。奇怪,没加md的标题字符啊 我下边大概说一下HashMap的存储: 开始之前要知道hashMap中是有一个数组来存放key-value数据的。
首先我们调用HashMap.put(key,value)的方法; 接着我们通过一个算法将key值转换成一个int值temp,然后我们查看数组下标为temp的数据是不是存在,如果不存在,我们直接讲key-value存到数组中,下标就是通过hash算法对key计算出来的temp值; 如果数组中temp下标中有数据怎么办呢?那说明之前存过一个可能是同样key的数据。为什么是可能相同呢?因为通过hash计算两个不相同的key,有可能得到一个相同的temp。那么我们就需要使用equals方法计算当前数组中存在的key和我要存的key是不是一样的
其实到这里我们可以发现,链表的作用其实是很小的,但是也很必要,他就是为了防止hash算法的可能会重复的特性备用的数据结构。 这其中,数组才是最主要用到的数据结构,通过hash算法计算key得到的hash值temp,我们可以很快的从数组中得到我们需要的数据,这样查询大大提高了效率。 以上我们还能得到一个结论,就是我们计算出来的temp不可能是连续的,那就是说hashMap的数据也不可能是连贯性的。
以上是我看源码连蒙带猜,加看博客得出的个人理解结论,其实源码中的put方法还有几处困扰我的点,暂时还没有找到答案。但是大致的思路应该就是这样,中间我省掉了一步
if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
的讲解,这个我接下来再琢磨一下。 看源码还挺有意思,可是我基础的知识太薄弱了,中间好多都会卡壳。接下来有时间我把获取数据的源码再看一下,然后放上来给自己公开处刑。。。
看这一段源码我的几个问题,放上来大家帮我看下
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//1.tab[i = (n - 1) & hash]这一步没搞懂,这一步是很关键的一个验证步骤,但是内部做了什么我不太清楚
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//2.这是被我省掉的一步,应该就是上边小伙伴说的红黑树(这个有点懵逼,要看一下)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
我说说自己的看法吧:使用最多的是HashMap。
HashMap里面存入的键值对在取出时没有固定的顺序,是随机的。一般而言,在Map中插入、删除和定位元素,HashMap是最好的选择。
在使用多线程时,考虑线程安全,优先使用HashTable。
由于TreeMap实现了SortMap接口,能够把它保存的记录根据键排序,因此,取出来的是排序的键值对,如果需要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序排列。
HashMap
HashMap底层是使用数组加链表实现的,所以我们说HashMap是散列链表。 添加元素时,首先通过key进行哈希计算,计算出该元素在节点数组的下标,如果该数组下标没有元素,则将该节点放到该位置 如果该节点已有元素,则遍历该元素与其后面的节点,如果发现有和要添加节点的key一样的节点,则替换该节点的值。若最终未发现,则将要添加的节点放到数组当前下标处,next指向原先放在此下标位置的节点 当然这些操作之前,还需要判断table数组是否需要扩容操作。 HashMap实现了Map的全部方法,它有以下特点: key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类需要重写 hashCode 和 equals 方法 允许空键和空值 元素是无序的,而且顺序会不定时改变 插入、获取的时间复杂度基本是 O(1)(前提是有适当的哈希函数,让元素分布在均匀的位置) 遍历整个 Map 需要的时间与 桶(数组) 的长度成正比(因此初始化时 HashMap 的容量不宜太大)
HashTable
除了不允许Null key和Null Value并且同步,其他均和HashMap一致
LinkedHashMap
LinkedHashMap是HashMap的子类。它通过双向列表来保证了添加的元素的有序性。它有以下几个特点: 1 双向链表+哈希表 2 线程不安全 3 允许空的键或者值 4 有序(插入排序和访问排序)