java hashmap 改变value的值_get方法


HashMap的工作原理是什么

1.HashMap的数据结构(jdk1.8之前):(数组+链表)底层是一个数组,数组的每一项是一个链表,每次新建一个map其实就是新建了一个数组。


java hashmap 改变value的值_get方法_02



2.链表:每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Entry节点。其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。


transient Entry[] table;
 
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;
        }
}


来一个非常形象的图(有点模糊,看个大概意思)


java hashmap 改变value的值_数组_03



3.HashMap的存储:HashMap的主要方法就是get方法,也是最复杂的。这里涉及到的具体哈西算法我也没有深入研究,只研究了大概。先上代码。


public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value); //null总是放在数组的第一个链表中
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果key在链表中已存在,则替换为新value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 如果i索引处的Entry为null,表明此处还没有Entry。
        modCount++;
        // 将key、value添加到i索引处。
        addEntry(hash, key, value, i);
        return null;


当调用了hashmap的put方法时

--①判断key值是否为空

----如果key值是null的话,则将它的value放到数组的第一个位置

--②key值不是null,计算key.hashcode()的hash()值,这一步是为了元素在数组的各位置上能够均匀散列分布。

--③使用indexFor()方法,结果为此元素在table中的位置i

--④这个位置上的链表

----如果 i 索引处的 Entry 不为 null,key值相同替换value,key值不同将新Entry插在链表头部

下面是addEntry()


void addEntry(int hash, K key, V value, int bucketIndex) {  
    // 获取指定 bucketIndex 索引处的 Entry   
    Entry<K,V> e = table[bucketIndex];  
    // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry  
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
    // 如果 Map 中的 key-value 对的数量超过了极限  
    if (size++ >= threshold)  
    // 把 table 对象的长度扩充到原来的2倍。  
        resize(2 * table.length);  
}



4.get方法:如果put理解,那么get就很容易了


public V get(Object key) {  
    if (key == null)  
        return getForNullKey();  
    int hash = hash(key.hashCode());  
    for (Entry<K,V> e = table[indexFor(hash, table.length)];  
        e != null;  
        e = e.next) {  
        Object k;  
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
            return e.value;  
    }  
    return null;  
}


首先依然是判断key值是否为空,如果不为空的话,计算key.hashcode()的hash(),得到在数组中的位置,循环链表,取出对应的value值。


5.面试:面试的时候,除了HashMap的工作原理,还问过我几个问题,在这里整理一下。

工作原理:

  • HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值。
  • 其它关于HashMap比较重要的问题是容量、负荷系数和阀值调整。HashMap默认的初始容量是16,负荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你知道你需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对HashMap进行初始化是个不错的做法。

hashCode()和equals()方法有何重要性:

  • HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则
  • (1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。
  • (2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。

为什么重写equals()的时候一定要重写hashcode():

  • HashMap中,如果要比较key是否相等,要同时使用这两个函数!因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地 址,这样即便有相同含义的两个对象,比较也是不相等的,例如,生成了两个“羊”对象,正常理解这两个对象应该是相等的,但如果你不重写hashcode()方法的话,则比较是不相等的。

总结

以上均为互联网搜索下的产物,肯定有不对的地方,也有不清楚的地方,由于hashmap很乱,深入研究起来十分复杂,如果有不对的地方将及时修正和补充。