HashMap源碼全解析從一道面試題說起:請(qǐng)一行一行代碼描述下hashmap put方法

jsbintask-HashMap

本文原創(chuàng)地址,我的博客https://jsbintask.cn/2019/02/27/jdk/jdk8-hashmap-sourcecode/(食用效果最佳),轉(zhuǎn)載請(qǐng)注明出處!

前言

前陣子(估計(jì)也快半年了吧)遇到這么一個(gè)面試題:請(qǐng)一行代碼一行代碼描述下HashMap put方法。
我:。。。
哈哈,其實(shí)也沒有無(wú)語(yǔ),當(dāng)時(shí)知道HashMap的原理,數(shù)據(jù)結(jié)構(gòu),以及一些要注意的點(diǎn),沒想到面試官這么狠,所以本文的目的就是全方位的從源碼角度分析下HashMap。

注意點(diǎn)

jdk1.8相對(duì)于jdk1.7有較大改動(dòng),本次將只會(huì)詳細(xì)分析jdk1.8的代碼,對(duì)于1.7只會(huì)比較兩者不同之處。

源碼

數(shù)據(jù)結(jié)構(gòu)

jdk1.8以前,HashMap使用的是數(shù)組+鏈表的結(jié)構(gòu)存儲(chǔ)數(shù)據(jù)。如下:

HashMap

維護(hù)一個(gè)Enyry[]數(shù)組存儲(chǔ)數(shù)據(jù),當(dāng)發(fā)生hash沖突時(shí),數(shù)組節(jié)點(diǎn)則會(huì)變成一個(gè)鏈表,用于存儲(chǔ)hash沖突的數(shù)據(jù),而在jdk1.8中,這個(gè)鏈表則變成了紅黑樹,如下:
HashMap

值得注意的是,jdk8中不是一開始就使用紅黑樹維護(hù)這些hash沖突的節(jié)點(diǎn),而是當(dāng)鏈表長(zhǎng)度超過某個(gè)閾值時(shí)才將鏈表轉(zhuǎn)換為紅黑樹,在代碼分析中看到。

源碼分析

  1. 首先查看HashMap中的靜態(tài)成員常量
public class HashMap {
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    static final int TREEIFY_THRESHOLD = 8;

    static final int UNTREEIFY_THRESHOLD = 6;

    static final int MIN_TREEIFY_CAPACITY = 64;
}

各個(gè)靜態(tài)常量如圖:


HashMap

各個(gè)靜態(tài)成員常量意義已經(jīng)注明.

  1. HashMap成員變量,接著,我們看下成員變量:
public class HashMap<K, V> {

    /**
     * 一開分析的 `儲(chǔ)存數(shù)據(jù)的數(shù)組`
     */
    transient java.util.HashMap.Node<K,V>[] table;

    /**
     * 用于 **entrySet()和values()**方法,返回一個(gè)迭代器遍歷Map結(jié)構(gòu)
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * 整個(gè)hashmap 所包含的節(jié)點(diǎn)數(shù)
     */
    transient int size;

    /**
     * hashmap 的結(jié)構(gòu)修改次數(shù),比如 Put,remove的次數(shù),
     * 和上面的 迭代器配合使用,在迭代過程中,如果其它線程更改了這個(gè)值,則會(huì)拋出 `ConcurrentModificationException`異常
     */
    transient int modCount;

    /**
     * hashmap擴(kuò)容的閾值,值為 loadFactor*table.length  e.g: 0.75 * 16 = 12
     * 也就是說默認(rèn)是當(dāng)數(shù)組大小超過 12時(shí)就會(huì)進(jìn)行數(shù)組擴(kuò)容
     */
    int threshold;

    /**
     * 加載因子,默認(rèn)值上圖已經(jīng)說明
     */
    final float loadFactor;
}

其中各個(gè)值的含義已經(jīng)在注釋中說明,配合上圖默認(rèn)值更能理解。

  1. 接著我們繼續(xù)看下 Node類的數(shù)據(jù)結(jié)構(gòu):
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

很清楚,四個(gè)成員變量, key, key的hash值, key對(duì)應(yīng)的value,下一個(gè)節(jié)點(diǎn)的引用,其中鏈表的形成就是 next這個(gè)引用的作用。

  1. 好了,準(zhǔn)備條件都做好了,接下來就是分析put方法了:
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}

很清楚,通過hash(key)方法獲取到了key的hash值,然后調(diào)用了putVal()方法:

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;
        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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

