1.HashMap-1.8介绍

HashMap为Map接口的一个实现类,实现了所有Map的操作。HashMap除了允许key和value保存null值和非线程安全外,其他实现几乎和HashTable一致。

HashMap使用散列存储的方式保存kay-value键值对,因此其不支持数据保存的顺序。如果想要使用有序容器可以使用LinkedHashMap。

在性能上当HashMap中保存的key的哈希算法能够均匀的分布在每个bucket中的是时候,HashMap在基本的get和set操作的的时间复杂度都是O(n)。

在遍历HashMap的时候,其遍历节点的个数为bucket的个数+HashMap中保存的节点个数。因此当遍历操作比较频繁的时候需要注意HashMap的初始化容量不应该太大。 

这一点其实比较好理解:当保存的节点个数一致的时候,bucket越少,遍历次数越少。

另外HashMap在resize的时候会有很大的性能消耗,因此当需要在保存HashMap中保存大量数据的时候,传入适当的默认容量以避免resize可以很大的提高性能。 

具体的resize操作请参考下面对此方法的分析

HashMap是非线程安全的类,当作为共享可变资源使用的时候会出现线程安全问题。需要使用线程安全容器:

Map m = new ConcurrentHashMap();或者Map m = Collections.synchronizedMap(new HashMap());

具体的HashMap会出现的线程安全问题分析请参考9中的分析。

2.数据结构介绍

HashMap使用数组+链表+树形结构的数据结构。其结构图如下所示。

 

HashMap android 封装 hashmap oom_数组

3.HashMap源码分析(基于JDK1.8)

3.1关键属性分析

  transient Node<K,V>[] table;  

    Node类型的数组,记我们常说的bucket数组,其中每个元素为链表或者树形结构

  transient Set<Map.Entry<K,V>> entrySet;

    保存缓存的entrySet()

  transient int size;

    HashMap中保存的数据个数

  transient int modCount;

    此哈希映射在结构上被修改的次数

  int threshold;

    HashMap需要resize操作的阈值

  final float loadFactor;

    负载因子,用于计算threshold。计算公式为:threshold = loadFactor * capacity

  备注:有默认容量capacity  2^4 = 16,默认扩容负载因子loadFactor=0.75等.用于构造函数没有指定数值情况下的默认值。

3.2Node分析

他是Hashmap的内部类,存储key,value键值对

1.4个参数

常量

常量

  Node<K,V> next;

2.只有有参构造

3.key被final修饰-不能修改-只能创建的时候赋值

4.重写equals方法:key和value都相等(地址值相等)返回true

static class Node<K,V> implements Map.Entry<K,V> {

        final int hash; //final修饰-常量-不可变

        final K key; //final修饰-常量-不可变

        V value;

        Node<K,V> next;  //下一个元素,形成链表

        Node(int hash, K key, V value, Node<K,V> next) {//只有有参构造没有无参构造

            this.hash = hash;

            this.key = key;

            this.value = value;

            this.next = next;

        }

 

        public final K getKey()        { return key; }

        public final V getValue()      { return value; }

        public final String toString() { return key + "=" + value; }

 

        public final int hashCode() {

            return Objects.hashCode(key) ^ Objects.hashCode(value);

        }


        public final V setValue(V newValue) { //value设值:旧值新值被覆盖且返回旧值

            V oldValue = value;

            value = newValue;

            return oldValue;

        }

 

        public final boolean equals(Object o) {//key和value相等(地址值)

            if (o == this)

                return true;

            if (o instanceof Map.Entry) {

                Map.Entry<?,?> e = (Map.Entry<?,?>)o;

                if (Objects.equals(key, e.getKey()) &&

                    Objects.equals(value, e.getValue()))

                    return true;

            }

            return false;

        }

    }

 

 

3.3关键函数源码分析

3.3.1构造函数

无参构造:默认扩容负载因子0.75

有参构造:最多两个参数 1.cap初始容量  2.loadFactor 扩容因子

虽然传入cap的值,但是没有创建tab,只是对阀值threshold 进行了赋值,赋值为传入cap最近的大于等于cap的2的整数次幂(这个值resize的时候会赋值给cap,保证cap是2的整数次幂),这个时候不创建tab(cap=tab.length就还没有确定)是为了节省空间

HashMap提供了三个不同的构造函数

static final int MAXIMUM_CAPACITY = 1 << 30;  最大容量

public HashMap() {

         this.loadFactor = DEFAULT_LOAD_FACTOR;

    //无参构造-默认扩容负载因子0.75
      //static final float DEFAULT_LOAD_FACTOR = 0.75f;

}

