Java集合(7):HashMap

一.HashMap介绍

  HashMap是基于哈希表的 Map 接口的实现,以key-value的形式存在。在HashMap中,key-value总是会当做一个整体来处理,系统会根据hash算法来来计算key-value的存储位置,我们总是可以通过key快速地存、取value。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。HashMap不保证映射的顺序,特别是它不保证该顺序恒久不变。

  HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。

Map map = Collections.synchronizedMap(new HashMap());

  除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。

1.HashMap的继承关系

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

2.HashMap的类图关系

  HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作(其实AbstractMap类已经实现了Map)。

二.HashMap源码解析

1.数据结构

  在Java中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两种来组合实现,HashMap也是如此。实际上HashMap是一个“链表散列”,它的底层实现还是数组,只是数组的每一项都是一条链。如下是它数据结构:

  最左侧的部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。

HashMap中Entry类的代码如下:

 1 /** Entry是单向链表。
 2 * 它是 “HashMap链式存储法”对应的链表。
 3 *它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数
 4 **/
 5 static class Entry<K,V> implements Map.Entry<K,V> {
 6     final K key;
 7     V value;
 8     // 指向下一个节点
 9     Entry<K,V> next;
10     final int hash;
11
12     // 构造函数。
13     // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
14     Entry(int h, K k, V v, Entry<K,V> n) {
15         value = v;
16         next = n;
17         key = k;
18         hash = h;
19     }
20
21     public final K getKey() {
22         return key;
23     }
24
25     public final V getValue() {
26         return value;
27     }
28
29     public final V setValue(V newValue) {
30         V oldValue = value;
31         value = newValue;
32         return oldValue;
33     }
34
35     // 判断两个Entry是否相等
36     // 若两个Entry的“key”和“value”都相等,则返回true。
37     // 否则,返回false
38     public final boolean equals(Object o) {
39         if (!(o instanceof Map.Entry))
40             return false;
41         Map.Entry e = (Map.Entry)o;
42         Object k1 = getKey();
43         Object k2 = e.getKey();
44         if (k1 == k2 || (k1 != null && k1.equals(k2))) {
45             Object v1 = getValue();
46             Object v2 = e.getValue();
47             if (v1 == v2 || (v1 != null && v1.equals(v2)))
48                 return true;
49         }
50         return false;
51     }
52
53     // 实现hashCode()
54     public final int hashCode() {
55         return (key==null   ? 0 : key.hashCode()) ^
56                (value==null ? 0 : value.hashCode());
57     }
58
59     public final String toString() {
60         return getKey() + "=" + getValue();
61     }
62
63     // 当向HashMap中添加元素时,绘调用recordAccess()。
64     // 这里不做任何处理
65     void recordAccess(HashMap<K,V> m) {
66     }
67
68     // 当从HashMap中删除元素时,绘调用recordRemoval()。
69     // 这里不做任何处理
70     void recordRemoval(HashMap<K,V> m) {
71     }
72 }

  HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。

2.HashMap关键属性

1 transient Entry[] table;//存储元素的实体数组
2 transient int size;//存放元素的个数
3 int threshold; //临界值。当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
4 final float loadFactor; //加载因子
5 transient int modCount;//被修改的次数

其中loadFactor加载因子是表示Hsah表中元素的填满的程度.

若加载因子越大,填满的元素越多,好处是空间利用率高了,但冲突的机会加大了。链表长度会越来越长,查找效率降低。

反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了。表中的数据将过于稀疏(很多空间还没用,就开始扩容了)

冲突的机会越大,则查找的成本越高。必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。

3.HashMap的构造方法

 1 /**
 2  *使用默认的容量及装载因子构造一个空的HashMap
 3  */
 4 public HashMap() {
 5     this.loadFactor = DEFAULT_LOAD_FACTOR;
 6     threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//计算下次需要调整大小的极限值
 7     table = new Entry[DEFAULT_INITIAL_CAPACITY];//根据默认容量(16)初始化table
 8     init();
 9 }
