【Explore SRC】一起看看HashMap源码

HashMap源码一直是众多Java程序员的必经之路,今天我也看看,大家凑热闹不?基于水平有限,有些地方理解错误、理解不了,请大家指出哦~~

> 版本说明

查看的版本是jdk1.7.0_71

> 结构概要图

> 从构造方法看起吧

public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity)
public HashMap()
public HashMap(Map<? extends K, ? extends V> m)

HashMap有4个构造方法,具体看下代码,可知第2、3个方法都是调用第1个方法进行操作的。那么,具体看第1个吧。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

查看参数的全局变量,知道初始化容量是16,扩容因子(容量达到哪里时要重新构造HashMap的容器)默认为0.75。

最后具体看第1个方法的方法体,主要作了3件事:

1、如果入参异常,则抛出异常

2、对初始化容量进行饱顶

3、将入参设置为属性,这里有点注意:threshold(阀值),在HashMap刚初始化市被赋值为初始容量。

4、后面,还调用了init()供子类的开发人员扩展?(猜的)

> 哪些方法常用,当然是上子弹的方法了--put(K key, V value)

if (table == EMPTY_TABLE) {
    inflateTable(threshold);
}
......
/**
 * Inflates the table.
 */
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

内部有个叫table的数组,其元素是指向链表,而这链表装载的就是实际包含key、value的元素。

刚刚初始化HashMap时,此时table为空,这时就需要根据threshold对table进行扩容。

将table扩容至threshold的上随2的n次方大小。比如,threshold为16,则扩容至16;threshold为17,则扩容至32。

注:

roundUpToPowerOf2()见下述。

if (key == null)
    return putForNullKey(value);

查看putForNullKey方法,将key为null的元素,放入table下表为0的链表里。而逻辑与下面要讲的放入元素的逻辑基本一致。

注:

为什么是下标为0的元素放key为null的值呢?见下述。

int hash = hash(key);
int i = indexFor(hash, table.length);

对key对象进行哈希计算后,映射到table数组中一个位置,为i。

注:

indexFor(),见下述。

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
    }
}

上面已找到当前要插入的元素位于table数组的哪个位置了,接下来就线性遍历这个位置指向的链表,如果发现hash值相等并且key也相等的,就说明此Map已包含此元素,那么,就用新值覆盖旧值,并返回旧值吧。

modCount++;
addEntry(hash, key, value, i);

...

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

/**
 * Like addEntry except that this version is used when creating entries
 * as part of Map construction or "pseudo-construction" (cloning,
 * deserialization).  This version needn‘t worry about resizing the table.
 *
 * Subclass overrides this to alter the behavior of HashMap(Map),
 * clone, and readObject.
 */
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

程序跑到这里,说明在Map中并没有找到Key值,需要作插入。

modCount是记录插入的次数,估计用作限制并发操作的。

addEntry()在插入元素前,要判断元素是否达到一个阀值,如果达到,就对table进行2倍的扩容、重新哈希。(此点内容下面讲述)

然后,重新计算元素在扩容后的位置,调用createEntry()作实际的插入操作。插入操作,就是将新插入的元素的next指向链表的第一个元素,然后将table数字的该下表指向新插入的元素。

/**
 * Rehashes the contents of this map into a new array with a
 * larger capacity.  This method is called automatically when the
 * number of keys in this map reaches its threshold.
 *
 * If current capacity is MAXIMUM_CAPACITY, this method does not
 * resize the map, but sets threshold to Integer.MAX_VALUE.
 * This has the effect of preventing future calls.
 *
 * @param newCapacity the new capacity, MUST be a power of two;
 *        must be greater than current capacity unless current
 *        capacity is MAXIMUM_CAPACITY (in which case value
 *        is irrelevant).
 */
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

