HashMap源码分析(jdk1.8)

HashMap源码前前后后看了好几次,也和同事分享过好几次,每次都有新的收获。

分享也是一种提高!

本文首写于个人云笔记(点击访问),经多次修改,短期内不会有重大修改了,现发于此,有任何问题欢迎交流指正。

    本文最初借鉴于http://www.cnblogs.com/hzmark/archive/2012/12/24/HashMap.html,其基于jdk1.6,自己分析jdk1.8后,发现有很大的不同,遂记录于此。

    Java最基本的数据结构有数组和链表。数组的特点是空间连续(大小固定)、寻址迅速,但是插入和删除时需要移动元素,所以查询快,增加删除慢链表恰好相反,可动态增加或减少空间以适应新增和删除元素,但查找时只能顺着一个个节点查找,所以增加删除快,查找慢。有没有一种结构综合了数组和链表的优点呢?当然有,那就是哈希表(虽说是综合优点,但实际上查找肯定没有数组快,插入删除没有链表快,一种折中的方式吧)。一般采用拉链法实现哈希表

        JDK1.6中HashMap采用的是位桶+链表的方式,即我们常说的散列链表的方式;JDK1.8中采用的是位桶+链表/红黑树的方式,也是非线程安全的。当某个位桶的链表的长度达到某个阀值的时候,这个链表就将转换成红黑树。


1、HashMap的定义

1.1 所属包:package java.util;

1.2 导入包:

import java.io.IOException;

import java.io.InvalidObjectException;

import java.io.Serializable;

import java.lang.reflect.ParameterizedType;

import java.lang.reflect.Type;

import java.util.function.BiConsumer;

import java.util.function.BiFunction;

import java.util.function.Consumer;

import java.util.function.Function;

1.3定义:

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

2、HashMap的部分属性

    private static final long serialVersionUID = 362498820763181265L;

     //The default initial capacity - MUST be a power of two.

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //jdk1.6直接写16,这效率更快??

     // The maximum capacity,MUST be a power of two <= 1<<30.

    static final int MAXIMUM_CAPACITY = 1 << 30; // 2的30次方

    static final float DEFAULT_LOAD_FACTOR = 0.75f; //填充比,装载因子

    /**(jdk1.8新加)

     * The bin count threshold for using a tree rather than list for a

     * bin.当add一个元素到某个位桶,其链表长度达到8时将链表转换为红黑树.

     * 2< value<=8 时to mesh with assumptions in  tree removal about conversion back to plain bins upon shrinkage.

     *链表转为binCount>=TREEIFY_THRESHOLD-1,-1 for 1st。

     */ //当某个桶中的键值对数量大于8个【9个起】,且桶数量大于等于64,则将底层实现从链表转为红黑树   

       // 如果桶中的键值对达到该阀值,则检测桶数量   

    static final int TREEIFY_THRESHOLD= 8; //jdk1.8新加

     /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
    * 仅用于 TreeNode的 final void split(HashMap<K,V> map, Node<K,V>[] tabint indexint bit) {
            if  (lc <= UNTREEIFY_THRESHOLD)  
                    //太小则转为链表   
                   tab[index] = loHead.untreeify(map); 
        }

     */

    static final int UNTREEIFY_THRESHOLD = 6; //jdk1.8新加

     /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     * 链表转树时,if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize(); // 即不转为树了

     */  //当桶数量到达64个的时候,才可能将链表转为红黑树

    static final int MIN_TREEIFY_CAPACITY = 64; //jdk1.8新加

    /* ---------------- Fields -------------- */

    // jdk1.6 为 transient Entry[] table;

    transient Node<K,V>[] table; //存储元素(位桶)的数组,length power of two

    transient Set<Map.Entry<K,V>> entrySet;

    transient int size; // key-value对,实际容量

    transient int modCount; //结构改变次数,fast-fail机制

    int threshold; // 新的扩容resize临界值,当实际大小(容量*填充比)大于临界值时,会进行2倍扩容

    final float loadFactor;

Node 内部类

        是HashMap内部类(jdk1.6就是Entry),继承自 Map.Entry这个内部接口,它就是存储一对映射关系的最小单元,也就是说key,value实际存储在Node中。与1.6相比,修改了 hashCode()equals()方法【直接调用Object的hashCode、equals方法,而不是copy代码过来】,去掉了toString()、recordAccess(HashMap<K,V> m)【the value in an entry is overwritten时调用】、recordRemoval(HashMap<K,V> m)【remove entry时调用】方法。【键值对】

