HashMap源码分析

  • HashMap应该算是工作中用到的频率最高的数据结构了,网上也有很多人分析过HashMap的源码,但为了加深印象,逼自己好好阅读源码,还是自己试着看源码进行一次自己的分析。

  • 正如类里注释所说:除了支持null值和null键,线程不安全之外,功能大致和HashTable一致。可以用常量时间get和put元素,HashMap不保证元素的顺序。

  • HashMap的capacity和loadfactor两个属性非常有讲究,会影响hashMap的性能。capacity是hashMap初始时的数组大小,必须是2的N次幂,factor的值默认为0.75,capacity和loadFactor的乘积是hashMap扩容的阈值。factor取值0.75是在空间和时间上的一个折中,如果actor值越小,则扩容阈值越小,rehash越频繁,越浪费空间,越不容易造成hash冲突(hash冲突时,会使用链表存数据,冲突越严重,链表越长),所以检索效率就会比较高,反之,扩容阈值大,rehash次数减少,也节省空间,但容易造成hash冲突(rehash冲突造成链表长度过长),检索效率就会降低。至于capacity为什么必须是2的N次幂,我是知其然,但不知其所以然。用网上高人的解释就是:当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了,参考博客:http://blog.csdn.net/jiary5201314/article/details/51439982

  • 关于提高hashMap性能的另外一点需要注意的是,如果能提前知道要存放的元素的个数,可以在创建hashMap时,指定数组的大小来避免扩容,rehash造成的性能影响。如有1000个元素,可以指定:Map map = new HashMap(2048);为什么成了2048,而不是1000呢,还是要记住capacity必须是2的N次幂。比1000大的2的N次幂不是1024吗?因为扩容的阈值是capacity*loadFactor,如果capacity设置成1024,还是会扩容一次,所以要想capacity*loadFactor既大于1000,还要保证capacity为2的N次幂,那capacity的最小值就是2048了。这个例子也是举了上面博客的例子:http://blog.csdn.net/jiary5201314/article/details/51439982

接下来分析下HashMap主要的几个属性和方法:

1.主要属性:

//默认的数组大小
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认的加载因子大小
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//hashMap真正的底层数据结构:数组 数组元素为Entry,是个单向链表
transient Entry[] table;
//hashMap中元素的个数
transient int size;
//扩容的阈值 等于capacity*loadFactor
int threshold;
//加载因子
final float loadFactor;

2.主要的链表结构内部类(注意:如果hash冲突时,元素会放在链表的头部):

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

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
 }

3.无参的构造方法:

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;//默认0.75
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//默认为16*0.75=12
    table = new Entry[DEFAULT_INITIAL_CAPACITY];//默认为16
    init();//空方法,什么都没做
}

4.put方法:

public V put(K key, V value) {
    //支持key为null,放在数组的下标为0的位置
    if (key == null)
        return putForNullKey(value);
    //根据hashcode算出一个hash值
    int hash = hash(key.hashCode());
    //根据计算的hash值和数组长度做&运算,得到元素要存放的数组位置
    int i = indexFor(hash, table.length);
    //检查指定的下标出,是否有元素,有说明发生了hash冲突,需要检查链表中的每个元素跟要放入的元素是否相等,相等则替换原位置的值,不相等则执行新增操作;如果下标处没有元素,也直接执行新增操作
    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++;
    //新增元素
    addEntry(hash, key, value, i);
    return null;
}

addEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
    //取出指定位置的元素,可能为null,可能为一个元素,可能是一个链表
    Entry<K,V> e = table[bucketIndex];
    //新建一个Entry,next指向e,即新建的这个Entry放在了头部
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //如果元素个数>=阈值,扩容到原来数组的两倍
    if (size++ >= threshold)
         resize(2 * table.length);
}

resize方法:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果数组大小达到最大值时,只设置threshold值为最大值,然后返回,不做扩容操作
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //新建一个是原来两倍容量的数组
    Entry[] newTable = new Entry[newCapacity];
    //将旧数组中的元素转移到新的数组里面,需要rehash
    transfer(newTable);
    //table属性指向新数组
    table = newTable;
    //重新计算threshold值
    threshold = (int)(newCapacity * loadFactor);
}

transfer方法:

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    //1.遍历旧数组,取出数组中的每一个元素;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            //2.如果旧数组中不为null,设置成null;有助于GC
            src[j] = null;
            //3.从头部开始遍历每一个链表;
            do {
                Entry<K,V> next = e.next;
                //4.根据hash和新数组长度重新计算下标;
                int i = indexFor(e.hash, newCapacity);
                //5.将链表中的每一个元素重新放到新数组中,经过此次循环,旧数组中链表的头会变成新数组中链表的尾,尾会变成头
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

get方法:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    //根据hashCode算出hash值
    int hash = hash(key.hashCode());
    //遍历链表
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        //hashCode和equals都是true,才认为是同一个key,这也是自己定义的类如果作为hashMap的key时,必须按要求重写hashCode和equals方法的原因
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}
  • 到这里,hashMap的主要功能也算是分析完了。主要理解的点有:capacity值为什么为2的N次幂?loadFactor为0.75的好处是什么?创建HashMap时,若确定元素多少要指定capacity大小来避免扩容、rehash带来的性能影响;如果发生hash冲突时,是通过链表解决的,先加的元素放链表末尾,后添加的放头部,rehash时,链表会头尾颠倒下。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值