【Java学习】从源码层面彻底搞懂HashMap(Java8)



一、属性

//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;

//最大容量
static final int MAXIMUM_CAPACITY = 1073741824;

//默认加载因子,当插入,MAXIMUM_CAPACITY/DEFAULT_LOAD_FACTOR(最大/加载因子)此时就该扩容了,加载因子越大空间利用越多。
static final float DEFAULT_LOAD_FACTOR = 0.75F;

//树枝的阈值,java8之前解决hash冲突是通过链表的方式,java8中引入了红黑树,当某个hash节点冲突大于【TREEIFY_THRESHOLD】,使用红黑树,从而大大的提高查找效率
static final int TREEIFY_THRESHOLD = 8;

//当扩容时,桶中元素个数小于这个值就会把树形的桶元素还原(切分)为链表结构
static final int UNTREEIFY_THRESHOLD = 6;

//当哈希表中的容量大于这个值时,表中的桶才能进行树形化否则桶内元素太多时会扩容,而不是树形化为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

//第一次使用时初始化,是resize必须品,分配时,长度应该是2的幂(允许成度为0)
transient HashMap.Node<K, V>[] table;

//保留EntrySet,将节点最为【Set<Entry<K, V>>】形式
transient Set<Entry<K, V>> entrySet;

//hashMap中键值对的个数
transient int size;

//用于判断,多线程下访问使用迭代器遍历hashMap时,别的线程修改了map的内容,【解释,modCount指的是更新次数,迭代器遍历时,可以判断是否被修改】
transient int modCount;

//下一个大小值,用于扩容(容量负载因素)。HashMap的size大于threshold时会执行resize操作【threshold=capacity*loadFactor】,还可以加载多少
int threshold;

//加载因子,上面那个是默认的加载因子,这个是实际中使用到的
final float loadFactor;



   /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)

        hash表中每个节点,使用更多的entries,看下文的TreeNode 标准类,和在链表HashMap中的Entry标准类
        此处为经常提到entrty,这是因为,hash表解决hash冲突的方法不止一个,此处用的时链表node
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; //hash 值,
        final K key;//用来匹配,同一个hash值可能有多个node,座椅保留key
        V value;//值
        Node<K,V> next;//下一个节点。

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {//比较两个Entrty是否相等。
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }



二、构造方法

/**
带两个参数的构造,var1是初始容量,var2是加载因子
【初始容量】【加载因子】指的是什么,请看上面文
【tableSizeFor】看下面详细解释
*/
public HashMap(int var1, float var2) {
    if(var1 < 0) {//初始容量不能小于0,否则抛出异常
        throw new IllegalArgumentException("Illegal initial capacity: " + var1);
    } else {//最大容量为1073741824,大于最大容量都是【1073741824】
        if(var1 > 1073741824) {
            var1 = 1073741824;
        }
        //【isNaN】方式写的过于精确,过于精确是没必要的
        if(var2 > 0.0F && !Float.isNaN(var2)) {
            this.loadFactor = var2;
            this.threshold = tableSizeFor(var1);
        } else {
            throw new IllegalArgumentException("Illegal load factor: " + var2);
        }
    }
}

/**
带一个参数的构造方法,var指的是初始容量,
this(var1, 0.75F);使用默认加载因子进行构造,【此处一引用形式写可能会更好】
*/
public HashMap(int var1) {
    this(var1, 0.75F);
}

/**
不带参的构造方法,直接实例一个对象,
*/
public HashMap() {
    this.loadFactor = 0.75F;
}

public HashMap(Map<? extends K, ? extends V> var1) {
    this.loadFactor = 0.75F;
    this.putMapEntries(var1, false);
}