[java]  view plain  copy
  1. static class Node<K,V>implements Map.Entry<K,V> {  
  2.         final int hash; //结点的哈希值,不可变  
  3.         final K key;  
  4.         V value;  
  5.         Node<K,V> next; //指向下一个节点  
  6.    
  7.         Node(int hash, K key, V value, Node<K,V> next) {  
  8.             this.hash = hash;  
  9.             this.key = key;  
  10.             this.value = value;  
  11.             this.next = next;  
  12.         }  
  13.         public final K getKey()        { return key; }  
  14.         public final V getValue()      { return value; }  
  15.         public final String toString() { return key + "=" + value; }  
  16.       // 由直接实现 变为 调用Object的HashCode,实际是一样的  
  17.         public final int hashCode() {  
  18.             return Objects.hashCode(key) ^ Objects.hashCode(value);  
  19.         } //按位异或^不同为真,数a两次异或同一个数b(a=a^b^b)仍然为原值a。  
  20.         public final V setValue(V newValue) {  
  21.             V oldValue = value;  
  22.             value = newValue;  
  23.             returnoldValue;  
  24.         }  // 优化逻辑  
  25.         public final boolean equals(Object o) {//改为调用Object的equals  
  26.             if (o == this//内存地址(1.8新增)  
  27.                 return true;  
  28.             if (o instanceof Map.Entry) {//1.6中!(instanceof)返回false  
  29.                 Map.Entry<?,?> e = (Map.Entry<?,?>)o; //新加<?,?>泛型  
  30.                 if (Objects.equals(key, e.getKey()) &&  
  31.                     Objects.equals(value, e.getValue()))  
  32.                     return true;  
  33.             }  
  34.             return false;  
  35.         }  
  36.     }  
  37. /*  jdk 1.6 Entry 的equals方法 
  38. public final boolean equals(Object o) { 
  39.             if (!(o instanceof Map.Entry)) 
  40.                 return false; 
  41.             Map.Entry e = (Map.Entry)o; 
  42.             Object k1 = getKey(); 
  43.             Object k2 = e.getKey(); 
  44.             if (k1 == k2 || (k1 != null && k1.equals(k2))) { 
  45.                 Object v1 = getValue(); 
  46.                 Object v2 = e.getValue(); 
  47.                 if (v1 == v2 || (v1 != null && v1.equals(v2))) 
  48.                     return true; 
  49.             } 
  50.             return false; 
  51.         } */  


// 新增的红黑树,继承LinkedHashMap .Entry<K,V>

[java]  view plain  copy
  1. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {  
  2.         TreeNode<K,V> parent;  // red-black tree links  
  3.         TreeNode<K,V> left;  
  4.         TreeNode<K,V> right;  
  5.         TreeNode<K,V> prev; // needed to unlink next upon deletion,节点的前一个节点  
  6.         boolean red; //true表示红节点,false表示黑节点  
  7.         TreeNode(int hash, K key, V val, Node<K,V> next) {  
  8.             super(hash, key, val, next);  
  9.         }  
  10.         /** 
  11.          * Returns root of tree containing this node.获取红黑树的根 
  12.          */  
  13.         final TreeNode<K,V> root() {   
  14.            for (TreeNode<K,V> r=this, p;;){//p定义,int a=1,b;不能直接输出b(未初始化)  
  15.                 if ((p = r.parent) == null)  //若改为类似并查集的路径压缩(结构改变)  
  16.                     return r;  
  17.                 r = p;  
  18.             }  
  19.         }  
  20.         /** 
  21.          * Ensures that the given root is the first node of its bin. 
  22.          */ //确保root是桶中的第一个元素,将root移到桶中的第一个【平衡思想】  
  23.         static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {}  
  24.         /** 
  25.          * Finds the node starting at root p with the given hash and key. 
  26.          * The kc argument caches comparableClassFor(key) upon first use 
  27.          * comparing keys. 
  28.          *///查找hash为h,key为k的节点    
  29.         final TreeNode<K,V> find(int h, Object k, Class<?> kc) { // 详见get相关  
  30.               TreeNode<K,V> p = this;   ……   }  
  31.         /** 
  32.          * Calls find for root node. 
  33.          */ //获取树节点,通过根节点查找   
  34.         final TreeNode<K,V> getTreeNode(int h, Object k) { // 详见get相关  
  35.             return ((parent != null) ? root() : this).find(h, k, null);  
  36.         }  
  37.         /** 
  38.          * Tie-breaking utility for ordering insertions when equal 
  39.          * hashCodes and non-comparable. We don't require a total 
  40.          * order, just a consistent insertion rule to maintain 
  41.          * equivalence across rebalancings. Tie-breaking further than 
  42.          * necessary simplifies testing a bit. 
  43.          */ //比较2个对象的大小   
  44.         static int tieBreakOrder(Object a, Object b) {}  
  45.         /** 
  46.          * Forms tree of the nodes linked from this node. 
  47.          * @return root of tree 
  48.          */ //将链表转为二叉树  
  49.         finalvoid treeify(Node<K,V>[] tab) {} //根节点设置为黑色   
  50.         /** 
  51.          * Returns a list of non-TreeNodes replacing those linked from 
  52.          * this node. 
  53.          */ //将二叉树转为链表   
  54.         final Node<K,V> untreeify(HashMap<K,V> map) {}  
  55.         /** 
  56.          * Tree version of putVal. 
  57.          */ //添加一个键值对   
  58.         final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,  
  59.                                        inth, K k, V v) {}  
  60.         /** 
  61.          * Removes the given node, that must be present before this call. 
  62.          * This is messier than typical red-black deletion code because we 
  63.          * cannot swap the contents of an interior node with a leaf 
  64.          * successor that is pinned by "next" pointers that are accessible 
  65.          * independently during traversal. So instead we swap the tree 
  66.          * linkages. If the current tree appears to have too few nodes, 
  67.          * the bin is converted back to a plain bin. (The test triggers 
  68.          * somewhere between 2 and 6 nodes, depending on tree structure). 
  69.          */  
  70.         final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,  
  71.                                   boolean movable) {}  
  72.         /** 
  73.          * Splits nodes in a tree bin into lower and upper tree bins, 
  74.          * or untreeifies if now too small. Called only from resize; 
  75.          * see above discussion about split bits and indices. 
  76.          * 
  77.          * @param map the map 
  78.          * @param tab the table for recording bin heads 
  79.          * @param index the index of the table being split 
  80.          * @param bit the bit of hash to split on 
  81.          */ //将结点太多的桶分割    
  82.         finalvoid split(HashMap<K,V> map, Node<K,V>[] tab, intindex, intbit) {}  
  83.         /* --------------------------------------------------*/  
  84.         // Red-black tree methods, all adapted from CLR  
  85.         //左旋转  
  86.         static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,  
  87.                                               TreeNode<K,V> p) {}  
  88.         //右旋转  
  89.         static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,  
  90.                                                TreeNode<K,V> p) {}  
  91.          //保证插入后平衡,共5种插入情况  
  92.         static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,  
  93.                                                     TreeNode<K,V> x) {}  
  94.          //删除后调整平衡 ,共6种删除情况  
  95.         static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,  
  96.                                                    TreeNode<K,V> x) {}  
  97.         /** 
  98.          * Recursive invariant check 
  99.          */ //检测是否符合红黑树   
  100.         static <K,V> boolean checkInvariants(TreeNode<K,V> t) {}  
  101. }  