這是putVal的原始方法,看起來有點(diǎn)復(fù)雜,很多操作在一行代碼中寫完,我們稍微改下寫法,為每行代碼加上注釋:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        /* 聲明本地變量 tab,p,n,i(提高性能,effective java),可以先多記兩邊,防止后面不知道變量怎么來的! */
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;

        /* 將成員變量 table 賦值給本地變量 tab,并且將tab的長(zhǎng)度賦值給本地變量 n */
        tab = table;
        if (tab != null) {
            n = tab.length;
        }

        /* 如果tab為空或者 數(shù)組長(zhǎng)度為0,進(jìn)行初始化,調(diào)用 resize()方法,并且獲取賦值后的數(shù)組長(zhǎng)度 */
        if (tab == null || n = 0) {
            tab = resize();
            n = tab.length;
        }

        /* 根據(jù)key的hash值得到當(dāng)前key在數(shù)組中的 位置,賦值給 i */
        i = (n - 1) & hash;
        /* 將i在數(shù)組中對(duì)應(yīng)的key值去除賦值給p,所以p代表當(dāng)前的key */
        p = tab[i];

        /* 判斷當(dāng)前數(shù)組中取出來的key是否為空(數(shù)組中沒有),就new一個(gè)新的節(jié)點(diǎn),并且放在這個(gè)索引 i的位置 */
        if (p == null) {
            tab[i] = newNode(hash, key, value, null);

            /* 如果不為空,那就表示已經(jīng)有這樣的hash 值已經(jīng)存在了,可能存在hash沖突 或者 直接替換原來的value */    
        } else {
            /* 聲明本地變量 e, k */
            Node<K, V> e;
            K k;

            /* 如果取出來的節(jié)點(diǎn) hash值相等,key也和原來的一樣( == 或者 equals方法為true),直接將 這個(gè)節(jié)點(diǎn) 
            * p 賦值給剛剛聲明的本地變量 e (這個(gè)操作很重要,在心中記?。?            * 另外這里還將 節(jié)點(diǎn) p的key 賦值給了本地變量 k
            * */
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
                e = p;
                
                /* 如果 hash值一樣,但不是同一個(gè) key,則表示hash沖突,接著判斷這個(gè)節(jié)點(diǎn)是不是 紅黑樹的節(jié)點(diǎn)
                 * 如果是,則生成一個(gè)紅黑樹的節(jié)點(diǎn)然后賦值給本地變量 e */
            } else if (p instanceof TreeNode) {
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);

                /* 不是紅黑樹,hash沖突了,這個(gè)時(shí)候開始擴(kuò)展鏈表 */
            } else {
                /* 聲明一個(gè)本地變量 binCount,開始遍歷 p節(jié)點(diǎn)后面的鏈表 */
                for (int binCount = 0; ; ++binCount) {
                    /* 首先將p節(jié)點(diǎn)的 next(鏈表的下一個(gè))賦值給 本地變量e */
                    e = p.next;
                    
                    /* 如果e為空,表示p指向的下一個(gè)節(jié)點(diǎn)不存在,這個(gè)時(shí)候直接將 新的 key,value放在鏈表的最末端 */
                    if (e == null) {
                        p.next = newNode(hash, key, value, null);

                        /* 放入后,還要判斷下 這個(gè)鏈表的長(zhǎng)度是否已經(jīng)大于等于紅黑樹的閾值 (前面分析靜態(tài)成員變量已經(jīng)說明), 
                        *  一旦大于,就可以變形,調(diào)用 treeifyBin方法將原來的鏈表轉(zhuǎn)化為紅黑樹 !
                        * */
                        if (binCount >= TREEIFY_THRESHOLD - 1) { // -1 for 1st
                            treeifyBin(tab, hash);
                        }

                        break;

                    }

                    /* 如果不為空,表示還沒有到鏈表的末端, 
                    將 e 賦值給 p(p的下一個(gè)節(jié)點(diǎn)賦值給p),開啟下一次循環(huán) */
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        break;
                    }
                    p = e;
                }
            }
            
            /* e不等于null,則表示 key值相等,替換原來的value即可,
             * 這里需要注意,這里不是表示 hash沖突(再觀察下前面的分析),
             * hash沖突鏈表的擴(kuò)展已經(jīng)在最后一個(gè) else完成了!
             * */
            if (e != null) { // existing mapping for key
                V oldValue = e.value;

                if (!onlyIfAbsent || oldValue == null) {
                    e.value = value;
                }

                /* 替換新值后,回調(diào)該方法(子類可擴(kuò)展) */
                afterNodeAccess(e);

                /* 返回原來的 key對(duì)應(yīng)的舊值 */
                return oldValue;
            }
        }

        /* 完成一次 put方法后,加一次 modCount,看前面成員變量分析 */
        ++modCount;

        /* 加入了新節(jié)點(diǎn),把 size 自加,并且 判斷是否已經(jīng)大于要擴(kuò)容的閾值(觀察前面成員變量分析),開始擴(kuò)容 */
        if (++size > threshold)
            resize();
        
        /* 插入新節(jié)點(diǎn)后,回調(diào)方法(子類可擴(kuò)展) */
        afterNodeInsertion(evict);
        
        /* 插入的新節(jié)點(diǎn),直接返回 null即可 */
        return null;
    }