【tableSizeFor】求表的大小
【1】大于等于var0(大于等于
【2】最小的二的幂

   static final int tableSizeFor(int var0) {
        int var1 = var0 - 1;
        var1 |= var1 >>> 1;
        var1 |= var1 >>> 2;
        var1 |= var1 >>> 4;
        var1 |= var1 >>> 8;
        var1 |= var1 >>> 16;
        return var1 < 0?1:(var1 >= 1073741824?1073741824:var1 + 1);
    }

这里写图片描述



三、成员方法


【1】hash()

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

【扰动函数】(参考知乎
此函数主要用于计算hash值,通过hash值和当前hash表size()取“与”,即可得到对hash表中插入的位置。


【2】comparableClassFor()判断某个类是否实现,Comparable接口,

/**
判断某个类是否实现,Comparable接口,

*/
static Class<?> comparableClassFor(Object var0) {
    if(var0 instanceof Comparable) {//对象的比较需要实现comparable接口
        Class var1;
        if((var1 = var0.getClass()) == String.class) {//String默认实现了,comparable
            return var1;
        }

        Type[] var2;
        if((var2 = var1.getGenericInterfaces()) != null) {//遍历var0类的所有接口,同时判断泛型的情况
            for(int var6 = 0; var6 < var2.length; ++var6) {
                Type[] var3;
                Type var4;
                ParameterizedType var5;
                //var4是【ParameterizedType参数化类型】,参数化类型为Comparable.class,并且【ActualTypeArguments真是参数类型】只有Comparable,一般泛型只会传一个。
                if((var4 = var2[var6]) instanceof ParameterizedType && (var5 = (ParameterizedType)var4).getRawType() == Comparable.class && (var3 = var5.getActualTypeArguments()) != null && var3.length == 1 && var3[0] == var1) {
                    return var1;
                }
            }
        }
    }

    return null;
}



【3】compareComparables()判断两个对象是否属于var0类,同时比较两个对象


/**判断两个对象是否属于var0类,同时比较两个对象**/
static int compareComparables(Class<?> var0, Object var1, Object var2) {
    return var2 != null && var2.getClass() == var0?((Comparable)var1).compareTo(var2):0;
}



【4】tableSizeFor()//上文有详细说明



【5】putMapEntries()

/** 
put一个map,
var1即输入的map
var2,只在最后一句代码中使用到,下面分析putVal()的时候详解
*/
final void putMapEntries(Map<? extends K, ? extends V> var1, boolean var2) {
    int var3 = var1.size();//插入map的长度
    if(var3 > 0) {
        if(this.table == null) {
            float var4 = (float)var3 / this.loadFactor + 1.0F;//计算需要的容量,为什么除以loadFactor?假设总容量为n,容量达到n*loadfactor时,就准备扩容了,所以此处需要对var3 / this.loadFactor,至于为什么转换成float,除法么,整形的话误差太大。
            int var5 = var4 < 1.07374182E9F?(int)var4:1073741824;//【1.07374182E9F】不是1.几,,注意看后面的E9,,这个十个很大的数,【MAXIMUM_CAPACITY = 1073741824;】
            if(var5 > this.threshold) {
                this.threshold = tableSizeFor(var5);//重新计算容量,很奇怪,为什么不考虑原来table的大小尼?上面有个条件【if(this.table == null)】,也就是当前table为空的时候才执行这个操作。
            }
        } else if(var3 > this.threshold) {//【threshold】,上面内容有详解
            this.resize();
        }

        //将put的 map中的内容依次插入map中。
        Iterator var8 = var1.entrySet().iterator();

        while(var8.hasNext()) {//迭代器方式遍历map更高效一点
            Entry var9 = (Entry)var8.next();
            Object var6 = var9.getKey();
            Object var7 = var9.getValue();
            this.putVal(hash(var6), var6, var7, false, var2);
        }
    }

}



【6】size()返回当前map的长度/大小

   public int size() {
        return this.size;
    }



【7】isEmpty()判断当前map是为有内容,返回boolean

public boolean isEmpty() {
    return this.size == 0;
}



【8】get(),,通过key查找对应的value

public V get(Object var1) {
    HashMap.Node var2;
    //【getNode()下面分析】,此处根据输入的【var1】作为key值,获取节点的内容,如果查到节点了,返回节点的value,否则返回null
    return (var2 = this.getNode(hash(var1), var1)) == null?null:var2.value;
}



【9】getNode(int var1, Object var2),是一个比较核心的方法。

/**
上面get()方法通过key查找对应的value,此处根据hash值和object查找对应的节点(包含,key和value的节点,节点还包含hash,以及next Node)

有个疑问?此处为什么有了hash值还需要价格【var2】(也就是传入key的对象)
hash表并不能保证每个hash值对应一个node,实际使用中经常会出现,hash冲突的现象,hashMap采用链表的方式解决hash冲突,也就是说,hash值查出来的,可能有多个。

*/
final HashMap.Node<K, V> getNode(int var1, Object var2) {
    HashMap.Node[] var3 = this.table;//关于table属性,上文有详细的解释
    HashMap.Node var4;
    int var6;
    //表不为空,且长度 > 0,【var3[var6 - 1 & var1]】这个很巧妙,正常情况下,输入的hash值通过这个求的是 var3[var1],但是异常情况下,var1输入的大于var6,此时就会出现数组下标溢出的现象。var6是表的长度,也就是2的幂,减一之后从原来数字最左侧的1开始右边的数全变成1了,不懂的会看上文的插图。
    if(this.table != null && (var6 = var3.length) > 0 && (var4 = var3[var6 - 1 & var1]) != null) {
        Object var7;
        if(var4.hash == var1) {//判断一下第一个node是不是需要的
            var7 = var4.key;
            //这个判断还是有点意思的,没有括号,从左往右依次执行,第一个判断是为了判断是不是为空(对象是不能直接比的,所以只有都为null是才会为true),紧接着判断如果不能与null就需要用到equals判断,,很巧妙很简洁。
            if(var4.key == var2 || var2 != null && var2.equals(var7)) {
                return var4;
            }
        }

        HashMap.Node var5 = var4.next;
        if(var4.next != null) {
            if(var4 instanceof HashMap.TreeNode) {//这种是切换成红黑树的节点管理方式,通过getTreeNode()返回结果
                return ((HashMap.TreeNode)var4).getTreeNode(var1, var2);
            }

            do {//单链表的方式查找
                if(var5.hash == var1) {
                    var7 = var5.key;
                    if(var5.key == var2 || var2 != null && var2.equals(var7)) {
                        return var5;
                    }
                }
            } while((var5 = var5.next) != null);
        }
    }

    return null;
}



【10】containsKey(Object var1)//判断key有木有对应内容

public boolean containsKey(Object var1) {
    //判断key有木有对应内容,直接使用key去查,返回结果不为空就说明有值
    return this.getNode(hash(var1), var1) != null;
}



【11】put(K var1, V var2),插入key和value

public V put(K var1, V var2) {
    //直接调用的putVal(hash(var1), var1, var2, false, true)方法完成插入。
    return this.putVal(hash(var1), var1, var2, false, true);
}



【12】putVal(int var1, K var2, V var3, boolean var4, boolean var5),插入元素,

/**
插入元素
【1】var1 指的是传入的hash值
【2】var2 指的是传入的key值
【3】var3 指的是传入的value值
【4】var4 指的是,如果key重复了,是否保留不覆盖,falst为覆盖
【5】var5

*/
final V putVal(int var1, K var2, V var3, boolean var4, boolean var5) {
    HashMap.Node[] var6 = this.table;//获取hash表
    int var8;
    if(this.table == null || (var8 = var6.length) == 0) {
        var8 = (var6 = this.resize()).length;   //表为空或者长度为0时进行扩容
    }

    Object var7;
    int var9;
    if((var7 = var6[var9 = var8 - 1 & var1]) == null) {//【var6[var9 = var8 - 1 & var1])】上文【9】有提到,hash表中的某个值为空,则直接插入即可。
        var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
    } else {//不为空,此时出现hash冲突。
        Object var10;
        label79: {
            Object var11;
            if(((HashMap.Node)var7).hash == var1) {
                var11 = ((HashMap.Node)var7).key;
                if(((HashMap.Node)var7).key == var2 || var2 != null && var2.equals(var11)) {//存在相同的key,即key重复不用新建节点,直接覆原来key的value
                    var10 = var7;//为下面覆盖value做准备
                    break label79;
                }
            }

            if(var7 instanceof HashMap.TreeNode) {//如果是红黑树的管理方式则使用红黑书的方式获取
                var10 = ((HashMap.TreeNode)var7).putTreeVal(this, var6, var1, var2, var3);
            } else {
                int var12 = 0;

                while(true) {
                    var10 = ((HashMap.Node)var7).next;
                    if(((HashMap.Node)var7).next == null) {
                        //var7的下一个节点为空则直接新建一个插入即可,形式同链表
                        ((HashMap.Node)var7).next = this.newNode(var1, var2, var3, (HashMap.Node)null);
                        if(var12 >= 7) {
                            this.treeifyBin(var6, var1);
                        }
                        break;
                    }

                    if(((HashMap.Node)var10).hash == var1) {//这个其实判断的是,是否之前插入过,如果差如果,直接跳出。
                        var11 = ((HashMap.Node)var10).key;
                        if(((HashMap.Node)var10).key == var2 || var2 != null && var2.equals(var11)) {
                            break;
                        }
                    }

                    var7 = var10;//链表的遍历,,p = p->next
                    ++var12;//计数,如果大于7了就需要执行treeifyBin()。
                }
            }
        }

        if(var10 != null) {//var10保存的是插入的节点的引用。
            Object var13 = ((HashMap.Node)var10).value;
            if(!var4 || var13 == null) {//判断是否覆盖当前结点的value
                ((HashMap.Node)var10).value = var3;
            }

            this.afterNodeAccess((HashMap.Node)var10);
            return var13;
        }
    }

    ++this.modCount;//修改此处加1,保证并发情况下不会出现数据上的异常(会抛出异常)
    if(++this.size > this.threshold) {//判断当前长度情况,是否需要扩容
        this.resize();
    }

    this.afterNodeInsertion(var5);
    return null;
}



刚刚才发现,为什么jdk源码的遍历都是这么不规范,,var0–var10,,原来我看的是class文件,( ▼-▼ ),啊。。。。。!

不过很幸运发现反汇编的一个秘密,上面的就不改了(对于像我这样英语水平很次的人,变量名都不是重点,)




【13】resize(),hashmap的扩容机制是很经典的,仅看代码可能有点片面了,之后加上图解,详细分析。

/**
Hashmap扩容机制

*/
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//存放原来的table,下文成为【老table】
    //获取老table的长度,有个疑问为什么搞这么复杂,直接.length不行吗?,想想还真不行,空指针。
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //老临界值
    int oldThr = threshold;
    //新长度,以及新临界值
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {//老table长度是不会大于MAXIMUM_CAPACITY,最多等于,此处只是为了防止特殊情况,写的大于等于。这种情况下不进行扩容。
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)//扩容之后的长度不能大于最大长度,而且也不能小于默认的初始长度。
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) 
        newCap = oldThr;
    else {               
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        //【如何计算临界值threshold】DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
    }
    if (newThr == 0) {//临界值如果等于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;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {//遍历所有节点
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;//节点的值已经赋值给e,此处赋值给null为了释放该对象。
                if (e.next == null)//该表节点中只有一个节点,【无hash冲突】
                    newTab[e.hash & (newCap - 1)] = e;//直接防置到扩容后hash表的对应位置。
                else if (e instanceof TreeNode)//如果是红黑树则执行红黑书的方法。
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 不是红黑树,且有hash冲突,需要把这个节点后面的链表重新放到扩容后的hash表中
                    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) {//此处时分巧妙【oldcap是2的幂】,此处其实只判断了一位,也就是【newCap - 1】最左侧的那个1,,在这里多想想,为什么?
                        //假如每个都拿出来取【e.hash & (newCap - 1)】也是可以的,和去【e.hash & oldCap】的结果一样,将链表分为两块。,,,给设计者跪了,巧妙到爆
                            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);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;//插入位置也是很巧妙,【j + oldCap】和【j】只差了一位,想想是吧。
                    }
                }
            }
        }
    }
    return newTab;
}



