继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

解析HashMap源码

眼眸繁星
关注TA
已关注
手记 109
粉丝 7
获赞 59

HashMap

HashMap是一个基于哈希表的实现,可以提供键-值映射.上次巴神指点我应该去读读它的源码,所以今天就来看看它心里是怎么想的.

第一行

阅读源码首先就要看看第一行咯,看看它继承了谁,可以给它定性.再看看它实现的接口,可以知晓它能够拥有哪些动作.

1
2
3
public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable{
    ...
}

对于AbstractMap<K, V>,显然是一个抽象类

1
2
3
public abstract class AbstractMap<K, V> implements Map<K, V> {
    ...
}

Map

对于Map<K, V>,首先是一个叫Entry的接口,显而易见,这就是map的存储结构,它的每一个元素都是这样的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  /**
   * {@code Map.Entry} is a key/value mapping contained in a {@code Map}.
   */
public static interface Entry<K,V> {
      //比较传过来的object对象和当前的 entry对象的hashCode是否相等,如果相等就返回true,否则返回false
      public boolean equals(Object object);
      //返回key
      public K getKey();
      //返回Value
      public V getValue();
      //获取hashCode  是一个int值
      public int hashCode();
      //将这个entry的值设为指定的值(object)
      public V setValue(V object);
  };

然后就是一系列方法

方法

作用

clear()

清除所有entrty

containsKey(Object key)

检查Map是否包含key

containsValue(Object value)

检查Map是否包含Value

entrySet()

返回一个Set<map.entry>集合,这个集合包含了Map所有的Entry</map.entry

equals(Object object)

判断Map集合是否与指定的对象相同

get(Object key)

通过key得到Value

isEmpty()

判断Map是否为空

keySet()

返回一个Set集合,包含了所有的key

put(K key, V value)

将指定的key和value映射到map里

putAll(Map<? extends K,?   extends V> map)

将指定map里的数据copy到当前map

remove(Object key)

删除指定key的映射

size()

返回映射的个数

values()

返回map中所有value的集合

HashMap#HashMapEntry

HashMap的爷爷是Map,所以自然也继承了Entry,构造方法包括四个参数

o    key 你懂的

o    value 你懂的

o    hash 当前映射的hash

o    next 下一个entry

hash相同的entry将会被放到一个链表里,next指向下一个entry.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static class HashMapEntry<K, V> implements Entry<K, V> {
    final K key;
    V value;
    final int hash;
    HashMapEntry<K, V> next;
    HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
        this.key = key;
        this.value = value;
        this.hash = hash;
        this.next = next;
    }
    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }
    @Override public final boolean equals(Object o) {
        if (!(o instanceof Entry)) {
            return false;
        }
        Entry<?, ?> e = (Entry<?, ?>) o;
        return Objects.equal(e.getKey(), key)
                && Objects.equal(e.getValue(), value);
    }
    @Override public final int hashCode() {
        return (key == null ? 0 : key.hashCode()) ^
                (value == null ? 0 : value.hashCode());
    }
    @Override public final String toString() {
        return key + "=" + value;
    }
}

三个构造方法

无参构造

1
2
3
4
5
@SuppressWarnings("unchecked")
public HashMap() {
    table = (HashMapEntry<K, V>[]) EMPTY_TABLE;
    threshold = -1; // Forces first put invocation to replace EMPTY_TABLE
}

以此方法可以构造一个空的HashMap;
如果散列表包含空映射,table就被赋值new HashMapEntry[MINIMUM_CAPACITY >>> 1]; 这个是啥呢?
MINIMUM_CAPACITY不用说,就是最小容量了,是4.
MINIMUM_CAPACITY>>>1 这里的>>>是无符号右移,忽略了符号位扩展,0补最高位,所以就是4>>>1=2;
所以这里 Entry[] EMPTY_TABLE = new HashMapEntry[2];

threshold:这里赋值为-1 其实就是让它成为一个新的表,当表size大于它时表将会被重新hash.

两个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
public HashMap(int capacity, float loadFactor) {
       this(capacity);
       if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
           throw new IllegalArgumentException("Load factor: " + loadFactor);
       }
       /*
        * Note that this implementation ignores loadFactor; it always uses
        * a load factor of 3/4. This simplifies the code and generally
        * improves performance.
        */
   }