static final int hash(Object key) { // 计算key的hash值hash(key)

        int h;

        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

    }

n = tab.length

table的下标【bucket的index】:(n - 1) & hash


由上可知:
        key是有可能是null的,并且会在0桶位位置。
        hashCode的计算也进行了改进,取得key的hashcode后,高16位与低16位异或运算重新计算hash值。
首先由key值通过hash(key)获取hash值h,再通过 hash &(length-1)得到所在数组位置。一般对于哈希表的散列常用的方法有直接定址法,除留余数法等,既要便于计算,又能减少冲突。
        在Hashtable中就是通过除留余数法散列分布的,具体如下:  int index = (hash & 0x7FFFFFFF) % tab.length; 
但是取模中的除法运算效率很低,HashMap则通过hash &(length-1)替代取模,得到所在数组位置,这样效率会高很多。

3、HashMap的4种构造方法

[java]  view plain  copy
  1. public HashMap(int initialCapacity, float loadFactor) {  
  2.         if (initialCapacity < 0)  
  3.             throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);  
  4.         if (initialCapacity > MAXIMUM_CAPACITY)  
  5.             initialCapacity = MAXIMUM_CAPACITY;  
  6.         if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  7.             throw new IllegalArgumentException("Illegal load factor: " + loadFactor);  
  8.        this.loadFactor = loadFactor;  
  9.        this.threshold=tableSizeFor(initialCapacity);  
  10.     }  
[java]  view plain  copy
  1. public HashMap() {  
  2.         this.loadFactor = DEFAULT_LOAD_FACTOR; //all default即16;0.75  
  3.     }  
[java]  view plain  copy
  1. public HashMap(int initialCapacity) {  
  2.         this(initialCapacity, DEFAULT_LOAD_FACTOR);  
  3.     }  
[java]  view plain  copy
  1. public HashMap(Map<? extends K, ? extends V> m) { // 参数本就是Map  
  2.         this.loadFactor = DEFAULT_LOAD_FACTOR; // 0.75  
  3.         putMapEntries(m, false); // 仅putAll时传参为true  
  4.     }  

putMapEntries
[java]  view plain  copy
  1. final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {  
  2.         int s = m.size();  
  3.         if (s > 0) {  
  4.             if (table == null) {// pre-size  
  5.                 float ft = ((float)s / loadFactor) + 1.0F;  
  6.                 int t = ((ft < (float)MAXIMUM_CAPACITY) ?  
  7.                          (int)ft : MAXIMUM_CAPACITY); // 取较小值  
  8.                 if (t > threshold) // t 大于扩容临界值  
  9.                     threshold = tableSizeFor(t);  
  10.             }  
  11.             else if (s > threshold)  
  12.                 resize();  
  13.             for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {  
  14.                 K key = e.getKey();  
  15.                 V value = e.getValue();  
  16.                 putVal(hash(key), key, value, false, evict); //HashMap核心方法,后讲  
  17.             }  
  18.         }  
  19.     }  


tableSizeFor:

// 经程序测试:结果为>=cap的最小2的自然数幂(64-》64;65-》128)

static final int tableSizeFor(int cap) { //计算下次需要调整大小的扩容resize临界值

        int n = cap - 1;

        n |= n >>> 1; // >>>“类似于”除以2,高位补0;|=(有1为1)

        n |= n >>> 2; // int--4byte--32bit,共32位

        n |= n >>> 4; 

        n |= n >>> 8;

        n |= n >>> 16; // 至此后每位均为1,00001111

        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

    }

证明:

n=96=0110 0000(暂已8位为例,事实上32位)

n>>>1使1位为0,若n首位为1,则结果为1,若为0,则忽略;确保首位有值时结果为1;至此首位完毕

0110 0000 |=0011 0000》0111 0000》112

n>>>2使新的n前2位为0,0111 0000|=0011 1100》0111 1100》124

1+2+4+8+16=31

   01100000

1  00110000

=  01110000 确保1、2位为1,所以接下来移2位

2  00011100 

=  01111100 确保3、4位为1(此时1-4位均为1),所以接下来移4位

4  00000111(1)

=  01111111 以此类推

    计算新的扩容临界值,不再是JDK1.6的while循环来保证哈希表的容量一直是2的整数倍数,用移位操作取代了1.6的while循环移位(不断乘2)。HashMap的构造函数中,都直接或间接的调用了tableSizeFor函数。
    在1.6中,int capacity = 1;  while (capacity < initialCapacity)  capacity <<= 1。
    其返回值是>=cap的最小2的自然数幂。(大于等于1tableSizeFor(-111)=1)
性能优化:
       length2的整数幂保证了length - 1 最后一位(二进制表示)为1,从而保证了索引位置index即  hash & length -1)的最后一位同时有为0和为1的可能性,保证了散列的均匀性。反过来讲,若length为奇数,length-1最后一位为0,这样与h按位与【同1为1】 的最后一位肯定为0,即索引位置肯定是偶数,这样数组的奇数位置全部没有放置元素,浪费了大量空间。

简而言之:length为2的幂保证了按位与最后一位的有效性,使哈希表散列更均匀