【14】treeifyBin,jdk8中加入了红黑树管理为解决hash冲突使用的链表,提高查找效率。

/**
用红黑树来解决,由于hash冲突过于频繁引起链表长度较长的情况,提高查找效率(链表的查找效率为n)
当链表长度大于8是就会触发此方法,过于小时会使用链表进行管理
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {//节点的hash值是一样的,
    int n, index; 
    Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//过于小时,会重新扩容【扩容不仅仅指的是增大】
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {//这一步感觉有点随机,也就是判断一下,tab[]中的节点不为空,随机判断一个不为空,就代表全不为空。
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);//复位红黑树的节点,博主也不精通红黑书( ▼-▼ )
            if (tl == null)//第一次必然为null,下面的就是红黑树的插入,插入之后通过上面这个【replacementTreeNode】做调整
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);//遍历所有节点
        if ((tab[index] = hd) != null)//这个应该是取根节点。即访问树的根。
            hd.treeify(tab);
    }
}



【15】putAll,插入一个map,
【原理】是遍历然后对节点依次做插入操作。

 /**
 没啥说的就是调用另一个方法,上面有对 putMapEntries()方法的详解
 */
 public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }



【16】remove,删除操作,removeNode这个方法下面做详细分析。

/**
删除某个key及其对应的value
通过【removeNode】节点删除,返回删除节点的value(如果等于null)
*/
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;         
}