两个参数

o    capacity:指定初始化容量你懂的

o    loadFactor: the initial load factor(指定的一个拓展因素,但是因为一般3/4就是最高性能,所以这个参数没有给予实现).

调用了下面的一个参数.

一个参数构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public HashMap(int capacity) {
      if (capacity < 0) {
          throw new IllegalArgumentException("Capacity: " + capacity);
      }
      if (capacity == 0) {
          @SuppressWarnings("unchecked")
          HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
          table = tab;
          threshold = -1; // Forces first put() to replace EMPTY_TABLE
          return;
      }
      if (capacity < MINIMUM_CAPACITY) {
          capacity = MINIMUM_CAPACITY;
      } else if (capacity > MAXIMUM_CAPACITY) {
          capacity = MAXIMUM_CAPACITY;
      } else {
          capacity = Collections.roundUpToPowerOfTwo(capacity);
      }
      makeTable(capacity);
  }

参数

o    capacity:the initial capacity of this hash map(指定的一个初始化容量).

所以一般就会运行这句capacity = Collections.roundUpToPowerOfTwo(capacity);
这句冲洗给capacity赋值,具体方法是这样的

1
2
3
4
5
6
7
8
9
10
11
12
public static int roundUpToPowerOfTwo(int i) {
       i--; // If input is a power of two, shift its high-order bit right.
       // "Smear" the high-order bit all the way to the right.
       i |= i >>>  1;
       i |= i >>>  2;
       i |= i >>>  4;
       i |= i >>>  8;
       i |= i >>> 16;
       return i + 1;
   }

将容量编程了4的倍数且大于原容量.
5->8 6->8 7->8 8->8 9->16 17->32
其实目的就是要让容量能大于等于指定的初始容量,但这个值还得是2的指数倍,即底层数组的长度总是为2的n次方。

根据容量执行了makeTable()方法

1
2
3
4
5
6
7
private HashMapEntry<K, V>[] makeTable(int newCapacity) {
    @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
            = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
    table = newTable;
    threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
    return newTable;
}

按照容量初始化了entry表,并且给threshold重新赋值.

put

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override public V put(K key, V value) {
    if (key == null) {
        return putValueForNullKey(value);
    }
    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            preModify(e);
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }
    // No entry for (non-null) key is present; create one
    modCount++;
    if (size++ > threshold) {
        tab = doubleCapacity();
        index = hash & (tab.length - 1);
    }
    addNewEntry(key, value, hash, index);
    return null;
}
void addNewEntry(K key, V value, int hash, int index) {
    table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}

来看看put方法做了什么:

key==null

首先判断key是否为空,key为空其实就是没有元素,调用putValueForNullKey方法插入.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private V putValueForNullKey(V value) {
        HashMapEntry<K, V> entry = entryForNullKey;
        if (entry == null) {
            addNewEntryForNullKey(value);
            size++;
            modCount++;
            return null;
        } else {
            preModify(entry);
            V oldValue = entry.value;
            entry.value = value;
            return oldValue;
        }
    }

如果插入以前没有null作为key的情况,就插入一个null key的entry,否则替换原来的null key的value,并且返回老的entrty.

key!=null

o    根据我们插入的entry.key计算hash

1
int hash = Collections.secondaryHash(key);

o    根据hash和tab.length得到index

1
2
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);

o    遍历tab[index]看是否存在 e.hash==hash 且 key.equals(e.key)

o    如果存在就替换旧的值,返回旧的值

o    否则就要准备插入entry了,这时第一件事是判断size++有没有大于阈值,如果大于了就扩容,并且重新hash(为了使entry尽可能分散).

1
2
3
4
 if (size++ > threshold) {
    tab = doubleCapacity();
    index = hash & (tab.length - 1);
}

o    最后调用addNewEntry插入一个新的entry

1
2
3
void addNewEntry(K key, V value, int hash, int index) {
      table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
  }

这样完成了一个entry的插入.

最主要的疑问是:hash在这其中是如何作用的?