10 /**
11  * 根据给定的初始容量的装载因子创建一个空的HashMap
12  * 初始容量小于0或装载因子小于等于0将报异常
13  */
14 public HashMap(int initialCapacity, float loadFactor) {
15     if (initialCapacity < 0)
16         throw new IllegalArgumentException("Illegal initial capacity: " +
17                                            initialCapacity);
18     if (initialCapacity > MAXIMUM_CAPACITY)//调整最大容量
19         initialCapacity = MAXIMUM_CAPACITY;
20     if (loadFactor <= 0 || Float.isNaN(loadFactor))
21         throw new IllegalArgumentException("Illegal load factor: " +
22                                            loadFactor);
23     int capacity = 1;//初始容量
24     //设置capacity为大于initialCapacity且是2的幂的最小值
25     while (capacity < initialCapacity)
26         capacity <<= 1;
27     this.loadFactor = loadFactor;
28     threshold = (int)(capacity * loadFactor);
29     table = new Entry[capacity];
30     init();
31 }
32 /**
33  *根据指定容量创建一个空的HashMap
34  */
35 public HashMap(int initialCapacity) {
36     this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上面的构造方法,容量为指定的容量,装载因子是默认值
37 }
38 /**
39  *通过传入的map创建一个HashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值
40  */
41 public HashMap(Map<? extends K, ? extends V> m) {
42     this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
43                   DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
44     putAllForCreate(m);
45 }

  在构造HashMap的时候如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则的话就是用默认的。默认初始容量为16,默认加载因子为0.75。我们可以看到上面代码中13-15行,这段代码的作用是确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂。

4.存储数据put

 1 public V put(K key, V value) {
 2      // 若“key为null”,则将该键值对添加到table[0]中。
 3          if (key == null)
 4             return putForNullKey(value);
 5      // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
 6          int hash = hash(key.hashCode());
 7      //搜索指定hash值在对应table中的索引
 8          int i = indexFor(hash, table.length);
 9      // 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
10          for (Entry<K,V> e = table[i]; e != null; e = e.next) {
11               Object k;
12               if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值
13                  V oldValue = e.value;
14                  e.value = value;
15                  e.recordAccess(this);
16                  return oldValue;
17               }
18          }
19      //修改次数+1
20      modCount++;
21      //将key-value添加到table[i]处
22      addEntry(hash, key, value, i);
23      return null;
24 }

  Map 集合中的 value 是 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

  下面具体解析put函数里面用到的其他的函数。

4.1 putForNullKey()

第3和4行的作用就是处理key值为null时用到的putForNullKey(value)方法如下:

 1 private V putForNullKey(V value) {
 2     for (Entry<K,V> e = table[0]; e != null; e = e.next) {
 3         if (e.key == null) {   //如果有key为null的对象存在,则覆盖掉
 4             V oldValue = e.value;
 5             e.value = value;
 6             e.recordAccess(this);
 7             return oldValue;
 8        }
 9    }
10     modCount++;
11     addEntry(0, null, value, 0); //如果键为null的话,则hash值为0
12     return null;
13 }

注意:如果key为null的话,hash值为0,对象存储在数组中索引为0的位置,即table[0]。

4.2 hash()

put方法中第6行通过key的hashCode值计算hash的的函数hash(int h)如下:

1 //计算hash值的方法 通过键的hashCode来计算
2 static int hash(int h) {
3     h ^= (h >>> 20) ^ (h >>> 12);
4     return h ^ (h >>> 7) ^ (h >>> 4);
5 }
4.3 indexFor()

put方法中第8行通过hash码去计算数组中存储的索引值的函数indexFor(int h, int length)如下:

1 static int indexFor(int h, int length) { //根据hash值和数组长度算出索引值
2     return h & (length-1);  //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出
3 }

  一般对哈希表的散列进行取模是用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低。HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。

下面我们分析下为什么哈希表的容量一定要是2的整数次幂

  首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

下面通过流程图来梳理一下整个put过程:

下面是两个例子。

例1:假设length为16(2^n)和15,h为5、6、7。如下图:

当n=15时,6和7的结果一样,这样表示他们在table存储的位置是相同的,也就是产生了碰撞,6、7就会在一个位置形成链表,这样就会导致查询速度降低。

例2:假设length为15,h为0-15。如下图:

  上图中共发生了8此碰撞,同时发现浪费的空间非常大,有1、3、5、7、9、11、13、15处没有记录,也就是没有存放数据。这是因为他们在与14进行&运算时,得到的结果最后一位永远都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的,空间减少,进一步增加碰撞几率,这样就会导致查询速度慢。而当length = 16时,length – 1 = 15 即1111,那么进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

4.4 addEntry()

  当我们想一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。具体的实现过程见addEntry方法如下:

1 void addEntry(int hash, K key, V value, int bucketIndex) {
2    Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点
3    table[bucketIndex] = new Entry<>(hash, key, value, e);//将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
4    if (size++ >= threshold) //如果大于临界值就扩容
5        resize(2 * table.length); //以2的倍数扩容
6 }

注意:

1.是链的产生。

  这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

2.扩容问题。

  随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

5.数据读取get()

  HashMap读取数据相对简单一些,通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。

 1 public V get(Object key) {
 2     if (key == null)// 若为null,调用getForNullKey方法返回相对应的value
 3         return getForNullKey();
 4     int hash = hash(key.hashCode());// 根据该 key 的 hashCode 值计算它的 hash 码
 5     for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { // 取出 table 数组中指定索引处的值
 6         Object k;
 7         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
 8             return e.value;  //若搜索的key与查找的key相同,则返回相对应的value
 9     }
10     return null;
11 }

