HashMap底层实现(源码分析)

一、数据结构

Map将实际数据存储在Entry类的数组中。

代码片段:

Java代码
 
 

  1. transient Entry[] table;//HashMap的成员变量,存放数据
  2. static class Entry<K,V> implements Map.Entry<K,V> {//内部类Entry
  3. final K key;
  4. V value;
  5. Entry<K,V> next;//指向下一个数据
  6. final int hash;
  7. /**
  8. * Creates new entry.
  9. */
  10. Entry(int h, K k, V v, Entry<K,V> n) {
  11. value = v;
  12. next = n;
  13. key = k;
  14. hash = h;
  15. }

执行下面代码后,可能的存储内部结构是:

Map map = new HashMap();

map.put("key1","value1");

map.put("key2","value2");

map.put("key3","value3");

执行put方法时根据key的hash值来计算放到table数组的下标,如果hash到相同的下标,则新put进去的元素放到Entry链的头部,如上图所示。put方法的源码后面详细解释。

二、属性和构造方法

Java代码
 
 

  1. static final int DEFAULT_INITIAL_CAPACITY = 16;//默认的初始大小,如果执行无参的构造方法,则默认初始大小为16
  2. static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量,1073741824
  3. static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的负载因子,如果没有通过构造方法传入负载因子,则使用0.75。
  4. transient Entry[] table; //存放具体键值对的Entry数组
  5. transient int size; //HashMap的大小
  6. int threshold;//阀值  threshold = (int)(capacity * loadFactor); 即容量*负载因子,执行put方法时如果size大于threshold则进行扩容,后面put方法将会看到
  7. final float loadFactor; //用户设置的负载因子
  8. transient volatile int modCount;//HashMap实例被改变的次数,这个同ArrayList

构造方法一、

Java代码
 
 

  1. public HashMap() {
  2. this.loadFactor = DEFAULT_LOAD_FACTOR;
  3. threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
  4. table = new Entry[DEFAULT_INITIAL_CAPACITY];
  5. init();
  6. }

使用了默认的容量和默认的负载因子。

构造方法二、

Java代码
 
 

  1. public HashMap(int initialCapacity) {
  2. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  3. }

使用了用户设置的初始容量和默认的负载因子。

构造方法三、

Java代码
 
 

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. if (initialCapacity < 0)
  3. throw new IllegalArgumentException("Illegal initial capacity: " +
  4. initialCapacity);
  5. if (initialCapacity > MAXIMUM_CAPACITY)
  6. initialCapacity = MAXIMUM_CAPACITY;
  7. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8. throw new IllegalArgumentException("Illegal load factor: " +
  9. loadFactor);
  10. // Find a power of 2 >= initialCapacity
  11. int capacity = 1;
  12. while (capacity < initialCapacity)
  13. capacity <<= 1;
  14. this.loadFactor = loadFactor;
  15. threshold = (int)(capacity * loadFactor);
  16. table = new Entry[capacity];
  17. init();
  18. }

用户传入了初始容量和负载因子,这两个值是HashMap性能优化的关键,涉及到了HashMap的扩容问题。

HashMap的容量永远是2的倍数,如果传入的不是2的倍数则被调整为大于传入值的最近的2的倍数,例如如果传入130,则capacity计算后是256。是这段代码起的作用:

Java代码
 
 

  1. while (capacity < initialCapacity)
  2. capacity <<= 1;

构造方法四、

Java代码
 
 

  1. public HashMap(Map<? extends K, ? extends V> m) {
  2. this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
  3. DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);//计算Map的大小
  4. putAllForCreate(m);//初始化
  5. }
  6. private void putAllForCreate(Map<? extends K, ? extends V> m) {
  7. for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {//通过entryset进行遍历
  8. Map.Entry<? extends K, ? extends V> e = i.next();
  9. putForCreate(e.getKey(), e.getValue());
  10. }
  11. }

根据传入的map进行初始化。

三、关键方法

1)put