public HashMap(int initialCapacity) { 

        //有参-设定初始容量大小,默认扩容负载因子0.75
        this(initialCapacity, DEFAULT_LOAD_FACTOR);

    }

public HashMap(int initialCapacity, float loadFactor) {
    //有参-设定初始容量大小,和扩容负载因子
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity); 
//tableSizeFor:返回大于输入参数且最近的2的整数次幂的数(当这个值大于最大容量MAXIMUM_CAPACITY时,返回MAXIMUM_CAPACITY)。
//注意:此处的initialCapacity为数组table的大小,即bucket的个数。另外此处赋值为this.threshold,是因为构造函数的时候并不会创建table,
//只有实际插入数据的时候才会创建。目的应该是为了节省内存空间。
//在第一次插入数据的时候,会将table的capacity设置为threshold,同时将threshold更新为loadFactor * capacity
    }

 

tableSIzeFor()方法

tableSizeFor的功能:返回大于输入参数且最近的2的整数次幂的数(当这个值大于最大容量MAXIMUM_CAPACITY时,返回MAXIMUM_CAPACITY)。比如10,则返回16。该算法源码如下

1 static final int tableSizeFor(int cap) {
2     int n = cap - 1;
3     n |= n >>> 1;
4     n |= n >>> 2;
5     n |= n >>> 4;
6     n |= n >>> 8;
7     n |= n >>> 16;
8     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
9 }

先来分析有关n位操作部分:先来假设n的二进制为01xxx...xxx。接着

对n右移1位:001xx...xxx,再位或:011xx...xxx

对n右移2为:00011...xxx,再位或:01111...xxx

此时前面已经有四个1了,再右移4位且位或可得8个1

同理,有8个1,右移8位肯定会让后八位也为1。

综上可得,该算法让最高位的1后面的位全变为1。

最后再让结果n+1,即得到了2的整数次幂的值了。

现在回来看看第一条语句:

int n = cap - 1;

  让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。

 

 

hash(Object key)方法

1 static final int hash(Object key) {
2     int h;
3     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
4 }

看一个方法indexFor,在jdk1.7中有indexFor(int h, int length)方法。jdk1.8里没有,但原理没变,1.8中用tab[(n - 1) & hash]代替但原理一样。下面看下1.7源码

 

static int indexFor(int h, int length) {
        return h & (length-1);
    }

 

调用hash()获取的值,length - node数组的长度,即hashmap的capacity容量大小

indexFor这个方法返回的是这个键值对在数组中存储的位置的下标,也就是说下标的结果与hash值有关

而h & (length-1)这个计算有个规律:

当length=8时    下标运算结果取决于哈希值的低三位

当length=16时  下标运算结果取决于哈希值的低四位

当length=32时  下标运算结果取决于哈希值的低五位

当length=2的N次方, 下标运算结果取决于哈希值的低N位

如果hash()方法中不进行>>> 和 ^运算,在大多数情况下,length的值(hashma的容量)小于2^16次方,根据上面的规律,hash值的高16位是没有参与下标的结果的。那么这样子会导致获取的下标不够分散均匀。所以对key.hashCode()进行>>>和^运算后,再去进行h & (length-1)运算,那么高16位就参与了下标的结果

 

例如1:为了方便验证,假设length为8。HashMap的默认初始容量为16

 

length = 8;  (length-1) = 7;转换二进制为111;

假设一个key的 hashcode = 78897121 转换二进制:100101100111101111111100001,与(length-1)& 运算如下

 

   

0000 0100 1011 0011 1101 1111 1110 0001

 

运算

 

     

0000 0000 0000 0000 0000 0000 0000 0111

 

(就是十进制1,所以下标为1)

 

上述运算实质是:001 与 111 & 运算。也就是哈希值的低三位与length与运算。如果让哈希值的低三位更加随机,那么&结果就更加随机,如何让哈希值的低三位更加随机,那么就是让其与高位异或。

 

3. 原因总结

 

由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。

 

所以这样高16位是用不到的,如何让高16也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。所以(h >>> 16)得到他的高16位与hashCode()进行^运算。

 

4. 为什么用^而不用&和|

 

因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^。

 

这就是为什么有hash(Object key)的原因。

 

 

 

3.3.2put方法