来看一下上面调用的resize()方法,这也是HashMap中一个非常重要的方法。
与1.6相比,将舍去transfer(Entry[] newTable),直接写到resize中并优化copy逻辑,并舍去static方法indexFor(hash, table.length),将其直接写到resize中。
Node<K,V>[] resize() 
[java]  view plain  copy
  1. // Initializes or doubles table size,两倍扩容并初始化table  
  2. final Node<K,V>[] resize() {  
  3.         Node<K,V>[] oldTab = table;  
  4.         int oldCap = (oldTab == null) ? 0 : oldTab.length;  
  5.         int oldThr = threshold;  
  6.         int newCap, newThr = 0// 新容量,新阀值  
  7.         if (oldCap > 0) {  
  8.             if (oldCap >= MAXIMUM_CAPACITY) {  
  9.                 threshold = Integer.MAX_VALUE;  
  10.                 return oldTab; //到达极限,无法扩容  
  11.             }  
  12.             else if((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
  13.                      oldCap >= DEFAULT_INITIAL_CAPACITY)  
  14.                 newThr = oldThr << 1// double threshold阀值  
  15.        }  
  16.       // oldCap=0 ,oldThr>0,threshold(新的扩容resize临界值)  
  17.        else if (oldThr > 0)   
  18.            newCap = oldThr; //新容量=旧阀值(扩容临界值)  
  19.        else {     // oldCap=0 ,oldThr=0,调用默认值来初始化  
  20.          newCap = DEFAULT_INITIAL_CAPACITY;  
  21.          newThr=(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  
  22.         }  
  23.         if (newThr== 0) { //新阀值为0,则需要计算新的阀值   
  24.            float ft = (float)newCap * loadFactor;  
  25.            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);  
  26.         }  
  27.         threshold = newThr; //设置新的阀值  
  28.         @SuppressWarnings({"rawtypes","unchecked"})  
  29.             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //创建新的桶  
  30.         table = newTab;   
  31.          // table初始化,bucket copy到新bucket,分链表和红黑树  
  32.         if (oldTab != null) { // 不为空则挨个copy,影响效率!!!  
  33.             for (int j = 0; j < oldCap; ++j) {  
  34.                Node<K,V> e;  
  35.                if ((e = oldTab[j]) != null) { //先赋值再判断  
  36.                   oldTab[j] = null//置null,主动GC  
  37.                   //如果该桶只有一个元素,重新计算桶位,则直接赋到新的桶里面  
  38.                   if (e.next == null)   
  39.                 //1.6的indexFor,计算key;tableSizeFor性能优化  
  40.                     newTab[e.hash &(newCap - 1)]= e; //hash&(length-1)  
  41.                   else if (e instanceof TreeNode) // 红黑树  
  42.                      ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  
  43.                   else { //链表,preserve order保持顺序  
  44.                         //一个桶中有多个元素,遍历将它们移到新的bucket或原bucket  
  45.                         Node<K,V> loHead = null,loTail = null;//lo原bucket的链表指针  
  46.                         Node<K,V> hiHead = null, hiTail = null;//hi新bucket的链表指针  
  47.                         Node<K,V> next;  
  48.                         do {  
  49.                             next = e.next;  
  50.                             if ((e.hash & oldCap) == 0) {//还放在原来的桶  
  51.                                 if (loTail == null)  
  52.                                     loHead = e;  
  53.                                 else  
  54.                                     loTail.next = e;  
  55.                                 loTail = e; //更新尾指针  
  56.                             }  
  57.                             else {//放在新桶  
  58.                                 if (hiTail == null)  
  59.                                     hiHead = e;  
  60.                                 else  
  61.                                     hiTail.next = e;  
  62.                                 hiTail = e;  
  63.                             }  
  64.                         } while ((e = next) != null); //  
  65.                         if (loTail != null) { //原bucket位置的尾指针不为空(即还有node)  
  66.                             loTail.next = null//链表最后得有个null  
  67.                             newTab[j] = loHead;//链表头指针放在新桶的相同下标(j)处  
  68.                         }  
  69.                         if (hiTail != null) {  //放在桶 j+oldCap  
  70.                             hiTail.next = null;  
  71.                             newTab[j + oldCap] = hiHead;//j+oldCap见下  
  72.                         }  
  73.                     }  
  74.                 }  
  75.             }  
  76.         }  
  77.         return newTab;  
  78.     }  

这里详细解释一下 resize】时【链表】的变化
元素位置在【原位置】或【原位置+oldCap】
①、由前面的知识可知, table的下标【bucket的index】由( n  - 1) &  hash计算
(n为table的length,hash即hash(key)计算方式为(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16));
、假设我们length从16resize到32(以下仅写出8位,实际32位), hash(key)是不变的。
n-1:     0000 1111-----》0001 1111【高位全0,&不影响】
hash1:    0000 0101-----》0000 0101
index1:  0000 0101-----》0000 0101【index不变
hash2:    0001 0101-----》0001 0101
index2:  0000 0101-----》0001 0101【新index=5+16即原index+oldCap】

、新bucket下标:(2n - 1) & hash(key), 由于是&操作,同1为1;hash(key)不变;2n-1在原来n-1的基础上仅最高位0变1;
、so,HashMap在Resize时,只需看(新) n-1最高位对应的hash(key)位是0还是1即可,0则位置不变,1则位置变为原位置+oldCap
、如何确认(新)n-1最高位对应的hash(key)位是0还是1呢?源码给出了很巧妙的方式(e.hash & oldCap):e即Node,由put和Node构造函数相关源码可知,e.hash即为hash(key);oldCap为0001 0000(仅最高位为1);
相&为0说明e.hash最高位为0,否则为1.

总结:
1、无需重新计算Hash,节省了时间;
2、由于所计算的hash(key)位是1是0可以认为是随机的,所以将一个冲突长链表又“均分”成了两个链表,减少碰撞。

4、HashMap的方法实现

1)put相关

public put(K key, V value) {

        return putVal(hash(key), keyvaluefalsetrue);

    }

// 检测指定的key对应的value是否为null,如果为null,则用新value代替原来的null。

@Override

    public V putIfAbsent(K key, V value) {

        return putVal(hash(key), keyvaluetruetrue);

    }