Java代码
 
 

  1. public V put(K key, V value) {
  2. if (key == null)
  3. return putForNullKey(value);//单独处理,总是放到table[0]中
  4. int hash = hash(key.hashCode());//计算key的hash值,后面介绍性能的时候再说这个hash方法。
  5. int i = indexFor(hash, table.length);//将hash和length-1取&来得到数组的下表
  6. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  7. Object k;
  8. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  9. V oldValue = e.value;
  10. e.value = value;
  11. e.recordAccess(this);
  12. return oldValue;
  13. }
  14. }//如果这个key值,原来已经则替换后直接返回。
  15. modCount++;
  16. addEntry(hash, key, value, i);
  17. return null;
  18. }
  19. void addEntry(int hash, K key, V value, int bucketIndex) {
  20. Entry<K,V> e = table[bucketIndex];
  21. table[bucketIndex] = new Entry<K,V>(hash, key, value, e);//如果table[bucketIndex]中已经存在Entry则放到头部。
  22. if (size++ >= threshold)//如果大于了阀值,则扩容到原来大小的2倍。
  23. resize(2 * table.length);
  24. }
  25. void resize(int newCapacity) {
  26. Entry[] oldTable = table;
  27. int oldCapacity = oldTable.length;
  28. if (oldCapacity == MAXIMUM_CAPACITY) {
  29. threshold = Integer.MAX_VALUE;
  30. return;
  31. }
  32. Entry[] newTable = new Entry[newCapacity];
  33. transfer(newTable);//赋值到新的table中,注意转移后会重新hash,所以位置可能会跟之前不同,目的是均匀分不到新的table中。
  34. table = newTable;
  35. threshold = (int)(newCapacity * loadFactor);
  36. }

2)get方法

Java代码
 
 

  1. public V get(Object key) {
  2. if (key == null)
  3. return getForNullKey();
  4. int hash = hash(key.hashCode());
  5. for (Entry<K,V> e = table[indexFor(hash, table.length)];//找到数组的下表,进行遍历
  6. e != null;
  7. e = e.next) {
  8. Object k;
  9. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
  10. return e.value;//找到则返回
  11. }
  12. return null;//否则,返回null
  13. }

3)remove方法

Java代码
 
 

  1. public V remove(Object key) {
  2. Entry<K,V> e = removeEntryForKey(key);
  3. return (e == null ? null : e.value);
  4. }
  5. final Entry<K,V> removeEntryForKey(Object key) {
  6. int hash = (key == null) ? 0 : hash(key.hashCode());
  7. int i = indexFor(hash, table.length);
  8. Entry<K,V> prev = table[i];
  9. Entry<K,V> e = prev;
  10. while (e != null) {//Entry链未遍历完则一直遍历
  11. Entry<K,V> next = e.next;
  12. Object k;
  13. if (e.hash == hash &&
  14. ((k = e.key) == key || (key != null && key.equals(k)))) {
  15. modCount++;
  16. size--;
  17. if (prev == e)//如果是第一个,则将table[i]执行e.next
  18. table[i] = next;
  19. else //否则将前一个的next指向e.next
  20. prev.next = next;
  21. e.recordRemoval(this);
  22. return e;
  23. }
  24. prev = e;//未找到则继续往后遍历
  25. e = next;
  26. }
  27. return e;
  28. }

4)HashMap的遍历方法

Java代码
 
 

  1. Map map = new HashMap();
  2. map.put("key1","value1");
  3. map.put("key2","value2");
  4. map.put("key3", "value3");
  5. for(Iterator it = map.entrySet().iterator();it.hasNext();){
  6. Map.Entry e = (Map.Entry)it.next();
  7. System.out.println(e.getKey() + "=" + e.getValue());
  8. }
  9. System.out.println("-----------------------------------------");
  10. for(Iterator it = map.keySet().iterator();it.hasNext();){
  11. Object key = it.next();
  12. Object value = map.get(key);
  13. System.out.println(key+"="+value);
  14. }
  15. System.out.println("-----------------------------------------");
  16. System.out.println(map.values());
  17. 输出为:
  18. key3=value3
  19. key2=value2
  20. key1=value1
  21. -----------------------------------------
  22. key3=value3
  23. key2=value2
  24. key1=value1
  25. -----------------------------------------
  26. [value3, value2, value1]

四、性能相关

1)hash

如果总计算到相同的数组下标,则得进行Entry的遍历来取值和存放值,必然会影响性能。

所以HashMap提供了hash方法,来解决key的hashCode方法质量不高问题。

Java代码
 
 

  1. public V put(K key, V value) {
  2. ...
  3. int hash = hash(key.hashCode());
  4. ...
  5. }
  6. static int hash(int h) {
  7. // This function ensures that hashCodes that differ only by
  8. // constant multiples at each bit position have a bounded
  9. // number of collisions (approximately 8 at default load factor).
  10. h ^= (h >>> 20) ^ (h >>> 12);
  11. return h ^ (h >>> 7) ^ (h >>> 4);
  12. }

2)初始容量和负载因子

因为put的时候可能需要做扩容,扩容会导致性能损耗,所以如果可以预知Map大小的话,可以设置合理的初始大小和负载因子来避免HashMap的频繁扩容导致的性能消耗。

