学习 HashMap 原理笔记

感谢 https://www.cnblogs.com/chengxiao/p/6059914.html#t4 ,学习了,写写笔记。

 hashMap 数据结构

        哈希表。

         哈希表利用数组根据下标查找元素,一次定位就可以找到,复杂度为O(1)。

         可以理解,哈希表的主干是数组,通过某个函数把当前元素的关键字映射到数组中的某个位置上,通过下标就可以完成定位。

哈希冲突:

         插入的时候,用哈希函数算出来的映射下标插入元素的时候,发现该位置已经被占用。HashMap采用了链地址法解决哈希冲突的,也就是数组+链表的方式,

HashMap 的实现原理

       HashMap 的主干其实是一个Entry  数组,Entry 是HashMap 中的一个静态内部类:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
        int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算

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

前面说了解决哈希冲突是链地址法, 所以Entry存在指向下一个entry  的引用,这个引用的存在就是为了解决哈希冲突而存在的,插入的时候,如果发现函数映射的下标已经被占用,如果该位置的next 为空,就会在该位置生成一个链表。如果该位置已经存下链表,就会遍历该下标的链表,如果有相等的元素就覆盖,没有就在链表尾部添加新的元素。

结论,HashMap 的性能决定链表的多少。链表越少,性能越好。

JDK 1.7 的源码在上面博客有讲解很清楚,这里写些笔记:

执行添加操作时,根据key的hashcode进行hash()散列函数计算,确保均匀分布,再根据散列值获取下标(indexFor 函数

),将值放入该位置。

即: hashcode(key的hashcode) ——>  h(通过神奇的hash函数获取) ——>下标

这里考虑的问题时扩容,什么时候扩容????下面是HashMap 比较重要的变量:

transient int size; //实际键值对的个数 

 /** 阈值 初始容量16*/
int threshold;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始容量

/** 负载因子*/
final float loadFactor;
static final float DEFAULT_LOAD_FACTOR = 0.75f;  //初始负载因子

/** HashMap是非线程安全的,在对HashMap进行迭代时,由于其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),都会抛出异常ConcurrentModificationException*/
transient int modCount;

代码片段:

public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold); 
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);//获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i);//新增一个entry
        return null;
    }

/*
*用于为主干数组table在内存中分配存储空间
*/
private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此处为            
      threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过
     MAXIMUM_CAPACITY,除非loadFactor大于1
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
/**
*roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如*toSize=13,则*capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.
*/
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;
    }

/**
* 添加节点
*/
 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);
    }

/*
* 扩容
**/
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);
    }

/**
* 将数据复制到新的数组中
*/
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
     //for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去(数组不存储实际数据,所以仅仅是拷贝引用而已)
        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);
          //将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

添加元素时,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容,扩容时,新建一个长度为之前数组2倍的新的数组,然后将原Entry数组中的元素全部复制到新的数组,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。

常见面试题

1、为什么Hashmap的数组长度一定是2的次幂?

从获取下标的indexFor 函数开始说

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

假如 length 为16 ,那么length 减1 就是15 ,二进制为01111 ,无论h为何值,与运算出来的数组下标都值取决于后面4位。

假设,h的低位是1010,length为16 ,indexFor 得到的index 二进制依然是1010,无论后四位的值是多少,下标都是后四位。

但是,如果length为10,length-1 的二进制为1001 ,与1010 得到的值为1000,index 为8,但是h的低位为1110 、1000、1101算出来的都是1000,这意味着进一步增加了碰撞的几率,增大了链表数量,减慢了查询的效率!这就违背算法均匀分布的要求。

所以,只要通过hash算法的h是均匀分布的,indexFor 算出的下标也肯定是均匀分布的。

从上面扩容的代码可以看到,每次扩容,都要重新生成数组的下标,length-1 的值由15(01111) 到 31(11111) ,二进制仅仅差了一位,当h对应的那一位为0时,得到的新的数组索引和老数组索引一致(大大减少了之前已经散列良好的老数组的数据位置重新调换,引用博客观点)。

2、为什么重写equals方法要重写hashCode?

       我们在进行get和put操作的时候,使用的key从逻辑上讲是等值的(通过equals比较是相等的),但由于没有重写hashCode方法,所以put操作时,key(hashcode1)-->hash-->indexFor-->最终索引位置 ,而通过key取出value的时候 key(hashcode1)-->hash-->indexFor-->最终索引位置,由于hashcode1不等于hashcode2,导致没有定位到一个数组位置而返回逻辑上错误的值null(也有可能碰巧定位到一个数组位置,但是也会判断其entry的hash值是否相等,上面get方法中有提到。)

  所以,在重写equals的方法的时候,必须注意重写hashCode方法,同时还要保证通过equals判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发生哈希冲突,应尽量避免)。

3、hashMap 和 Hashtable的区别

a. 出现时间:Hashtable产生于JDK 1.1,而HashMap产生于JDK 1.2;

b. HashMap是支持null键和null值的,而Hashtable在遇到null时,会抛出NullPointerException异常。这并不是因为Hashtable有什么特殊的实现层面的原因导致不能支持null键和null值,这仅仅是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中;

c. Hashtable是同步的,HashMap不是,也就是说Hashtable在多线程使用的情况下,不需要做额外的同步,而HashMap则不行;

d. Hashtable已经被淘汰了,自己写代码不要再使用它。

详细区别查看:http://www.importnew.com/24822.html

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值