核心方法: putVal ( hash , key , value , onlyIfAbsent , evict )
[java]  view plain  copy
  1. @param onlyIfAbsent if true, don't change existing value //   
  2. @param evict if false, the table is in creation mode.  
  3. @return previous value, or null if none  
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {  
  5.         Node<K,V>[] tab; Node<K,V> p; int n, i;  
  6.         if ((tab = table) == null || (n = tab.length) == 0)//table空||length为0  
  7.             n = (tab = resize()).length; // 分配空间,初始化  
  8.         if ((p = tab[i = (n - 1) & hash]) == null)//hash所在位置(第i个桶)为null,直接put  
  9.             tab[i] = newNode(hash, key, value, null);  
  10.         else {//tab[i]有元素,则需要遍历结点后再添加   
  11.             Node<K,V> e; K k;  
  12.             // hash、key均等,说明待插入元素和第一个元素相等,直接更新  
  13.             if (p.hash == hash &&  
  14.                 ((k = p.key) == key || (key != null && key.equals(k))))  
  15.                 e = p;  
  16.             else if (p instanceof TreeNode) //红黑树冲突插入  
  17.                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  
  18.             else// 链表  
  19.                 for (int binCount = 0; ; ++binCount){ //死循环,直到break  
  20.                     if ((e = p.next) == null) { //表尾仍没有key相同节点,新建节点  
  21.                         p.next = newNode(hash, key, value, null);  
  22. //若链表数量大于阀值8【9个】,则调用treeifyBin方法,仅当tab.length大于64才将链表改为红黑树  
  23.                   // 如果tab.length<64或table=null,则重构一下链表  
  24.                         if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st  
  25.                             treeifyBin(tab, hash); //binCount>=9则链表转树  
  26.                         break// 退出循环  
  27.                     }  
  28.                     if (e.hash == hash &&  
  29.                         ((k = e.key) == key || (key != null && key.equals(k))))  
  30.                         break;  // hash、key均相等,说明此时的节点==待插入节点,更新  
  31.                     p = e; //更新p指向下一个节点  
  32.                }  
  33.             }  
  34.             //当前节点e = p.next不为null,即链表中原本存在了相同key,则返回oldValue  
  35.             if (e != null) {// existing mapping for key  
  36.                 V oldValue = e.value;  
  37.                //onlyIfAbsent值为false,参数主要决定当该键已经存在时,是否执行替换  
  38.                 if (!onlyIfAbsent || oldValue == null)   
  39.                     e.value = value;  
  40.                 afterNodeAccess(e); //调用linkedHashMap,move node to last  
  41.                 return oldValue;  
  42.             }  
  43.         }  
  44.         ++modCount;  
  45.         if (++size > threshold) //++size后再检测是否到了阀值  
  46.             resize();  
  47.         afterNodeInsertion(evict);//调用linkedHashMap,true则possibly remove eldest  
  48.         return null// 原hashMap中不存在相同key的键值对,则在插入键值对后,返回null。  
  49.     }  
[java]  view plain  copy
  1. /** 
  2.  * Replaces all linked nodes in bin at index for given hash unless 
  3.  * table is too small, in which case resizes instead. 
  4.  // MIN_TREEIFY_CAPACITY=64. 
  5. // tab.length 为2的幂,表示容量,不是size。 
  6.  */ //当桶中链表的数量>=9的时候,底层则改为红黑树实现   
  7.     final void treeifyBin(Node<K,V>[] tab, inthash) {  
  8.         intn, index; Node<K,V> e;  
  9.         if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)  
  10.             resize();  
  11.         else if ((e = tab[index = (n - 1) & hash]) != null) {  
  12.             TreeNode<K,V> hd = null, tl = null;  
  13.             do {  
  14.                 TreeNode<K,V> p = replacementTreeNode(e, null);  
  15.                 if (tl == null)  
  16.                     hd = p;  
  17.                 else {  
  18.                     p.prev = tl;  
  19.                     tl.next = p;  
  20.                 }  
  21.                 tl = p;  
  22.             } while ((e = e.next) != null);  
  23.             if ((tab[index] = hd) != null)  
  24.                 hd.treeify(tab);  
  25.         }  
  26.     }  
[java]  view plain  copy
  1. // For treeifyBin  
  2.    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {  
  3.         return new TreeNode<>(p.hash, p.key, p.value, next);  
  4.     }  
  5. // 链节点替换为树  

put(K key, V value)的逻辑:
  1. 判断键值对数组tab[]是否为空或为null,是则resize(); 
  2. 根据键值key的hashCode()计算hash值得到当前Node的索引i,如果tab[i]==null【没碰撞】,直接新建节点添加,否则【碰撞】转入3 
  3. 判断当前数组中处理hash冲突的方式为红黑树还是链表(check第一个节点类型即可),分别处理。【①是红黑树则按红黑树逻辑插入;②是链表,则遍历链表,看是否有key相同的节点;③有则更新value值,没有则新建节点,此时若链表数量大于阀值8【9个】,则调用treeifyBin方法(此方法先判断table是否为null或tab.length小于64,是则执行resize操作,否则才将链表改为红黑树)。】
  4. 如果size+1> threshold则resize。

public void putAll(Map<? extends K, ? extends V> m) {

        putMapEntries(mtrue); // 详见构造方法,仅putAll参数为true

    }

Note:
        p utAll可以合并map,如果key重复,则用新值替换就值(相见Study代码)。

2)Remove、clear

[java]  view plain  copy
  1. publicV remove(Object key) {  
  2.         Node<K,V> e;  
  3.         return (e = removeNode(hash(key), key, nullfalsetrue)) == null ? null:e.value;  
  4.     }  
[java]  view plain  copy
  1. @Override  
  2.     public boolean remove(Object key, Object value) {  
  3.         return removeNode(hash(key), key, value, truetrue) != null;  
  4.     }  