hash作用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int hash = Collections.secondaryHash(key);
public static int secondaryHash(Object key) {
    return secondaryHash(key.hashCode());
}
private static int secondaryHash(int h) {
    // Spread bits to regularize both segment and index locations,
    // using variant of single-word Wang/Jenkins hash.
    h += (h <<  15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h <<   3);
    h ^= (h >>>  6);
    h += (h <<   2) + (h << 14);
    return h ^ (h >>> 16);
}

这样一个调用链计算对应key的hash,刚刚看到也是一脸懵逼…

可以来说一说HashMap的实现原理了:
HashMap实现原理是数组加链表实现,先根据key的hash来确定其在数组中的位置,如果在哪个位置没有别的元素,那自然最好,直接插入到那个位置.如果那个位置有元素,就将元素插入到那个链表里.

所以key的hash值很重要,它直接决定了entry在hashMap中的位置,不进行hash,直接求index往往都比较集中,entry很容易重复,与我们追求的分散背道而驰,导致我们总是需要遍历很长的链表来查找entry.所以需要使用hash算法把entry分散,让我们在数组里尽可能的不需要遍历链表就能查找到指定的元素,这样的效率最高.

引用一段计算 From Qisen Tang

这里举一个例子,如果现在Table的容量是16,如果最后的结果是 hash & (16-1),也就是 hash & (00001111)。现在我们试着对hash值是31(00011111), 63(00111111),95(01011111)进行操作,理论上映射到的index值应该是不尽相同的,然而实际的情况确实如下的情形:
31=00011111 ==& 00001111==> 1111=15
63=00111111 ==& 00001111==> 1111=15
95=01011111 ==& 00001111==> 1111=15

因此Collections.secondaryHash需要解决的问题,就是避免上面的情况,效果见下面的例子:
31=00011111 ==secondaryHash==> 00011110==& 00001110==> 14
63=00111111 ==secondaryHash==> 00111100==& 00001100==> 12
95=01011111 ==secondaryHash==> 01011010==& 00001010==> 10

1
int index = hash & (tab.length - 1);

前面说了,数组长度总是2的n次方.
这时:hash&(length-1)运算等价于对length取模,也就是hash%length,但是&比%具有更高的效率.
通过上面的计算就得到了index,再获取数组tab[index]位置的链表,遍历链表,如果有相同entry则替换(hash相同且key相同).否则插入一个新的元素.
这样完成了put操作.

关于扩容 doubleCapacity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
     * Doubles the capacity of the hash table. Existing entries are placed in
     * the correct bucket on the enlarged table. If the current capacity is,
     * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which
     * will be new unless we were already at MAXIMUM_CAPACITY.
     */
    private HashMapEntry<K, V>[] doubleCapacity() {
        HashMapEntry<K, V>[] oldTable = table;
        //获取旧表长度
        int oldCapacity = oldTable.length;
        //如果旧表已经最大了 没法扩展了 直接返回
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        //新的容量变为旧的两倍
        int newCapacity = oldCapacity * 2;
        //使用makeTable创建新的table
        HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
        //size==0  就没有entry了 不用进行其他操作了 可以直接返回表
        if (size == 0) {
            return newTable;
        }
        for (int j = 0; j < oldCapacity; j++) {
            HashMapEntry<K, V> e = oldTable[j];
            if (e == null) {
                continue;
            }
            //oldCapacity是2的次方  所以一定是 00010000 的类型  hash与它 & 以后可以得到hash的高位
            int highBit = e.hash & oldCapacity;
            HashMapEntry<K, V> broken = null;
            //新的table的 index = j|highBit  此时highBit只有高位 j与highBit相或 得到新的index值
            newTable[j | highBit] = e;
            for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j | nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }
        return newTable;
    }

获取旧的容量,旧表容量如果不大于MAXIMUM_CAPACITY,新表容量为旧表容量的两倍,然后创建新表,再将旧表的数据转移到新表中去,在这个过程中会重新计算hash.
在代码中已经写了注释,得到新的index以后就可以遍历将之前index的链表转移到新的table的index位置去.

原文链接:http://www.apkbus.com/blog-705730-62499.html

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP