HashMap源码解析

android-23/java/util/HashMap.java

public class HashMap< K, V > extends AbstractMap< K, V > implements Cloneable, Serializable

没有实现Collection接口

属性

最小容量
private static final int MINIMUM_CAPACITY = 4;
最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
加载因子
static final float DEFAULT_LOAD_FACTOR = .75F;
底层存储数组,是个Entry类型的数组,其实就是个hash表
transient HashMapEntry<K, V>[] table;
键值为null的存储对对象
transient HashMapEntry<K, V> entryForNullKey;
元素个数
transient int size;
扩容限值,通常为容量和加载因子的乘积
private transient int threshold;

private transient Set<K> keySet;
private transient Set<Entry<K, V>> entrySet;
private transient Collection<V> values;

Map接口

public interface Map<K,V> {

    public static interface Entry<K,V> {       
        public boolean equals(Object object);
        public K getKey();
        public int hashCode();
        public V setValue(V object);
    };

    public void clear();
    public boolean containsKey(Object key);
    public boolean containsValue(Object value);
    public Set<Map.Entry<K,V>> entrySet();
    public boolean equals(Object object);
    public V get(Object key);
    public int hashCode();
    public boolean isEmpty();
    public Set<K> keySet();
    public V put(K key, V value);
    public void putAll(Map<? extends K,? extends V> map);
    public V remove(Object key);
    public int size();
    public Collection<V> values();
}

Map接口里面有内部定义了一个Entry< K,V >接口,定义了Map里面的存储元素结构。

元素对象

包括键、值、hash值、next指针域,因为HashMap解决hash冲突采用链地址法(不过java 8有了不同的方式,性能更高)

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> ne
        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;
    }
}

添加

@Override public V put(K key, V value) {
    if (key == null) {
         //键为null时,调用此方法
        return putValueForNullKey(value);
    }
    //对键进行hash
    int hash = Collections.secondaryHash(key);
    //table就是HashMapEntry<K,V>数组
    HashMapEntry<K, V>[] tab = table;
    //新元素在HashMapEntry<K,V>数组中的位置,由键的hash值和数组长度共同决定
    int index = hash & (tab.length - 1);
    //检测此下标上是否有元素,若有,遍历此处的链表(链表中的元素hash值都相同),覆盖键相同的值
    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;
        }
    }
    // Map里面没有键相同或键为null的元素
    modCount++;
    if (size++ > threshold) {
        //元素个数大于扩容限度,2倍扩容
        tab = doubleCapacity();
        //用新的长度来计算下标
        index = hash & (tab.length - 1);
    }
    //真正增加元素的地方,传入了hash值和下标
    addNewEntry(key, value, hash, index);
    return null;


//存入键值为null的值
private V putValueForNullKey(V value) {
    //entryForNullKey对象专门持有该空键的值
    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;
    }
}


//注意新建entry时next域传入头结点,添加新entry时,会采用头插法
void addNewEntry(K key, V value, int hash, int index) {
    table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}

//新建key为null的entry,传入的键值为null,hash为0
void addNewEntryForNullKey(V value) {
    entryForNullKey = new HashMapEntry<K, V>(null, value, 0, null);
}

为什么初始容量必须是2的幂且扩容必须是2倍?为什么下标index算法为int index = hash & (tab.length - 1);?
初学数据结构与算法,hash表的下标使用取模计算的,这里为什么用位运算?先上结论吧,这两个问题其实是相关联的,当tab.length 为2的幂的时候,hash&(tab.length - 1)等价于hash%tab.length。

取模的方法,本质上就是hash不停除以长度,直到结果小于除数。而按位与,在二进制下分两部分看:(1)hash值中比length高的字节,一与之后全没了,因为length的高字节全是0,那写被与的高字节其实就是length的倍数,准确的说是2的幂倍,相当于除了好多length。(2)除完之后的hash值二进制为全0或全1,即00000。。~11111。。即实际范围(0到length+length-1)而length二进制为1000。。。,hash值还是比长度大,length-1的二进制都是形如011111。。。,此时的hash与length-1一与,比length大的数就会减去一个length,而比length小的数不会变,之后范围就会变成(0~length-1),这样就对应了数组的下标了。

hash算法

由键的hashCode和Collections的hash算法决定

int hash = Collections.secondaryHash(key);

//Collections里面的静态方法
public static int secondaryHash(Object key) {
        //调用key对象的hashCode方法
    return secondaryHash(key.hashCode());
}

//真正产生hash值的地方,真正的hash值两个因素决定:1、键的hashCode值。2、此方法里面的算法
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);
}

扩容

若果元素个数大于扩容阈值,就2倍扩容

private HashMapEntry<K, V>[] doubleCapacity() {
    HashMapEntry<K, V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        return oldTable;
    }
    //两倍扩容
    int newCapacity = oldCapacity * 2;
    //使用新的容量建立新的entry数组,数组是空的,并且对扩容阈值重新计算赋值
    HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
    if (size == 0) {
        return newTable;
    }
    for (int j = 0; j < oldCapacity; j++) {
        HashMapEntry<K, V> e = oldTable[j];
        if (e == null) {
            continue;
        }
        //highBit为0代表index小于length,为length代表index大于length,详见上面红字的问题
        int highBit = e.hash & oldCapacity;
        HashMapEntry<K, V> broken = null;
        //重新散列
        newTable[j | highBit] = e;
        //遍历hash冲突的链表,从第二个元素开始
        for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
            //对链表内的元素也重新hash
            int nextHighBit = n.hash & oldCapacity;
            //不同的话就说明相邻两个节点要断开,n节点要划分 
            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;
}

在对链表进行再hash时,需要有三个指针,broken指向要断开的地方,e指向前一个节点、n指向当前要判断的节点

HashMap的迭代,HashIterator

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值