它是基于哈希表的 Map 接口的实现,HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则(一个map不能包含重复的key,每个key最多只能对应一个value,HashMap最多只允许一条记录的键为null,允许多条记录的值为null。),而AbstractMap类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实AbstractMap类已经实现了Map。
默认容量(哈希表桶的数量)和装载因子是16和0.75,也就是元素超过16*0.75时,会再哈希,每次在哈希默认容量乘以2,容量的最大值是2的30次方。(不对)Node
若存在则覆盖原来key的value,否则将该元素保存在链头,后入桶的Entry将next指向桶当前的Entry(最先保存的元素放在链尾)
HashMap 在并发时可能出现的问题主要是两方面,首先如果多个线程同时使用put方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程的 put 的数据被覆盖。第二就是如果多个线程同时检测到元素个数超过数组大小* loadFactor ,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失。
关于 HashMap 线程不安全这一点,《Java并发编程的艺术》一书中是这样说的:
HashMap 在并发执行 put 操作时会引起死循环,导致 CPU 利用率接近100%。因为多线程会导致 HashMap 的 Node 链表形成环形数据结构,一旦形成环形数据结构,Node 的 next 节点永远不为空,就会在获取 Node 时产生死循环。
google 了一下,才知道死循环并不是发生在 put 操作时,而是发生在扩容时。详细的解释可以看下面几篇博客:
• 酷壳-Java HashMap的死循环
• HashMap在java并发中如何发生死循环
• How does a HashMap work in JAVA
在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,具体证明可以参考http://blog.csdn.net/liuqiyao_01/article/details/14475159,HashMap采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
对于keySet其实是遍历了2次,一次是转为iterator,一次就从hashmap中取出key所对于的value。而entryset只是遍历了第一次,他把key和value都放到了entry中,所以就快了。
1。7的hash方法是纯数学计算,在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。取模用位运算(hash & (arrayLength-1))会比较快
这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
rehash的判断条件实际上就是当前的 数据量是否达到了当前容量乘以负载因子,如果达到了,只要再新加数据就会触发rehash。
如果当前容量已经达到了最大用量 ,则不扩充,直接更新threshold的值为int的最大值MAX_VALUE = 0x7fffffff;(2的31次幂减一),否则就扩充容量为当前容量的两倍。
主要在代码的transfer方法来完成的,实际上就是一次取出原始table中的数据,计算他们在newTable的 位置 ,插入即可。
http://www.xiaomager.com/299.html
jdk8以前:我们可以看出Entry是一个单向链表,这也是我们为什么说HashMap是通过拉链法解决hash冲突的。
jdk8后:不再使用链表,改用了红黑树,提高了查找效率。
jdk1.8中,hash map增加了比较的接口 在Map接口的Entry接口增加了 comparingByKey comparingByValue
在Java 8之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。因此在Java 8中,bucket中碰撞冲突的元素超过某个限制(默认是8),利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题.
这一动态的特性使得HashMap一开始使用链表,并在冲突的元素数量超过指定值(8)时用平衡二叉树替换链表。不过这一特性在所有基于hash table的类中并没有,例如Hashtable和WeakHashMap。
目前,只有ConcurrentHashMap,LinkedHashMap和HashMap会在频繁冲突的情况下使用平衡树。
Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。LinkedHashMap=散列表+循环双向链表 见:http://www.cnblogs.com/chenpi/p/5294077.html
TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
TreeMap 本质是红黑树,树节点 Entry
hashset底层使用的是hashmap
【参考】
Java HashMap工作原理及实现 :http://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/
美团:Java 8系列之重新认识HashMap:http://tech.meituan.com/java-hashmap.html
疫苗:Java HashMap的死循环:http://coolshell.cn/articles/9606.html
HashMap多线程死循环问题:http://blog.csdn.net/xuefeng0707/article/details/40797085
如何线程安全的使用 HashMap http://yemengying.com/2016/05/07/threadsafe-hashmap/