[java]  view plain  copy
  1. /** 
  2.      * Implements Map.remove and related methods 
  3.      * 
  4.      * @param hash hash for key 
  5.      * @param key the key 
  6.      * @param value the value to match if matchValue, else ignored 
  7.      * @param matchValue if true only remove if value is equal 
  8.      * @param movable if false do not move other nodes while removing 
  9.                        仅HashIterator的remove方法为false 
  10.      * @return the node, or null if none 
  11.      */  
  12.     final Node<K,V> removeNode(int hash, Object key, Object value,  
  13.                                boolean matchValue, boolean movable) {  
  14.         Node<K,V>[] tab; Node<K,V> p; intn, index;  
  15.         if ((tab = table) != null && (n = tab.length) > 0 &&  
  16.             (p = tab[index = (n - 1) & hash]) != null) {  
  17.             Node<K,V> node = null, e; K k; V v;  
  18.             if (p.hash == hash &&  
  19.                 ((k = p.key) == key || (key != null && key.equals(k))))  
  20.                 node = p;  
  21.             else if ((e = p.next) != null) {  
  22.                 if (p instanceof TreeNode)  
  23.                     node = ((TreeNode<K,V>)p).getTreeNode(hash, key);  
  24.                 else {  
  25.                     do {  
  26.                         if (e.hash == hash &&  
  27.                             ((k = e.key) == key ||  
  28.                              (key != null && key.equals(k)))) {  
  29.                             node = e;  
  30.                             break;  
  31.                         }  
  32.                         p = e;  
  33.                     } while ((e = e.next) != null);  
  34.                 }  
  35.             }  
  36.             if (node != null && (!matchValue || (v = node.value) == value ||  
  37.                                  (value != null && value.equals(v)))) {  
  38.                 if (node instanceof TreeNode)  
  39.                     ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);  
  40.                 else if (node == p)  
  41.                     tab[index] = node.next;  
  42.                 else  
  43.                     p.next = node.next;  
  44.                 ++modCount;  
  45.                 --size;  
  46.                 afterNodeRemoval(node); // 空方法??  
  47.                 return node;  
  48.             }  
  49.         }  
  50.         return null;  
  51.     }  
[java]  view plain  copy
  1. // for循环,挨个置为null,GC  
  2. public void clear() {  
  3.         Node<K,V>[] tab;  
  4.         modCount++;  
  5.         if ((tab = table) != null && size > 0) {  
  6.             size = 0;  
  7.             for (int i = 0; i < tab.length; ++i)  
  8.                 tab[i] = null;  
  9.         }  
  10.     }  

Tip:
A、 HashMap貌似不存在类似于ArrayList的trimToSize()压缩空间的方法。
B、HashMap map= new HashMap(); Hashmap map=null;有什么区别?
都实例化了map;
前者创建对象、分配地址,将该地址的引用赋值给对象
后者只是创建对象,地址为空(null),并且在赋值对象之前,进行任何操作都将报 NullPointerException。

3)迭代方式、迭代时如何正确Remove
        hashmap有自己的迭代器,
① 定义  abstract  class  HashIterator  {};
其remove方式实现如下:
final void remove() 
[java]  view plain  copy
  1. public final void remove() {  
  2.          Node<K,V> p = current;  
  3.          if (p == null)  
  4.                 thrownew IllegalStateException();  
  5.          if (modCount != expectedModCount)  
  6.                 thrownew ConcurrentModificationException();  
  7.          current = null;  
  8.          K key = p.key;  
  9.          removeNode(hash(key), key, nullfalsefalse);  
  10.          expectedModCount = modCount; //同步expectedModCount和modCount的值  
  11.    }  

② 定义迭代器  EntryIterator KeyIterator 、KeyIterator 

[java]  view plain  copy
  1. final class EntryIterator extendsHashIterator  
  2.         implementsIterator<Map.Entry<K,V>> {  
  3.         publicfinal Map.Entry<K,V> next() { return nextNode(); }  
  4.   }  

③  final  class  EntrySet  extends  AbstractSet<Map.Entry<K,V>> {…… EntryIterator…… }

KeySet、 Values类似。
迭代 具体实现:
[java]  view plain  copy
  1. for (Entry<Object, Object> entry : map.entrySet()) {  
  2.             System.out.println(entry.getKey());  
  3.             // entry.remove(); // error  
  4. }  
  5. // jdk1.8 lambda迭代,key、value缺一不可  
  6. map.forEach((key, value) -> System.out.println(key + "==" + value));  
Remove正确调用方式:
[java]  view plain  copy
  1. Iterator<Entry<Object, Object>> it = map.entrySet().iterator();  
  2. while (it.hasNext()) {  
  3.      Entry<Object, Object> entry = it.next();  
  4.      Integer key = (Integer) entry.getKey();  
  5.      if (key % 2 == 0) {  
  6.           System.out.println("Delete key:" + key);  
  7.           it.remove();  
  8.           System.out.println("The key " + +key + " was deleted");  
  9.      }  
  10. }  

发散:

1、如果是遍历过程中增加或修改数据呢?
    增加或修改数据只能通过Mapput方法实现,在遍历过程中修改数据可以,但如果增加新key就会在下次循环时抛异常,因为在添加新keymodCount也会自增。

2、有些集合类也有同样的遍历问题,如ArrayList通过Iterator方式可正确遍历完成remove操作,直接调用listremove方法就会抛异常。

3jdk为什么允许通过iterator进行remove操作?
        HashMap
keySetremove方法都可以通过传递key参数删除任意的元素,而iterator只能删除当前元素(current)【movable为false,一旦删除的元素是iterator对象中next所正在引用的,如果没有通过modCount expectedModCount的比较实现快速失败抛出异常,下次循环该元素将成为current指向,此时iterator就遍历了一个已移除的过期数据。ConcurrentModificationException是RuntimeException,不要在客户端捕获它。如果发生此异常,说明程序代码的编写有问题,应该仔细检查代码而不是在catch中忽略它。

    Iterator自身的remove()方法会自动同步expectedModCount和modCount的值(见上源码)。确保遍历可靠的原则是只在一个线程中使用这个集合,或者在多线程中对遍历代码进行同步。

4)get、contains相关

[java]  view plain  copy
  1. public V get(Object key) { // 返回value或null  
  2.         Node<K,V> e;  
  3.         return (e = getNode(hash(key), key)) == null ? null : e.value;  
  4.     }  
[java]  view plain  copy
  1. // 指定key不存在则返回 defaultValue  
  2. @Override  
  3.     public V getOrDefault(Object key, V defaultValue) {  
  4.         Node<K,V> e;  
  5.         return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;  
  6.     }  
