Java集合HashMap学习笔记

156 阅读5分钟

最近在学习java集合之一的hashMap,为了加深记忆,记录一下笔记。

一、什么是哈希表

哈希表又叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构。在哈希表中进行添加、删除、查找等操作,性能十分高,在不考虑哈希碰撞的情况下,时间复杂度为O(1)。

二、HashMap

HashMap是用哈希表+链表+红黑树实现的map类。它继承了AbstractMap,而AbstractMap实现了Map接口

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

这里比较有趣的地方是,HashMap的父类AbstractMap已经实现了Map接口,但是源码中HashMap类又实现了一次Map接口,这一步其实是多余的,集合的源码作者也承认了这里写错了,但是大佬就是人性,不改!

HashMap并非线程安全,如果同时存在多个线程写入数据,可能会造成数据不一致。

HashMap的数组部分被称作哈希桶,当链表数据长度超过8时候,会以红黑树形式存储,当长度降低到6的时候,转换会链表形式

链表时间复杂度O(n) 红黑树时间复杂度O(log n)

1246351-7f2191e121927c1a.webp (图片来源:www.jianshu.com/p/fb282d3d2…

简单使用HashMap

HashMap<Integer, String> hm = new HashMap<>();
System.out.println(hm.put(1, "小白"));
System.out.println(hm.put(2, "小红"));
System.out.println(hm.put(3, "小蓝"));
System.out.println(hm.put(1, "小绿"));
System.out.println(hm.put(4, "小黑"));
System.out.println("hashmap = " + hm);
System.out.println("hashmap 大小:" + hm.size());

输出结果:

null
null
null
小白
null
hashmap = {1=小绿, 2=小红, 3=小蓝, 4=小黑}
hashmap 大小:4

通过代码发现,hm的size是4,而不是5,hm.put(1,"小绿")返回了小白。为什么呢? 带着疑问查看下源码:

·HaspMap类中比较重要的属性

//默认的初始化数组容量 1<<4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大的数组容量 
static final int MAXIMUM_CAPACITY = 1 << 30;

//装载因子,代表了数组的填充度有多少,默认是0.75
//至于为什么是0.75,源码给出的解析是
//(As a general rule, the default load factor (.75) offers a good
//tradeoff between time and space costs. Higher values decrease the
//space overhead but increase the lookup cost )
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//用来接受用户传进来的装载因子参数,默认情况就是DEFAULT_LOAD_FACTOR
final float loadFactor;

//底层主数组
transient Node<K,V>[] table;

//记录数组中添加的元素数量
transient int size;

//阈值,当size超过这个值后,数组会扩容
int threshold;

//用来记录HashMap内部结构发生变化的次数
transient int modCount;

//超过8时候,会转成用红黑树存储
static final int TREEIFY_THRESHOLD = 8;

·算出hash值的方法

//向右移了16,将高位降低下来,降低了函数的离散度,减少出现哈希碰撞
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

·Map.put方法实现

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        //初始化table或扩容
        n = (tab = resize()).length;
    
    //i = (n - 1) & hash 算出哈希桶中的下标
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    
                    //如果bin数量超过8,则采用红黑树形式存储
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        //这里如果已经存在的情况,则直接把新的val赋值进去,返回旧的val,揭秘了hm.put(1,"小绿")为什么返回了小白
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    
    //数组内的元素大于阈值,进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    
    //put成功后返回null,System.out.println(hm.put(2, "小红"))为什么返回null的原因
    return null;
}

关于计算下标:i = (n - 1) & hash

我上面的代码使用的Key是Integer,假如key=1的时候,hash值是1, 运算后下标就是1,假如key=2,则下标是2

key=1        0000 0001
n-1=15     & 0000 1111
             0000 0001

key=2        0000 0010
n-1=15     & 0000 1111
             0000 0010

·初始化table或扩容的方法

//resize主要用于两种情况
//1.初始化table
//2.在table大小超过threshold之后进行扩容
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    //超过最大容量时,不再扩容,直接返回
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        
        //扩容,oldCap << 1 变成原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
                 
            //向左移一位,增加两倍,每次扩容都是2的N次幂
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               
        //新初始化的hashMap,默认的大小是16
        newCap = DEFAULT_INITIAL_CAPACITY;
        //默认的阈值是16*0.75 = 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    //算出默认容量情况下的阈值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    
    //扩容情况下,把原来table的数据搬到扩容后的新table
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                
                //如果链中没有数据,则直接存到新表中
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                    
                //如果是红黑树,则拆分树
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                    //这里定义两个链表
                    //一个用来记录低位的数据,一个用来记录高位数据
                    //分别用loHead和loTail指向它的头节点和尾节点
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //拆分的条件
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    
                    //如果lo链表不为空,则将整个链表数据放到新table上
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //如果hi链表不为空,则将整个链表数据放到新table上
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

e.hash & oldCap == 0, 证明元素在新表的下标与旧表的下标一致,都是j,
e.hash & oldCap == 1,证明元素在新表的下标是j+oldCap

最后一张图,是我学习理解画的结构图

微信图片_20220318202520.png

总结

1.hashMap初始默认长度是16,扩容阈值是16*0.75=12,当size的值超过了阈值,表就会进行扩容
2.创建表会发生在第一次调用put方法时候,如果table不存在则创建
3.hashMap每次扩容都是2的次幂
4.扩容后旧表的数据会重新装载到新表,下标也会有一定关系 5.添加元素时候会,如果是原来的已经存在的key,则替换val,返回旧的val,如果发生哈希冲突,则往链表末端追加,当binCount超过8时候,则改变成红黑树形式存储

最后,观看源码使我学习到了很多,加深了对数据结构的认识,对位运算的使用有了深刻的认知,加油!