/**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

先根据newCapacity实例化一个新的table。

因新table的长度变更了嘛,需遍历原table所指向的链表的所有元素,一个个转到新的table(计算hash、重新定位)。(至于是否重新hash,我还没看明白)

> 细节

> roundUpToPowerOf2(int number)

private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

这个方法是计算number最接近的2的N次方数。

其中Integer.highestOneBit()是取最高位1对应的数,如果是正数,返回的是最接近的比它小的2的N次方;如果是负数,返回的是-2147483648,即Integer的最小值。

那为什么要先减1,再求highestOneBit()?

举几个数的二进制就知道了:

00001111 = 15 -> 00011110 = 30 -> highestOneBit(30) = 16

00010000 = 16 -> 00100000 = 32 -> highestOneBit(32) = 32

所以,为了获取number最接近的2的N次方数,就先减一。

附一个简单的分解计算:

public class Lefter {

    public static void main(String[] args) {
        for (int i = 2; i <= 17; i++) {
            System.out.println(i);
            System.out.println(i - 1);
            System.out.println((i - 1) << 1);
            System.out.println(Integer.highestOneBit((i - 1) << 1));
            System.out.println("result : " + i + " -> " + Integer.highestOneBit((i - 1) << 1));
        }
    }

}

结果:

2
1
2
2
result : 2 -> 2
3
2
4
4
result : 3 -> 4
4
3
6
4
result : 4 -> 4
5
4
8
8
result : 5 -> 8
6
5
10
8
result : 6 -> 8
7
6
12
8
result : 7 -> 8
8
7
14
8
result : 8 -> 8
9
8
16
16
result : 9 -> 16
10
9
18
16
result : 10 -> 16
11
10
20
16
result : 11 -> 16
12
11
22
16
result : 12 -> 16
13
12
24
16
result : 13 -> 16
14
13
26
16
result : 14 -> 16
15
14
28
16
result : 15 -> 16
16
15
30
16
result : 16 -> 16
17
16
32
32
result : 17 -> 32

> indexFor(int h, int length)

将h映射到length的范围里,效果就像求模。

return h & (length-1);

将h和length - 1和操作就可以了。

比如length为16,那么:

16 = 00010000

15 = 00001111

> 为什么是下标为0的元素放key为null的值呢?

根据上述indexFor(int h, int length)映射的范围在1到length - 1,那么剩下的下标就是0。

> 为什么hash数组的长度要弄成2的N次方?

我觉得这是为了迁就&运算(因为&运算效率较高嘛),而用&运算有个限制,就是2的N次方内嘛,所以嘛。(个人理解,不知对否)

> 剩下的仍未想明白

1、initHashSeedAsNeeded(capacity)

2、hash()

时间: 2024-08-01 20:49:25

【Explore SRC】一起看看HashMap源码的相关文章

【Java集合源码剖析】HashMap源码剖析

转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955 HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap. HashMap 实现了Serializable接口,因此它支持序列化,

[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源码分析(基于JDK1.6)

在Java集合类中最常用的除了ArrayList外,就是HashMap了.本文尽自己所能,尽量详细的解释HashMap的源码.一山还有一山高,有不足之处请之处,定感谢指定并及时修正. 在看HashMap源码之前先复习一下数据结构. Java最基本的数据结构有数组和链表.数组的特点是空间连续(大小固定).寻址迅速,但是插入和删除时需要移动元素,所以查询快,增加删除慢.链表恰好相反,可动态增加或减少空间以适应新增和删除元素,但查找时只能顺着一个个节点查找,所以增加删除快,查找慢.有没有一种结构综合了

转:【Java集合源码剖析】HashMap源码剖析

转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955   您好,我正在参加CSDN博文大赛,如果您喜欢我的文章,希望您能帮我投一票,谢谢! 投票地址:http://vote.blog.csdn.net/Article/Details?articleid=35568011 HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动

HashMap源码分析二

jdk1.2中HashMap的源码和jdk1.3中HashMap的源码基本上没变.在上篇中,我纠结的那个11和101的问题,在这边中找到答案了. jdk1.2 public HashMap() { this(101, 0.75f); } public HashMap(Map t) { this(Math.max(2*t.size(), 11), 0.75f); putAll(t); } jdk1.3 public HashMap() { this(11, 0.75f); } public Has

Java集合系列之HashMap源码分析

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

HashMap 源码解析

HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表.允许null键和null值,不保证键值对的顺序. HashMap检索数据的大致流程: 当我们使用HashMap搜索key所对应的value时,HashMap会根据Hash算法对key进行计算,得到一个key的hash值,再根据hash值算出该key在数组中存储的位置index,然后获取数组在inde

Java集合之HashMap源码分析

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

【Java集合源码剖析】HashMap源码剖析(转)

HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap. HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆. HashMap源码剖析 HashMap的源码如下(加入了比较详细的注释): [ja