在存储的过程中,系统根据key的hashcode来决定Entry在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象。

6.其他方法

6.1 containsKey、getEntry

public boolean containsKey(Object key)

final Entry<K,V> getEntry(Object key)

containsKey调用了getEntry方法,根据getEntry的结果是否为null进行返回,是则返回false,否返回true。

6.2 remove

public V remove(Object key)

6.3 clear()

public void clear()

6.4 使用到迭代器Iterator的方法

entrySet()、keySet()、values()

参考:http://cmsblogs.com/?p=176

http://www.cnblogs.com/ITtangtang/p/3948406.html

http://blog.csdn.net/ghsau/article/details/16843543/

http://www.cnblogs.com/hzmark/archive/2012/12/24/HashMap.html

时间: 2024-11-05 14:36:58

Java集合(7):HashMap的相关文章

Java集合:HashMap源码剖析

一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调整大小 5.数据读取                       6.HashMap的性能参数                      7.Fail-Fast机制 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null

Java 集合学习--HashMap

一.HashMap 定义 HashMap 是一个基于散列表(哈希表)实现的键值对集合,每个元素都是key-value对,jdk1.8后,底层数据结构涉及到了数组.链表以及红黑树.目的进一步的优化HashMap的性能和效率.允许key和value为NULL,同样非线程安全. ①.继承AbstractMap抽象类,AbstractMap实现Map接口,实现部分方法的.同样在上面HashMap的结构中,HashMap同样实现了Map接口,这样做是否有什么深层次的用意呢?网上查阅资料发现,这种写法只是一

死磕 java集合之HashMap源码分析

欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改的速度都很快,能达到O(1)的平均时间复杂度.它是非线程安全的,且不保证元素存储的顺序: 继承体系 HashMap实现了Cloneable,可以被克隆. HashMap实现了Serializable,可以被序列化. HashMap继承自AbstractMap,实现了Map接口,具有Map的所有功能. 存储结构

Java集合系列-HashMap 1.8(一)

一.概述 HashMap是基于哈希实现的映射集合. HashMap可以拥有null键和null值,但是null键只能有一个,null值不做限制.HashTable是不允许null键和值的. HashMap是非线程安全的集合,HashTable是添加了同步功能的HashMap,是线程安全的. HashMap是无序的,并不能保证其内部键值对的顺序. HashMap提供了常量级复杂度的元素获取和添加操作(当然是在hash分散均匀的情况下). HashMap有两个影响功能的因素:初始容量与负载因子,当集

【Java集合】——HashMap源码分析

简介 HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改的速度都很快,能达到O(1)的平均时间复杂度.它是非线程安全的,且不保证元素存储的顺序: 继承体系 分析: HashMap实现了Cloneable,可以被克隆. HashMap实现了Serializable,可以被序列化. HashMap继承自AbstractMap,实现了Map接口,具有Map的所有功能. 存储结构 在Java中,HashMap的实现采用了(数组 + 链表 + 红黑树)的复杂结构,数组

JAVA集合------Map (HashMap实现)

package java_util_map; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; public class MapTest01 { public static void main(String[] args) { /*  * Map是一个接口,HashMap是Map的一个实现类  

【JAVA集合】HashMap源码分析(转载)

原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储的对象是一个键值对对象(Entry<K,V>): HashMap补充说明 基于数组和链表实现,内部维护着一个数组table,该数组保存着每个链表的表头结点:查找时,先通过hash函数计算hash值,再根据hash值计算数组索引,然后根据索引找到链表表头结点,然后遍历查找该链表: HashMap数据

java集合之hashmap

第1部分 HashMap介绍 HashMap简介 HashMap 的实现不是同步的,这意味着它不是线程安全的.它的key.value都可以为null.此外,HashMap中的映射不是有序的. HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”.容量是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量.加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度.当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构

Java集合之HashMap源码分析

一.HashMap简介 HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射.此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能. ps:本文中的源码来自jdk1.8.0_45/src. 1.重要参数 HashMap的实例有两个参数影响其性能. 初始容量:哈希表中桶的数量 加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度 当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的

Java集合之HashMap源码实现分析...

1.简介 通过上面的一篇随笔我们知道了HashSet的底层是采用Map实现的,那么Map是什么?它的底层又是如何实现的呢?这下我们来分析下源码,看看具体的结构与实现.Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值.Map.Entry是其的内部类,描述Map中的按键/数值对.需要指出的是Map,允许null的键也允许null的值.它的实现主要有HashMap和sortedMap,其中SortedMap扩展了Map使按键保持升序排列,下面我们简要分析下HashMap的具体