Java代码
 
 

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. ntry<K,V> e = table[bucketIndex];
  3. table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
  4. if (size++ >= threshold)
  5. resize(2 * table.length);
  6. }

五、同Hashtable的区别

1)HashMap允许key和value都可以为null,Hashtable都不可以为空。

HashMap的put方法,代码片段:

Java代码
 
 

  1. if (key == null)
  2. return putForNullKey(value);

Hashtable的put方法,代码片段:

Java代码
 
 

  1. if (value == null) {
  2. throw new NullPointerException();
  3. }
  4. Entry tab[] = table;
  5. int hash = key.hashCode();

2)HashMap是非线程安全的,Hashtable是线程安全的。

Hashtable的put和get方法均为synchronized的。

六、ConcurrentHashMap

ConcurrentHashMap是Doug Lea写的线程安全的HashMap实现,将HashMap默认划分为了16个Segment,减少了锁的争用,另外通过写时加锁读时不加锁减少了锁的持有时间,优雅的解决了高并发下锁的高竞争问题。感兴趣的可参见笔者的另一篇博客
http://frank1234.iteye.com/blog/2162490

时间: 2024-08-24 07:18:44

HashMap底层实现(源码分析)的相关文章

HashMap与TreeMap源码分析

1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Java这么久,也写过一些小项目,也使用过TreeMap无数次,但到现在才明白它的实现原理).因此本着"不要重复造轮子"的思想,就用这篇博客来记录分析TreeMap源码的过程,也顺便瞅一瞅HashMap. 2. 继承结构 (1) 继承结构 下面是HashMap与TreeMap的继承结构: pu

JDK1.8 HashMap中put源码分析

一.存储结构      在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构.它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置.HashMap通过key的hashCode来计算hash值,不同的hash值就存在数组中不同的位置,当多个元素的hash值相同时(所谓hash冲突),就采用链表将它们串联起来(链表解决冲突),放置在该hash值所对应的数组位置上.结构图如下:     图中,紫色部分代表哈希表,也称为哈希数组,数组中每个元素

面试(1)-HashMap原理与源码分析(JDK1.8)

1.HashMap介绍 HashMap为Map接口的一个实现类,实现了Map所有的操作.HashMap除了允许key.value为null值和非线程安全外,其他实现几乎和HashTable一致.HashMap使用散列存储的方式保存kay-value键值对,因此其不支持数据保存的顺序.如果想要使用有序容器可以使用LinkedHashMap.在性能上当HashMap中保存的key的哈希算法能够均匀的分布在每个bucket中的时候,HashMap在基本的get和set操作的的时间复杂度都是O(n).在

jdk8 hashmap 链表resize 源码分析

重点看这部分代码 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, o

《Java源码分析》:Vector

<Java源码分析>:Vector 虽然,Vector集合在我们的编程中,使用的比较少,至少我使用的比较少,一般情况下,我都是倾向于使用List来存储一些同类型的元素. 其实,Vector的内部实现和ArrayList的内部实现基本一致,内部都是借助于数组来实现的.下面就一起来分析下. HashMap类的源码分析,博客在这里:http://blog.csdn.net/u010412719/article/details/51980632 Hashtable类的源码分析,博客在这里:http:/

[Java] HashMap源码分析

1.概述 Hashmap继承于AbstractMap,实现了Map.Cloneable.java.io.Serializable接口.它的key.value都可以为null,映射不是有序的. Hashmap不是同步的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Collections.synchronizedMap(new HashMap()); (除了不同步和允许使用 null 之

HashMap源码分析(转载)

一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Coll

从HashMap到LrcCache的源码分析

打算研究android的一个图片加载库Android-Universal-Image-Loader,然后就看到了缓存的策略,于是又看到了LruCache,是一个最近最少使用算法LRU.前几天看操作系统也看到了LRU算法,是用在缺页中断发生时,进行置换算法才用的一种.缓存中的LrcCache和操作系统中的页置换算法思想是一样的,于是心血来潮,决定把这部分实现看看,然后就有了这篇博客,从HashMap的实现到LinkedHashMap再到LruCache,总共包含三个类的源码分析,花费了整整一晚上.

java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制

通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的引用放入数组中,每一个数组元素都是一个引用变量. 实际上,HashSet 和 HashMap 之间有非常多相似之处,对于 HashSet 而言.系统採用 Hash 算法决定集合元素的存储位置,这样能够保证能高速存.取集合元素:对于 HashMap 而言.系统 key-value 当成一个总体进行处理