HashMap android 封装 hashmap oom_赋值_02

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }


    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
         Node<K,V>[] tab; 
      Node<K,V> p; 
      int n, 
      i;
        if ((tab = table) == null || (n = tab.length) == 0)
      //1.首次添加,直接扩容resize()
            n = (tab = resize()).length;
      //table 是参数 - transient Node<K,V>[] table;
        if ((p = tab[i = (n - 1) & hash]) == null)
      //2.数组该下表没有元素,直接添加
            tab[i] = newNode(hash, key, value, null);
        else {
      //3.数组该下标有元素
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
        //3.1数组该下标元素key与put的key一致(单个元素或者链表的首个元素的key和put的key一致),e取旧的元素,e的value下面回赋值
                e = p;
            else if (p instanceof TreeNode)
        //3.2数组该下标元素为红黑树-处理
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
        //3.2数组该下标元素为链表且第一个元素与put的key不一致/数组该下标只有一个元素且key与put的key不一致
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) { //当链表下一个元素为null,创建新元素加入链表,结束循环
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
         //链表的下一个元素的key和put的key一致,结束循环
                        break;
                    p = e;
                }
            }
            if (e != null) { // e-用于put的key和数组原元素相同时,记录旧的元素,这里吧put的value更新到元素中
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

3.3.3resize方法

无参构造:默认扩容负载因子0.75

有参构造:最多两个参数 1.cap初始容量  2.loadFactor 扩容因子

虽然传入cap的值,但是没有创建tab,只是对阀值threshold 进行了赋值,赋值为传入cap最近的大于等于cap的2的整数次幂

(这个值会赋值给cap,保证cap是2的整数次幂),这个时候不创建tab(cap=tab.length就还没有确定)是为了节省空间

 

resize:

第一次扩容:

无参构造创建: cap:2^4  Thr:cap * 0.75

有参构造创建: 初始容量设为阀值阀值设置为threshold = cap * loadFactor 

 

非第一次扩容:2倍扩容(最大值为MAXIMUM_CAPACITY = 1 << 30)

阀值等于threshold = cap * loadFactor

 

1 final Node<K,V>[] resize() {
  2 
  3         Node<K,V>[] oldTab = table; //旧的node数组
  4 
  5         int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧的数组容量
  6 
  7         int oldThr = threshold;//旧的扩容阀值
  8 
  9         int newCap,  //新的容量
 10 
 11      newThr = 0;  //新的扩容阀值
 12 
 13         if (oldCap > 0) {  //1.原来容量不为0(不是第一次添加,已经扩过融了)
 14 
 15             if (oldCap >= MAXIMUM_CAPACITY) { 
 16 
 17         // static final int MAXIMUM_CAPACITY = 1 << 30;
 18 
 19         //如果旧容量大于等于1^30,扩容机制直接取int最大值2^32
 20 
 21                 threshold = Integer.MAX_VALUE;
 22 
 23                 return oldTab; //不扩容,直接返回旧的数组
 24 
 25             }
 26 
 27             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 28 
 29                      oldCap >= DEFAULT_INITIAL_CAPACITY)
 30 
 31                 newThr = oldThr << 1; // double threshold
 32 
 33           //static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 34 
 35           //如果旧的数组容量2倍小于最大容量并且旧的容量大于默认容量2^4,新的扩容阀值等于旧的2倍
 36         }
 37 
 38         else if (oldThr > 0) 
 39     //2.有参构造常见的第一次扩容 - 初始容量设置为阀值,初始化的时候有参构造传入了容量大小,但是初始化的时候只设置了threshold 
 40     //的值而没有设置capacity的值,而threshold 取得是大于等于传入容量大小的离他最近的一个2的整次幂的值,
 41     //保证threshold 是2的整次幂,此时将threshold 赋值给capacity,保证capacity是2的整次幂
 42             newCap = oldThr;
 43         else {               
 44     // 3.无参构造创建的,第一次添加
 45             newCap = DEFAULT_INITIAL_CAPACITY; //默认值  2^4
 46             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默认值  0.75 * 2 ^ 4 = 12 默认扩容阀值
 47         }
 48 
 49         if (newThr == 0) { 
 50     //扩容阀值等于容量乘以扩容因子最大值为最大int
 51             float ft = (float)newCap * loadFactor;
 52             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
 53             (int)ft : Integer.MAX_VALUE);
 54         }
 55 
 56         threshold = newThr;
 57 
 58         @SuppressWarnings({"rawtypes","unchecked"})
 59 
 60             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 61 
 62         table = newTab;
 63 
 64         if (oldTab != null) { //把旧的数组的Node放到新的数组里面
 65 
 66             for (int j = 0; j < oldCap; ++j) {
 67 
 68                 Node<K,V> e;
 69 
 70                 if ((e = oldTab[j]) != null) { //旧的Node元素不为null时,赋值给e
 71 
 72                     oldTab[j] = null;
 73 
 74                     if (e.next == null) //Node对象中的参数 Node<K,V> next为null,说明这里不是链表结构,原数组这里只有一个元素
 75 
 76                         newTab[e.hash & (newCap - 1)] = e;//e放入新数组中
 77 
 78                     else if (e instanceof TreeNode)//如果是红黑树树形结构,红黑树的重定位;
 79 
 80  
 81 
 82                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 83 
 84                     else { //当前是链表
 85 
 86                         Node<K,V> loHead = null,  //用于接收新数组中下标为j这里的元素
 87 
 88               loTail = null;      
 89 
 90                         Node<K,V> hiHead = null,  //用于结束新数组中下标为j+oldcap的元素
 91 
 92               hiTail = null;
 93 
 94                         Node<K,V> next;
 95 
 96                         do {
 97 
 98                             next = e.next;
 99 
100                             if ((e.hash & oldCap) == 0) { //处理新数组中下表为j的,这里结果是0的话,代表e.hash/oldcap的结果是2的倍数+余数,扩容后e.hash/2oldcap,余数不变,所以下标不变
101 
102                                 if (loTail == null)
103 
104                                     loHead = e;
105 
106                                 else
107 
108                                     loTail.next = e;
109 
110                                 loTail = e;
111 
112                             }
113 
114                             else { //用于处理新数组中下表为j+capacity的
115 
116                                 if (hiTail == null)
117 
118                                     hiHead = e;
119 
120                                 else
121 
122                                     hiTail.next = e;
123 
124                                 hiTail = e;
125 
126                             }
127 
128                         } while ((e = next) != null);
129 
130                         if (loTail != null) { //新数组线标j元素赋值
131 
132                             loTail.next = null;
133 
134                             newTab[j] = loHead;
135 
136                         }
137 
138                         if (hiTail != null) {//新数组线标j+oldcap元素赋值
139 
140                             hiTail.next = null;
141 
142                             newTab[j + oldCap] = hiHead;
143 
144                         }
145 
146                     }
147 
148                 }
149 
150             }
151 
152         }
153 
154         return newTab;
155 
156 }

 

