hashmap源码剖析

来源:互联网 发布:java防止接口频繁调用 编辑:程序博客网 时间:2024/05/16 01:23

一、HashMap概述
二、HashMap的数据结构
三、HashMap源码分析
1、关键属性
2、构造方法
3、存储数据
4、调整大小

 5、数据读取                  6、HashMap的性能参数                  7、Fail-Fast机制

一、HashMap概述

  HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

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

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

二、HashMap的数据结构

  HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同学都知道,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。

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

我们看看HashMap中Entry类的代码:

复制代码

 /** Entry是单向链表。         * 它是 “HashMap链式存储法”对应的链表。         *它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数      **/      static class Entry<K,V> implements Map.Entry<K,V> {            final K key;            V value;            // 指向下一个节点            Entry<K,V> next;            final int hash;            // 构造函数。            // 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"            Entry(int h, K k, V v, Entry<K,V> n) {                value = v;                next = n;                key = k;                hash = h;            }            public final K getKey() {                return key;            }            public final V getValue() {                return value;            }            public final V setValue(V newValue) {                V oldValue = value;                value = newValue;                return oldValue;            }            // 判断两个Entry是否相等            // 若两个Entry的“key”和“value”都相等,则返回true。            // 否则,返回false            public final boolean equals(Object o) {                if (!(o instanceof Map.Entry))                    return false;                Map.Entry e = (Map.Entry)o;                Object k1 = getKey();                Object k2 = e.getKey();                if (k1 == k2 || (k1 != null && k1.equals(k2))) {                    Object v1 = getValue();                    Object v2 = e.getValue();                    if (v1 == v2 || (v1 != null && v1.equals(v2)))                        return true;                }                return false;            }            // 实现hashCode()            public final int hashCode() {                return (key==null   ? 0 : key.hashCode()) ^                       (value==null ? 0 : value.hashCode());            }            public final String toString() {                return getKey() + "=" + getValue();            }            // 当向HashMap中添加元素时,绘调用recordAccess()。            // 这里不做任何处理            void recordAccess(HashMap<K,V> m) {            }            // 当从HashMap中删除元素时,绘调用recordRemoval()。            // 这里不做任何处理            void recordRemoval(HashMap<K,V> m) {            }        }

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

三、HashMap源码分析

   1、关键属性

  先看看HashMap类中的一些关键属性:

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

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

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

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

冲突的机会越大,则查找的成本越高.

因此,必须在 “冲突的机会”与”空间利用率”之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的”时-空”矛盾的平衡与折衷.

  如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。

2、构造方法

下面看看HashMap的几个构造方法:

复public HashMap(int initialCapacity, float loadFactor) {
2 //确保数字合法
3 if (initialCapacity < 0)
4 throw new IllegalArgumentException(“Illegal initial capacity: ” +
5 initialCapacity);
6 if (initialCapacity > MAXIMUM_CAPACITY)
7 initialCapacity = MAXIMUM_CAPACITY;
8 if (loadFactor <= 0 || Float.isNaN(loadFactor))
9 throw new IllegalArgumentException(“Illegal load factor: ” +
10 loadFactor);
11
12 // Find a power of 2 >= initialCapacity
13 int capacity = 1; //初始容量
14 while (capacity < initialCapacity) //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
15 capacity <<= 1;
16
17 this.loadFactor = loadFactor;
18 threshold = (int)(capacity * loadFactor);
19 table = new Entry[capacity];
20 init();
21 }
22
23 public HashMap(int initialCapacity) {
24 this(initialCapacity, DEFAULT_LOAD_FACTOR);
25 }
26
27 public HashMap() {
28 this.loadFactor = DEFAULT_LOAD_FACTOR;
29 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
30 table = new Entry[DEFAULT_INITIAL_CAPACITY];
31 init();
32 }码

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

重点分析下HashMap中用的最多的两个方法put和get

   3、存储数据

  下面看看HashMap存储数据的过程是怎样的,首先看看HashMap的put方法:

复public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
//搜索指定hash值在对应table中的索引
int i = indexFor(hash, table.length);
// 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
for (Entry

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);4         if (size++ >= threshold) //如果大于临界值就扩容5             resize(2 * table.length); //以2的倍数扩容6     }

参数bucketIndex就是indexFor函数计算出来的索引值,第2行代码是取得数组中索引为bucketIndex的Entry对象,第3行就是用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。

  第4行和第5行就是判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容,HashMap扩容是扩为原来的两倍。

4、调整大小

resize()方法如下:

重新调整HashMap的大小,newCapacity是调整后的单位
复制代码

 1     void resize(int newCapacity) { 2         Entry[] oldTable = table; 3         int oldCapacity = oldTable.length; 4         if (oldCapacity == MAXIMUM_CAPACITY) { 5             threshold = Integer.MAX_VALUE; 6             return; 7        } 8  9         Entry[] newTable = new Entry[newCapacity];10         transfer(newTable);//用来将原先table的元素全部移到newTable里面11         table = newTable;  //再将newTable赋值给table12         threshold = (int)(newCapacity * loadFactor);//重新计算临界值13     }

复制代码

新建了一个HashMap的底层数组,上面代码中第10行为调用transfer方法,将HashMap的全部元素添加到新的HashMap中,并重新计算元素在新的数组中的索引位置

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

5、数据读取

复制代码

1.public V get(Object key) {
2. if (key == null)
3. return getForNullKey();
4. int hash = hash(key.hashCode());
5. for (Entry

1 0
原创粉丝点击