zoukankan      html  css  js  c++  java
  • java集合-HashMap

    大纲:

    1. 数据结构
    2. 主要成员变量
    3. 主要方法

    一、数据结构

    1.1Node节点

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//key的hash值
        final K key;
        V value;
        Node<K,V> next; //下一个node节点
    }

    HashMap是一个存放键值对的集合,而最终存放键值对的地方就是这个Node,它是HashMap的嵌套类。是一个单链表节点。

    java8还新增了数节点、这里暂不讨论树化。这里的node为链表上的node,不影响理解hashmap的插入和扩容原理。

    1.2table数组

    transient Node<K,V>[] table;

    HashMap中一个table数组存放着一条条Node链表。

    这个数组+链表,组成了HashMap的内部结构。

    1.3HashMap结构示意图,图中黑点为Node节点。

    二、主要成员变量

    /**
     * HashMap默认参数
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认数组大小16。
    static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认装载因子。
    static final int MAXIMUM_CAPACITY = 1 << 30; //默认数组最大容量。
    static final int TREEIFY_THRESHOLD = 8; //默认树化最小节点数。
    static final int UNTREEIFY_THRESHOLD = 6;//默认链表化最大节点数,当树的节点数减少到这个数字的时候则将树链表化。
    static final int MIN_TREEIFY_CAPACITY = 64; //默认树化时,最小数组数量。当数组数量大于这个值时,才会出现节点链表转成红黑树,小于等于这个值时直接扩容。
    
    /**
     * 主要成员变量
     */
    transient Node<K,V>[] table;//哈希桶数组,存放一个个链表的数组。参考1.3中图。默认大小16。
    transient Set<Map.Entry<K,V>> entrySet;//Entry集合。
    transient int size;//键值对数量。
    transient int modCount;//hashMap被修改的次数,比如put,remove。用于迭代器操作中,如果hashMap被修改,则抛出异常,快速失败。
    
    //装载因子。默认0.75。
    //用于控制table数组中非null的数量与table.length的比例,超过一定比例table数组需要扩容。这个值越大空间占用越小,查询与插入时间越多;这个值越小,空间占用越大,,查询与插入时间越少。
    final float loadFactor;
    //当前装载因子下算出的允许键值对的最大容量,超过这个量则table扩容。threshold=loadFactor*size
    int threshold;

    三、重要函数

    3.1构造函数

    public HashMap() //空参,装载因子和容量都为默认值。
    
    public HashMap(int initialCapacity) //指定容量,内部会计算得到一个大于等于指定容量值的2的次方数(table数组的数量一定是2的次方)。
    
    public HashMap(int initialCapacity, float loadFactor) //指定容量,指定装载因子,但不建议修改装载因子,0.75是经验值,已经权衡时间和空间的结果。

    3.2hash与node位置

    hash:

        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

    根据key运算hash值
    1.(h = key.hashCode())获取key的hashcode
    2.将hashcode右移16位,异或上原来的hashcode。这样可以使高16位的hashcode也参与到运算当中。

    node位置:

    put操作首先要确定node存放在table数组的位置,会看到这样的操作

    tab[i = (n - 1) & hash] //(n - 1) & hash就是计算数组下标 ,tab是table、n是table.length、hash就是上面计算出的hash值

    首先n是2的次方数,n-1转化为二进制一定是一个高位全0,低位全1的数。

    将这个数&hash实际上就是保留hash的后多少位。
    例:
    (n-1):默认n=16 ,16-1=15 ,15二进制:00000000 00000000 00000000 00001111
    假设hash值的后四位是1010
    (n-1)&hash = 00000000 00000000 00000000 00001010
    1010十进制是5,因此计算出数组下标为5。

    小结:
    1.获取key的hashcode。
    2.将高16位也参与hash运算。
    3.计算存入table数组的下标,其实就是取模运算。(位运算效率比取模高)

    3.3put

        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
        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)
                n = (tab = resize()).length;
            //根据key的hash值计算对应的桶数组下标
            //检查对应桶数组该下标位置是否为null,为null则新增一个node,不为null进入下面else部分
            if ((p = tab[i = (n - 1) & hash]) == null) //p节点为table对应下标的节点
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                //头结点的hash与插入node的hash相同,头结点赋值给e。
           //在后面判断这个e是否为null,不为null说明同key(hashcode相同,key相等)
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果p节点为红黑树则进行树插入,有hash相同的节点就返回hash相同的节点 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //遍历链表所有节点 for (int binCount = 0; ; ++binCount) { //p节点下一个节点赋值给e //如果p的下一个节点为空,p为链表尾部,尾插一个节点,break,此时e为null if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) //树化 treeifyBin(tab, hash); break; } //e的hash与新插入的相同,break if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //下一个节点 p = e; } } //已经存在的key-value对的key的hash和本次插入的相等,e为老节点 //1.覆盖原有的key的value //2.返回老值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //iterator用的 //当key-value个数超出threshod则扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); //LinkedHashMap用的 return null; }

    put流程:

    1. 桶数组为空,初始化数组。
    2. 根据key计算桶数组位置,这个位置为空,则直接插入一个节点。
    3. 若不为空,进入插入链表和插入树的流程。
    4. 若在遍历链表的过程中发生相同key(hashcode && equals),将原来节点的value替换为新的value,将老的value返回。

    3.4resize

    final Node<K,V>[] resize() {
            //初始化table部分略,计算成员变量略...
            Node<K,V>[] oldTab = table;
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//newCap为原来数组长度2倍
            table = newTab;
            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 { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                //(e.hash & oldCap) == 0,注意这里是&oldCap。计算下标的时候是位运算,而且数组长度为2的n次。
                                // 因此oldCap二进制为00000001、00000010、00000100这样的形式,结合上面章节计算&lenth-1就能明白,这个操作是判断新增的一位是不是0。
                                // 如果是0表示,下标不变,是1,表示下标为(j+oldCap)
                                if ((e.hash & oldCap) == 0) {//下标不变的
                                    if (loTail == null)//数组第j个位置第一次插入,记录头
                                        loHead = e;
                                    else
                                        loTail.next = e;//尾插法
                                    loTail = e;//记录tail
                                }
                                else {//下标变的
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;//有些非尾节点可能被排到尾节点,所以他们next置为null
                                newTab[j] = loHead;
                            }
                            //同上
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;//下标为(j+oldCap),这个是位运算的结果
                            }
                        }
                    }
                }
            }
            return newTab;
        }

    resize流程:

    1. 重新计算主要成员变量。
    2. 新建长度为原来2倍的数组。
    3. 通过位运算&oldCap就能算出,节点下标是否在原位或者在原位+oldCap的地方。
    4. 尾插法重新将节点插入对应位置,这里暂不讨论树节点的变化。

    tip:尾插法是java1.8优化的,1.7用的是头插法,多线程的情况下会出现环状链表的情况。假设2个node在同一个链表上,resize后还是同一个链表,2个线程,a线程resize在(头结点.next=table[n])地方阻塞,b线程完成resize,a继续,这时头结点.next指向的就是resize好的原来的链表,由于头插会翻转原来链表的顺序,头结点.next指向了原来的尾节点,而resize好的尾节点的next指向的是原来的头结点,这样就形成了环状链表。

  • 相关阅读:
    MongoDB 释放磁盘空间 db.runCommand({repairDatabase: 1 })
    RK 调试笔记
    RK Android7.1 拨号
    RK Android7.1 移植gt9271 TP偏移
    RK Android7.1 定制化 itvbox 盒子Launcher
    RK Android7.1 双屏显示旋转方向
    RK Android7.1 设置 内存条作假
    RK Android7.1 设置 蓝牙 已断开连接
    RK Android7.1 进入Camera2 亮度会增加
    RK 3128 调触摸屏 TP GT9XX
  • 原文地址:https://www.cnblogs.com/liuboyuan/p/11263149.html
Copyright © 2011-2022 走看看