[java]  view plain  copy
  1. public boolean containsKey(Object key) {  
  2.         return getNode(hash(key), key) != null;  
  3.     }  
[java]  view plain  copy
  1. // 存在指定(1或多个)value即返回true  
  2. public boolean containsValue(Object value) {  
  3.         Node<K,V>[] tab; V v;  
  4.         if ((tab = table) != null && size > 0) {  
  5.             for (inti = 0; i < tab.length; ++i) {  
  6.                 for (Node<K,V> e = tab[i]; e != null; e = e.next) {  
  7.                     if ((v = e.value) == value ||  
  8.                         (value != null && value.equals(v)))  
  9.                         return true;  
  10.                 }  
  11.             }  
  12.         }  
  13.         returnfalse;  
  14.     }  
get相关的核心方法:

[java]  view plain  copy
  1. final Node<K,V> getNode(int hash, Object key) { // 返回Node or null  
  2.         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;  
  3.         if ((tab = table) != null && (n = tab.length) > 0 &&  
  4.             (first = tab[(n - 1) & hash]) != null) { //(n-1)&hash位置不为null  
  5.             if (first.hash == hash && // always check first node  
  6.                 ((k = first.key) == key || (key != null && key.equals(k))))  
  7.                 return first;  
  8.             if ((e = first.next) != null) {  
  9.                 if (first instanceof TreeNode) //遍历红黑树,得到节点值  
  10.                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);  
  11.                 do { //遍历链表,得到节点值,通过hash和equals(key)确认所查找元素。  
  12.                     if (e.hash == hash &&  
  13.                         ((k = e.key) == key || (key != null && key.equals(k))))  
  14.                         return e;  
  15.                 } while ((e = e.next) != null);  
  16.             }  
  17.         }  
  18.         return null;  
  19.     }  
[java]  view plain  copy
  1. // Calls find for root node.  
  2. final TreeNode<K,V> getTreeNode(int h, Object k) { // k即key  
  3.      return ((parent != null) ? root() : this).find(h, k, null);  
  4. }  
[java]  view plain  copy
  1. /** 
  2. * Finds the node starting at root p with the given hash and key. 
  3. * The kc argument caches comparableClassFor(key) upon first use 
  4. * comparing keys. 
  5. */ // getTreeNode核心方法  
  6. final TreeNode<K,V> find(int h, Object k, Class<?> kc) { // k即key,kc为null  
  7.             TreeNode<K,V> p = this;  
  8.             do {  
  9.                 int ph, dir; K pk;  
  10.                 TreeNode<K,V> pl = p.left, pr = p.right, q;  
  11.                 if ((ph = p.hash) > h) // ph存当前节点hash  
  12.                     p = pl;  
  13.                 elseif (ph < h) // 所查hash比当前节点hash大  
  14.                     p = pr; // 查右子树  
  15.                 elseif ((pk = p.key) == k || (k != null && k.equals(pk)))  
  16.                     return p; // hash、key均相同,【找到了!】返回当前节点  
  17.                 elseif (pl == null// hash等,key不等,且当前节点的左节点null  
  18.                     p = pr; // 查右子树  
  19.                 elseif (pr == null)  
  20.                     p = pl;  
  21.                // get->getTreeNode传递的kc为null。||逻辑或,短路运算,有真即可  
  22.                // false || (false && ??)  
  23.                 else if ((kc != null ||  
  24.                           (kc = comparableClassFor(k)) != null) &&  
  25.                          (dir = compareComparables(kc, k, pk)) != 0)  
  26.                     p = (dir < 0) ? pl : pr;   
  27.                 else if ((q = pr.find(h, k, kc)) != null)  
  28.                     return q; //通过右节点查找???  
  29.                 else  
  30.                     p = pl;  
  31.             } while (p != null);  
  32.             return null;  
  33.         }  
[java]  view plain  copy
  1. 看一下hashMap中的comparableClassFor的解释及部分代码:  
  2. // Returns x's Class if it is of the form "class C implements  Comparable<C>", else null.  
  3. // x实现Comparable接口则返回x的类型,否则返回null。  
  4. static Class<?> comparableClassFor(Object x) {  
  5.         if (xinstanceof Comparable) {  
  6.            ……  
  7.             if ((c = x.getClass()) == String.class// bypass checks  
  8.                 returnc;  
  9.             if ((ts = c.getGenericInterfaces()) != null) {  
  10.                 ……  
  11.                 }  
  12.             }  
  13.         }  
  14.         returnnull;  
  15.     }  
[java]  view plain  copy
  1. //Returns k.compareTo(x) if x matches kc (k's screened comparable class), else 0// 暂未理解透彻  
  2.     @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable  
  3.     static int compareComparables(Class<?> kc, Object k, Object x) { // x即pk  
  4.         return (x == null || x.getClass() != kc ? 0 :  
  5.                 ((Comparable)k).compareTo(x)); // 待查k与当前k(x)比较  
  6.     }  


V get(Object key):
        1、(n - 1) & hash计算bucket的index;【hash= hash(key)如下
        1、判断第一个节点hash、key是否相等,是则返回first【always check first node】;
        2、若(e = first.next) != null
         若first为红黑树,getTreeNode返回根节点,并调用其find方法,根据hash值判断进入左(右)子树,逐层查找;
         若为链表,遍历链表,得到节点值,通过hash和equals(key)确认所查找元素。
    3、没有该元素,返回null。

[java]  view plain  copy
  1. static final int hash(Object key) {  
  2.         inth;  
  3.         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
  4.     }  

Tip:
get、containsKey都可以用来判断map中是否存在该元素吗?
    当get()方法的返回值为null时,可能有两种情况,一种是在集合中没有该键对象,另一种是该键对象没有值本就为null。因此,在Map集合中不应该利用get()方法来判断是否存在某个元素,而应该利用containsKey()方法来判断。

5)replace相关
[java]  view plain  copy
  1. // 将指定key对应的value替换成新value。如果key不存在,返回null。  
  2. @Override  
  3.     publicV replace(K key, V value) {  
  4.         Node<K,V> e;  
  5.         if ((e = getNode(hash(key), key)) != null) {  
  6.             V oldValue = e.value;  
  7.             e.value = value;  
  8.             afterNodeAccess(e);  
  9.             return oldValue;  
  10.         }  
  11.         return null;  
  12.     }  