其中所有代碼均已經(jīng)加上詳細(xì)注釋,這里值得注意的是,由于這個(gè)方法沒有任何 線程同步手段,所以不論是在查找對(duì)應(yīng)的key,還是擴(kuò)容,插入節(jié)點(diǎn),增加size,modCount等,肯定會(huì)出現(xiàn)問題(這里先預(yù)留一篇文章,ConCurrentHashMap源碼分析),所以多線程環(huán)境下,絕對(duì)不能使用HashMap,
而應(yīng)該使用ConCurrentHashMap。
當(dāng)然到了現(xiàn)在,我們那個(gè)面試題的答案也已經(jīng)能夠較為完整的回答出來了!(大笑)

  1. 上面較為詳細(xì)的分析了put方法后,我們注意到resize()方法在這個(gè)方法中起到了關(guān)鍵作用,初始化,以及擴(kuò)容。那我們接著來觀察下resize()方法:
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;
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        } else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            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;
        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;
                            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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

這個(gè)方法看起來也較為復(fù)雜,我們同樣作下簡(jiǎn)單分析:

final Node<K, V>[] resize() {
        /* 同樣聲明本地變量,得到原來的數(shù)組,提高性能 */
        Node<K, V>[] oldTab = table;
        /* 獲得數(shù)組的長(zhǎng)度 */
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        /* 獲取擴(kuò)容閾值 */
        int oldThr = threshold;
        /* 新的數(shù)組長(zhǎng)度,新的閾值 */
        int newCap, newThr = 0;

        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
                /* 將原來的數(shù)組長(zhǎng)度 * 2 判斷是否小于最大值,并且原來的數(shù)組長(zhǎng)度大于 默認(rèn)初始長(zhǎng)度(16)
                * 直接雙倍擴(kuò)容, 閾值,長(zhǎng)度都 * 2
                * */
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold

        } else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        else {               // zero initial threshold signifies using defaults
            /* 第一次調(diào)用 resize方法,初始化數(shù)組長(zhǎng)度,閾值,這里就對(duì)應(yīng)我們前面成員變量的分析了:
             * 閾值 = 加載因子 * 數(shù)組長(zhǎng)度
            * */
            newCap = DEFAULT_INITIAL_CAPACITY;
            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;

        /* 根據(jù)前面計(jì)算出來的新長(zhǎng)度,聲明一個(gè)新數(shù)組 */
        @SuppressWarnings({"rawtypes", "unchecked"})
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
        table = newTab;

        /* 開始將舊數(shù)組的長(zhǎng)度復(fù)制到新數(shù)組 */
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K, V> e;
                if ((e = oldTab[j]) != null) {
                    /* 原數(shù)組的值先置換為null,幫助gc */
                    oldTab[j] = null;

                    /* 如果節(jié)點(diǎn)的next不為空(沒有形成鏈表),直接復(fù)制到新數(shù)組 */
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    
                        /* 不為空但是已經(jīng)是 紅黑樹了,按紅黑樹規(guī)則置換 */
                    else if (e instanceof TreeNode)
                        ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);

                        /* 已經(jīng)形成鏈表了,循環(huán)將鏈表的引用到新數(shù)組,不再使用鏈表 */
                    else { // preserve order
                        /* 聲明四個(gè)引用,可以防止多線程環(huán)境下 死循環(huán)! */
                        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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        /* 最后返回新數(shù)組 */
        return newTab;
    }

注釋已經(jīng)簡(jiǎn)要說明流程,這里可以看出有數(shù)組復(fù)制以及重新計(jì)算hash的操作,所以我們?cè)趯?shí)際開發(fā)中使用HashMap的時(shí)候,最好設(shè)置一個(gè)初始容量,防止經(jīng)常擴(kuò)容操作耗費(fèi)性能!

  1. 好了,HashMap兩個(gè)關(guān)鍵方法都分析完畢了,接下來我們最后分析一個(gè)方法,get(key):
