赞
踩
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,我们先来看一下其它数据结构的特点。
(在找到指定操作位置后)
,仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)我们可以发现,数组和链表几乎是两个极端,一个查找效率高,一个插入删除效率高,那么将两者的优点进行融合,得到的是一个存放链表的数组,这种数组和链表的结合体,叫散列表或哈希表
。
好的哈希函数会尽可能地保证散列的地址分布均匀
,但是再好的哈希函数也会出现冲突的情况,比如我们的两个元素通过哈希函数得到同一个存储地址,那么该如何解决呢?哈希冲突的解决方案有很多种,而HashMap采用了链地址法,就是数组+链表的方式,所有通过哈希函数得到同一地址的元素,将其加在对应的链表上,就构成了HashMap
。HashMap是一个散列桶(数组和链表),可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。当get(key)方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key为null,因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()
方法来判断
Hashtable的key与value均不能为null
HashMap线程不安全,没synchronized,但HashMap快
初始Table数组的长度为16(Table.length一定为2的n次幂),当HashMap中元素总数超过table数组的75%,触发扩容操作,扩容方式为 newTable.length = oldTable.length*2,扩容针对整个HashMap,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
HashMap 通过 key 的 hashCode()得到 hash 值,然后通过 (Table.length - 1) & hash
判断当前元素存放的位置(这里的 Table.length指的是数组的长度)
使用length-1的意义在于,length是2的倍数,所以length-1在二进制来说每位都是1,可以最大程度地散列hash值,充分利用数组的空间,并减少 key 的碰撞,否则,当有一位是0时,不管hash值对应位是1还是0,按位与后的结果都是0,会造成散列结果的重复
如果hash碰撞,后加入的值
//HashMap的主干数组,可以看到就是一个Node数组,初始值为空数组,主干数组的长度一定是2的次幂
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
final K key;
V value;
Node<K,V> next;//存储指向下一个Entry的引用,单链表结构
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
当HashMap中元素个数超过Table数组大小*loadFactor
时,就会进行数组扩容,(loadFactor的默认值为0.75),HashMap的Table数组扩容后,原数组中的数据必须重新计算其在新数组中的位置,并放进去
JDK1.8 之前
HashMap 底层是 数组和链表
结合在一起使用也就是 链表散列
。如果在一个HashMap中,有很多Key发生了碰撞的时候,就会产生一个超级长的链表,那么在数据查询的时候就会花费O(n)的时间。红黑树是一种近似平衡的二叉查找树,他并非绝对平衡,但是可以保证任何一个节点的左右子树的高度差不会超过二者中较低的那个的一倍。红黑树有这样的特点:
- 每个节点要么是红色,要么是黑色。
- 根节点必须是黑色。叶子节点必须是黑色NULL节点。
- 红色节点不能连续(黑色节点可以连续)
- 对于每个节点,从该点至叶子节点的任何路径,都含有相同个数的黑色节点
- 能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决
线程是否安全
:
如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!
);效率
: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;对 Null key 和 Null value 的支持
:
初始容量大小和每次扩充容量大小的不同
:
底层数据结构
: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制
。底层数组+链表
实现,无论key还是value都不能为null
在修改内部共享数据时,使用synchronized锁住整个HashTable
,保证线程安全,但是效率低,ConcurrentHashMap做了相关优化
当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
初始Table数组的长度为为11,当HashTable中元素总数超过Table数组的75%,扩容方式为newTable.length = oldTable.length*2+1
,扩容针对整个HashTable,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
计算索引的方法:index = (hash & 0x7FFFFFFF) % tab.length
将新元素加到链表头部
底层数据结构:
分段的数组+链表
实现数组+链表/红黑二叉树
实现线程安全的方式(重要):
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树
的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作(JDK1.6 以后 对 synchronized 锁做了很多优化)
。 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就可以产生并发,效率又提升 N 倍。
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
一、HahMap存储对象的过程如下
1、对HahMap的Key调用hashCode()方法,返回int值,即对应的hashCode;
2、把此hashCode作为哈希表的索引,查找哈希表的相应位置,若当前位置内容为NULL,则把hashMap的Key、Value包装成Entry数组,放入当前位置;
3、若当前位置内容不为空,则继续查找当前索引处存放的链表,利用equals方法,找到Key相同的Entry数组,则用当前Value去替换旧的Value;
4、若未找到与当前Key值相同的对象,则把当前位置的链表后移(Entry数组持有一个指向下一个元素的引用),把新的Entry数组放到链表表头;
二、HashSet存储对象的过程
往HashSet添加元素的时候,HashSet会先调用元素的hashCode方法得到元素的哈希值 ,然后通过元素 的哈希值经过移位等运算,就可以算出该元素在哈希表中 的存储位置。
- 情况1: 如果算出元素存储的位置目前没有任何元素存储,那么该元素可以直接存储到该位置上。
- 情况2: 如果算出该元素的存储位置目前已经存在有其他的元素了,那么会调用该元素的equals方法与该位置的元素再比较一次,如果equals返回的是true,那么该元素与这个位置上的元素就视为重复元素,不允许添加,如果equals方法返回的是false,那么允许添加该元素。
HashMap是无序的
,也就是说,迭代HashMap所得到的元素顺序并不是它们最初放置到HashMap的顺序。HashMap的这一缺点往往会造成诸多不便,因为在有些场景中,我们需要用到一个可以保持插入顺序的Map。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个子类 —— LinkedHashMap。虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序
。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap和保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的
。LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了Entry。LinkedHashMap中的Entry增加了两个指针 before 和 after,它们分别用于维护双向链接列表。特别需要注意的是,next用于维护LinkedHashMap各个桶中Entry的连接顺序,before、after用于维护Entry插入的先后顺序
参考:https://blog.csdn.net/c99463904/article/details/77619826
参考:https://mp.weixin.qq.com/s/DTr0vHtSk9n_QF7vfXZ7qQ
参考:https://blog.csdn.net/chen213wb/article/details/84647179
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。