[java]  view plain  copy
  1. // 仅当指定key的value为oldValue时,用newValue替换oldValue  
  2. @Override  
  3.     public boolean replace(K key, V oldValue, V newValue) {  
  4.         Node<K,V> e; V v;  
  5.         if ((e = getNode(hash(key), key)) != null &&  
  6.             ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {  
  7.             e.value = newValue;  
  8.             afterNodeAccess(e);  
  9.             returntrue;  
  10.         }  
  11.         returnfalse;  
  12.     }  
[java]  view plain  copy
  1. // function? lambda,详见study代码  
  2. //计算结果作为key-value对的value值  
  3. @Override  
  4.     publicvoidreplaceAll(BiFunction<? super K, ? super V, ? extends V> function) {  
  5.         Node<K,V>[] tab;  
  6.         if (function == null)  
  7.             thrownewNullPointerException();  
  8.         if (size > 0 && (tab = table) != null) {  
  9.             intmc = modCount;  
  10.             for (inti = 0; i < tab.length; ++i) {  
  11.                 for (Node<K,V> e = tab[i]; e != null; e = e.next) {  
  12.                     e.value = function.apply(e.key, e.value);  
  13.                 }  
  14.             }  
  15.             if (modCount != mc)  
  16.                 thrownew ConcurrentModificationException();  
  17.         }  
  18.     }  
[java]  view plain  copy
  1. map.replaceAll((key, value) -> {// 其他用法???  
  2.             if ((int) key > 6) {  
  3.                 value = 99;  
  4.             }  
  5.             returnvalue; // value改变,返回value  
  6.       });  
  7. // 将所有key>6的value置为99  

6)其他函数

toString(): 返回格式如{null=1, 2=8, 3=7, 9=8}或{ }。

int size()return size。
boolean isEmpty()return size == 0。
equals() :继承于AbstractMap、Object等。

3个afterNode……空方法,仅内部使用:

// Callbacks to allow LinkedHashMap post-actions

void afterNodeAccess(Node<K,V> p) { }

void afterNodeInsertion(booleanevict) { }

void afterNodeRemoval(Node<K,V> p) { }


小结:
1、多了1300行代码(共2380), static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> 。get性能O(n)-->O(lgN)
2、尽量直接调用系统函数
    Node<K,V>中调用Object的hashCode和Equals
3、 减少不必要的内部函数调用
    resize中的transfer(Entry[] newTable)【逻辑优化】和indexFor(hash, table.length)
4、储存方式
    位桶存储;若产生hash碰撞,位桶中按链表存储;若链表达到阀值,链表转换为红黑树。
5、赋值巧妙
if ((p = tab[i = (n - 1) & hash]) == null)判断赋值两不误。
6、通过bool或instance实现代码复用
putMapEntries(Map<? extends K, ? extends V> m, boolean evict)

put、get的红黑树部分有待再次阅读分析。


部分源码注释翻译:

  1. 基于map接口;
  2. key、value允许空(仅允许一个key为null);
  3. 和HashTable类似,除了非同步和允许null;
  4. 无序,顺序可能变;
  5. get、put效率O(1),迭代器与number of buckets、size相关(所以initial很重要);如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低);
  6. capacity:buckets的大小,table被create时便实实在在的存在;
  7. number of entries达到load factor*capacity便rehashed,approximately(大约)2倍buchets;
  8.  load factor (.75)是个时间和空间的折衷,higher-->减少空间开销,增加查询开销(包括get、put);
  9. 非同步,若结构改变(add、delete,不包括修改value),必须在外部同步;考虑synchronizedMap,Map m = Collections.synchronizedMap(new HashMap(...))
  10. 迭代时remove报异常ConcurrentModificationException,不能写一个依赖他正确性的程序;
  11. 链表转红黑树后,若树变短,会恢复为链表。


有任何问题,欢迎指正探讨。
本文作者:云海(花名)

相关文章:

基于jdk1.8的HashMap源码学习笔记:http://www.cnblogs.com/ToBeAProgrammer/p/4787761.html
Java8为Map新增的方法:http://www.data321.com/6bf91e95

[php]  view plain  copy
  1. ArrayList源码分析(jdk1.8):http://blog.csdn.net/u010887744/article/details/49496093  
  2. HashMap源码分析(jdk1.8):http://write.blog.csdn.net/postedit/50346257  
  3. ConcurrentHashMap源码分析--Java8:http://blog.csdn.net/u010887744/article/details/50637030  
  4. ThreadLocal源码分析(JDK8) :http://blog.csdn.net/u010887744/article/details/54730556  
  5.   
  6. 每篇文章都包含 有道云笔记地址,可直接保存。  
  7.   
  8. 在线查阅JDK源码:  
  9. JDK8:https://github.com/zxiaofan/JDK1.8-Src  
  10. JDK7:https://github.com/zxiaofan/JDK_Src_1.7  
  11.   
  12. 史上最全Java集合关系图:http://blog.csdn.net/u010887744/article/details/50575735  
[javascript]  view plain  copy
  1. 欢迎个人转载,但须在文章页面明显位置给出原文连接;  
  2. 未经作者同意必须保留此段声明、不得随意修改原文、不得用于商业用途,否则保留追究法律责任的权利。  
  3.   
  4. 【 CSDN 】:csdn.zxiaofan.com  
  5. 【GitHub】:github.zxiaofan.com  
  6.   
  7. 如有任何问题,欢迎留言。祝君好运!  
  8. Life is all about choices!   
  9. 将来的你一定会感激现在拼命的自己!  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值