【17】
removeNode()删除节点

   /**
     * Implements Map.remove and related methods
     * 实现 Map.remove的方法
     * @param hash hash for key key的hash值
     * @param key the key
     * @param value the value to match if matchValue, else ignored,用value去匹配
     * @param matchValue if true only remove if value is equal  如果时true值删除value相等的
     * @param movable if false do not move other nodes while removing 如果是false,删除的时候不移动其他节点。
     * @return the node, or null if none,返回删除的节点,如果为空则返回null
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {//index = (n - 1) & hash,,由于n时tab.length的,也就是2的幂,减一后二进制全为1,,此处就是为了防止下标越界。
            Node<K,V> node = null, e; K k; V v;//【null, e;】还有这种写法,第一次见
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)//是否是红黑树
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {//是链表
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {//matchValue为true时才判断值是否相同,如果是false删除节点值可以不同
                if (node instanceof TreeNode)//红黑树的删除
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)//链表的删除
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;//hashmap操作此处统计
                --size;
                afterNodeRemoval(node);//啥都没干,可能时为未来扩展留用。
                return node;
            }
        }
        return null;
    }



【18】
clear() ,清空所有的节点,和mapping

    /**
     * Removes all of the mappings from this map.
     * The map will be empty after this call returns.

     从map中删除所有的映射,
     */
    public void clear() {
        Node<K,V>[] tab;
        modCount++;//操作次数依然需要改变
        if ((tab = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;//遍历清空,等于null后,GC会回收相应的资源
        }
    }



【19】
containsValue(Object value)

    /**
     * Returns <tt>true</tt> if this map maps one or more keys to the
     * specified value.
     * 允许一个或多个key对应一个value
     * @param value value whose presence in this map is to be tested//待测试value
     * @return <tt>true</tt> if this map maps one or more keys to the
     *         specified value
     */
    public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))//遍历每一个node,判断有无value,,第一次遇见就返回true
                        return true;
                }
            }
        }
        return false;
    }



【20】

/**
返回当前map对应key的set,,不会返回null
*/
public Set<K> keySet() {
    Set<K> ks;
    return (ks = keySet) == null ? (keySet = new KeySet()) : ks;//如果没有则新建一个
}



【21】
有时间待续……



【22】



【23】

“`



【24】



【85】



【26】



【27】



【28】

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鼠晓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值