public V get(Object key) {
        Node<K, V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}

get方法首先通過 hash(key)方法獲取到了hash值,接著通過getNode(hash)方法獲取節(jié)點(diǎn),所以我們重點(diǎn)看下getNode方法:

final Node<K, V> getNode(int hash, Object key) {
        /* 聲明本地變量,提高性能 */
        Node<K, V>[] tab;
        Node<K, V> first, e;
        int n;
        K k;

        /* 本地變量賦值,n為數(shù)組長(zhǎng)度 */
        tab = table;
        if (tab != null) {
            n = tab.length;
        }
        /* 通過 hash值算出key在數(shù)組中的 位置,取出該節(jié)點(diǎn) */
        first = tab[(n - 1) & hash];

        /* 不為空,表示key在數(shù)組中存在,接下來開始遍歷鏈表獲取紅黑樹,找出具體位置 */
        if (tab != null && n > 0 && first != null) {

            /* 如果鏈表或者紅黑樹的第一個(gè)節(jié)點(diǎn) hash值,key相等,這個(gè)節(jié)點(diǎn)就是我們要找的,直接返回 */
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                
                return first;

            /* 開始遍歷鏈表 */
            if ((e = first.next) != null) {
                /* 如果是紅黑樹,直接按樹規(guī)則 查找然后返回 */
                if (first instanceof TreeNode)
                    return ((TreeNode<K, V>) first).getTreeNode(hash, key);
                do {
                    /* 遍歷鏈表找到了,返回 */
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }

        /* 最后沒有找到,直接返回null */
        return null;
    }

所有代碼均已經(jīng)加上詳細(xì)注釋,這里值得注意的是, 我們發(fā)現(xiàn)在鏈表中查找節(jié)點(diǎn)采用的是遍歷的方式,所以一旦鏈表過長(zhǎng),查找性能就較慢,這也是為什么jdk1.8會(huì)在 鏈表長(zhǎng)度超過閾值的時(shí)候?qū)㈡湵磙D(zhuǎn)換為紅黑樹的原因!(鏈表時(shí)間復(fù)雜度為O(n),紅黑樹為 O(logn).

總結(jié)

相信到了現(xiàn)在,HashMap的各類問題各位應(yīng)該都能夠明白了,我們通過閱讀源碼的方式較為詳細(xì)的分析了 HashMap(jdk1.8)中的關(guān)鍵方法(put,get,resize),明白了HashMap中的每一個(gè)成員變量,靜態(tài)常量的含義,
另外我們還通過源碼知道了多線程環(huán)境下HashMap會(huì)出現(xiàn)的問題,引申出了ConCurrentHashMap的解析,下一章,我們同樣將通過源碼解析ConCurrentHashMap!

關(guān)注我,這里只有干貨!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 如果說Java的HashMap是數(shù)組+鏈表,那么JDK 8之后就是數(shù)組+鏈表+紅黑樹組成了HashMap。 在之前...
    Java_Explorer閱讀 1,037評(píng)論 1 5
  • 簡(jiǎn)介 Java為數(shù)據(jù)結(jié)構(gòu)中的映射定義了一個(gè)接口java.util.Map,此接口主要有四個(gè)常用的實(shí)現(xiàn)類,分別是Ha...
    bbe9e62bc5ba閱讀 456評(píng)論 0 4
  • 自從升入二年級(jí)以來,夢(mèng)琪的作業(yè)我都沒有陪她一起,都是讓她自己一個(gè)人獨(dú)立完成,完成后有時(shí)候檢查的不仔細(xì),有的時(shí)...
    高夢(mèng)琪媽媽閱讀 282評(píng)論 1 5
  • 通過練習(xí) 培訓(xùn) 以及最重要的——方法 來設(shè)法增加自己的注意力 提高記憶力自己判斷力 讓自己切實(shí)比以前更聰明
    lubiaook閱讀 708評(píng)論 0 0
  • 今天是什么日子 起床:4點(diǎn)45分 不知道什么原因,簡(jiǎn)書日更晨間日記登錄不上,難道雙12的威力如此巨大,流量全用來跑...
    坤道率然閱讀 309評(píng)論 0 1

友情鏈接更多精彩內(nèi)容