resize链表处理关键代码

1                  if ((e.hash & oldCap) == 0) {
 2                                 if (loTail == null)
 3                                     loHead = e;
 4                                 else
 5                                     loTail.next = e;
 6                                 loTail = e;
 7                             }
 8                             else {
 9                                 if (hiTail == null)
10                                     hiHead = e;
11                                 else
12                                     hiTail.next = e;
13                                 hiTail = e;
14                             }
15                         } while ((e = next) != null);

HashMap android 封装 hashmap oom_数组_03

 

 

如上图:

  

  链表处理,把链表分为了两块,1.在型数组中与原数组下标相同的,2.在型数组中下标=原数组下标+原数组容量,我们只看第一种:新数组中下标和原数组中相同的(第二种是一样的):

  加入现在这个链表有4节   e1-e2-e3-e4,

if ((e.hash & oldCap) == 0)这个判断分别是true true false true,

  那么参数e lohead lotail在上图中4次判断的内存情况分别为 黑色  绿色  黄色  红色

    第一次true, e指向 e1 lohead 指向e1 lotail指向e1

e;lotail指向e2

e = next指向e3,其它两个不变

e = next指向e4,loTail.next =loTail = e;lotail指向e4

    那么最终的结果就是红色的线条,我们看lohead的指向lohead = e1    e1.next = e2    e2.next = e4,所以它现在是e1-e2-e4,就是我们想要的结果。

 

3.4Cloneable和Serializable分析

在HashMap的定义中实现了Cloneable接口,Cloneable是一个标识接口,主要用来标识 Object.clone()的合法性,在没有实现此接口的实例中调用 Object.clone()方法会抛出CloneNotSupportedException异常。可以看到HashMap中重写了clone方法。

HashMap实现Serializable接口主要用于支持序列化。同样的Serializable也是一个标识接口,本身没有定义任何方法和属性。另外HashMap自定义了

private void writeObject(java.io.ObjectOutputStream s) throws IOException
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException

两个方法实现了自定义序列化操作。

注意:支持序列化的类必须有无参构造函数。这点不难理解,反序列化的过程中需要通过反射创建对象。

 

4.HashMap线程不安全问题

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

其中第6行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

除此之前,还有就是代码的倒数第4行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。