HashMap源码分析 (jdk1.7)

HashMap (jdk1.7)

HashMap的存储结构是 数组 + 链表 的组合.

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; 数组 table 存储的是 Entry 对象, 而 Entry 对象是 HashMap 内定义的一个静态内部类, 可以组织成链表的形式.

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    ...
}
构造器
//带参构造
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;
        threshold = initialCapacity;
        init();
    }
//无参构造
public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

由上面这两个构造器可以看出, HashMap 初始化时只是设置了两个属性值, 并没有构造出一个二维的结构.

put() 操作

put 简单来说就是创建一个映射关系, 开始时会为 table 申请空间, threshold 同时会被赋值为 capacity*loadFactor

public V put(K key, V value) {
   //为 table 申请空间, 感觉放在这里挺奇怪的, 这种延迟初始化有什么好处呢
        if (table == EMPTY_TABLE) {
            inflateTable(threshold); 
        }
    //处理 null 键, 要注意的是, null 键会自动映射到table[0]
        if (key == null)
            return putForNullKey(value);
    //对 key.hashCode() 疯狂操作, 得到 hash 值
        int hash = hash(key);
    //这里的i代表桶号,值得注意的是,并不是用%运算来定位桶的.
        int i = indexFor(hash, table.length);
    //for 循环来判断重复键, 如果重复就覆盖, 并返回oldValue
        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;
            }
        }

        modCount++;
    //无重复, 首插法插入k-v
        addEntry(hash, key, value, i);
        return null;
    }

当 hash 值相同时, 会去比较 key 的地址和 equals() 方法

inflateTable(threshold);

table 在这个方法中被申请到空间, 同时threshold 被赋值为 capacity*loadFactor

private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

    	//threshold 被重新赋值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];  //申请数组
        initHashSeedAsNeeded(capacity);
    }

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

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

这里太巧妙了, 如何散列到桶上, 我只能想到取模运算. 而源码中用的是取末几位来确定, 这样运算效率就高了很多.

这里也是 capacity 要求为 2 的次数的原因. 这种情况下 table.length - 1 的 二进制每位全是1, 保证 key 可以均匀的散列到每个 bucket 里.

addEntry(hash, key, value, i);

void addEntry(int hash, K key, V value, int bucketIndex) {
    //扩容的条件 size >= threshold
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
	//首插法, 新添加的 entry 会被放到数组上
        createEntry(hash, key, value, bucketIndex);
    }
get() 操作
public V get(Object key) {
    if (key == null)
        //遍历 table[0]
        return getForNullKey(); 
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}

这里要注意的是, key 为 null, 它直接就去table[0] 中查找

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }
//同样对key疯狂操作得到hash值.
    int hash = (key == null) ? 0 : hash(key);
    //因为下面用不到i,所以散列值i放到for里
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        //根据has重复时,可以比较key地址和equals()
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

这里要注意的是, 如何根据 key 在链表中寻找到 value?

因为同一个链表上, hash值相等. 这时候就要用 key 同 entry 对象中的 key 挨个比较, 地址相同则相同, 地址不同, 用equals 方法来确定

总结

首先, 哈希表是在 put 内部完成空间申请的, 如果 put 的内容是 null, 则直接放到 0 号桶里, 不是 null, 根据 hash 值确定桶号, 用 hash 值, ==运算和 equals 方法确定元素是否重复, 重复就覆盖. 否则插入到链表首部.

“resize 和 可能导致的死锁原因以后哪天有空再学, 优先级比较低”

猜你喜欢

转载自blog.csdn.net/weixin_41889284/article/details/88623729
今日推荐