面試必問的HashMap之100種避坑方法

歡迎關(guān)注專欄:后端架構(gòu)技術(shù)精選。里面有大量關(guān)于的Java高級架構(gòu)知識點分享,還有各種面試趣聞以及程序員身邊事,如有好文章也歡迎投稿哦。

聲明:本文以jdk1.8為主!

搞定HashMap

作為一個Java從業(yè)者,面試的時候肯定會被問到過HashMap,因為對于HashMap來說,可以說是Java==集合中的精髓==了,如果你覺得自己對它掌握的還不夠好,我想今天這篇文章會非常適合你,至少,看了今天這篇文章,以后不怕面試被問HashMap了

其實在我學習HashMap的過程中,我個人覺得HashMap還是挺復雜的,如果真的想把它搞得明明白白的,沒有足夠的內(nèi)力怕是一時半會兒做不到,不過我們總歸是在不斷的學習,因此真的不必強迫自己把現(xiàn)在遇到的一些知識點全部搞懂。

但是,對于HashMap來說,你所掌握的應(yīng)該足夠可以讓你應(yīng)對面試,所以今天咱們的側(cè)重點就是學會那些經(jīng)常被問到的知識點。

我猜,你肯定看過不少分析HashMap的文章了,那么你掌握多少了呢?從一個問題開始吧

新的節(jié)點在插入鏈表的時候,是怎么插入的?

怎么樣,想要回答這個問題,還是需要你對HashMap有個比較深入的了解的,如果僅僅知道什么key和value的話,那么回答這個問題就比較難了。

這個問題大家可以先想想,后面我會給出解答,下面我們一步步的來看HashMap中幾個你必須知道的知識點。

Map是個啥?

HashMap隸屬于Java中集合這一塊,我們知道集合這塊有l(wèi)ist,set和map,這里的HashMap就是Map的實現(xiàn)類,那么在Map這個大家族中還有哪些重要角色呢?

image

上圖展示了Map的家族,都是狠角色啊,我們對這些其實都要了解并掌握,這里簡單的介紹下這幾個狠角色:

TreeMap從名字上就能看出來是與樹有關(guān),它是基于樹的實現(xiàn),而HashMap,HashTable和ConcurrentHashMap都是基于hash表的實現(xiàn),另外這里的HashTable和HashMap在代碼實現(xiàn)上,基本上是一樣的,還記得之前在講解ArrayList的時候提到過和Vector的區(qū)別嘛?這里他們是很相似的,一般都不怎么用HashTable,會用ConcurrentHashMap來代替,這個也需要好好研究,它比HashTable性能更好,它的鎖粒度更小。

由于這不是本文的重點,只做簡單說明,后續(xù)會發(fā)文單獨介紹。

簡單來說,Map就是一個映射關(guān)系的數(shù)據(jù)集合,就是我們常見的k-v的形式,一個key對應(yīng)一個value,大致有這樣的圖示

image

這只是簡單的概念,放到具體的實例當中,比如在HashMap中就會衍生出很多其他的問題,那么HashMap又是個啥?

HashMap是個啥

上面簡單提到過,HashMap是基于Hash表的實現(xiàn),因此,了解了什么是Hash表,那對學習HashMap是相當重要。

建議了解了哈希表之后再學習HashMap,這樣很多難懂的也就不那么難理解了。

接著,HashMap是基于hash表的實現(xiàn),而說到底,它也是用來存儲數(shù)據(jù)供我們使用的,那么底層是用什么來存儲數(shù)據(jù)的呢?可能有人猜到了,還是數(shù)組,為啥還是數(shù)組?想想之前的ArrayList。

所以,對于HashMap來說,底層也是基于數(shù)組實現(xiàn),只不過這個數(shù)組可能和你印象中的數(shù)組有些許不同,我們平常整個數(shù)組出來,里面會放一些數(shù)據(jù),比如基礎(chǔ)數(shù)據(jù)類型或者引用數(shù)據(jù)類型,數(shù)組中的每個元素我們沒啥特殊的叫法。

但是在HashMap中人家就有了新名字,我發(fā)現(xiàn)這個知識點其實很多人都不太清楚:

在HashMap中的底層數(shù)組中,每個元素在jdk1.7及之前叫做Entry,而在jdk1.8之后人家又改名叫做Node。

這里可能還是會有人好奇這Entry和Node長啥樣,這個看看源碼就比較清楚了,后面我們會說。

到了這里你因該就能簡單的理解啥是HashMap了,如果你看過什么是哈希表了,你就會清楚,在HashMap中同樣會出現(xiàn)哈希表所描述的那些問題,比如:

  1. 如何確定添加的元素在底層數(shù)組的哪個位置?
  2. 怎么擴容?
  3. 出現(xiàn)沖突了怎么處理?
  4. 。。。

沒事,這些問題我們后續(xù)都會談到。

HashMap初始化大小是多少

先來看HashMap的基礎(chǔ)用法:

HashMap map = new HashMap();
復制代碼

就這樣,我們創(chuàng)建好了一個HashMap,接下來我們看看new之后發(fā)生了什么,看看這個無參構(gòu)造函數(shù)吧

public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

解釋下新面孔:

  1. loadFactor : 負載因子,之前聊哈希表的時候說過這個概念
  2. DEFAULT_LOAD_FACTOR : 默認負載因子,看源碼知道是0.75

很簡單,當你新建一個HashMap的時候,人家就是簡單的去初始化一個負載因子,不過我們這里想知道的是底層數(shù)組默認是多少嘞,顯然我們沒有得到我們的答案,我們繼續(xù)看源碼。

在此之前,想一下之前ArrayList的初始化大小,是不是在add的時候才創(chuàng)建默認數(shù)組,這里會不會也一樣,那我們看看HashMap的添加元素的方法,這里是put

public V put(K key, V value) { return putVal(hash(key), key, value, false, true);
    }

這里大眼一看,有兩個方法;

  1. putVal 重點哦
  2. hash

這里需要再明確下,這是我們往HashMap中添加第一個元素的時候,也就是第一次調(diào)用這個put方法,可以猜想,現(xiàn)在數(shù)據(jù)已經(jīng)過來了,底層是不是要做存儲操作,那肯定要弄個數(shù)組出來啊,好,離我們想要的結(jié)果越來越近了。

先看這個hash方法:

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

記得之前聊哈希表的時候說過,哈希表的數(shù)據(jù)存儲有個很明顯的特點,就是根據(jù)你的key使用哈希算法計算得出一個下標值,對吧。

而這里的hash就是根據(jù)key得到一個hash值,并沒有得到下標值哦。

重點要看這個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;
    }

咋樣,是不是感覺代碼一下變多了,我們這里逐步的有重點的來看,先看這個:

if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;?

這個table是啥?

transient Node<K,V>[] table;

看到了,這就是HashMap底層的那個數(shù)組,之前說了jdk1.8中數(shù)組中的每個元素叫做Node,所以這就是個Node數(shù)組。

那么上面那段代碼啥意思嘞?其實就是我們第一次往HashMap中添加數(shù)據(jù)的時候,這個Node數(shù)組肯定是null,還沒創(chuàng)建嘞,所以這里會去執(zhí)行resize這個方法。

resize方法的主要作用就是初始化和增加表的大小,說白了就是第一次給你初始化一個Node數(shù)組,其他需要擴容的時候給你擴容

看看源碼:

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;
    }<

感覺代碼也是比較多的啊,同樣,我們關(guān)注重點代碼:

newCap = DEFAULT_INITIAL_CAPACITY; 

有這么一個賦值操作,DEFAULT_INITIAL_CAPACITY字面意思理解就是初始化容量啊,是多少呢?

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

這里是個移位運算,就是16,現(xiàn)在已經(jīng)確定具體的默認容量是16了,那具體在哪創(chuàng)建默認的Node數(shù)組呢?繼續(xù)往下看源碼,有這么一句

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

ok,到這里我們發(fā)現(xiàn),第一次使用HashMap添加數(shù)據(jù)的時候底層會創(chuàng)建一個長度為16的默認Node數(shù)組。

那么新的問題來了?

為啥初始化大小是16

這個問題想必你在HashMap相關(guān)分析文章中也看到過,那么該怎么回答呢?

想搞明白為啥是16不是其他的,那首先要知道為啥HashMap的容量要是2的整數(shù)次冪?

為什么容量要是 2 的整數(shù)次冪?

先看這個16是怎么來的:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

這里使用了位運算,為啥不直接16嘞?這里主要是位運算的性能好,為啥位運算性能就好,那是因為位運算人家直接操作內(nèi)存,不需要進行進制轉(zhuǎn)換,要知道計算機可是以二進制的形式做數(shù)據(jù)存儲啊,知道了吧,那16嘞?為啥是16不是其他的?想要知道為啥是16,我們得從HashMap的數(shù)據(jù)存放特性來說。

對于HashMap而言,存放的是鍵值對,所以做數(shù)據(jù)添加操作的時候會根據(jù)你傳入的key值做hash運算,從而得到一個下標值,也就是以這個下標值來確定你的這個value值應(yīng)該存放在底層Node數(shù)組的哪個位置。

那么這里一定會出現(xiàn)的問題就是,不同的key會被計算得出同一個位置,那么這樣就沖突啦,位置已經(jīng)被占了,那么怎么辦嘞?

首先就是沖突了,我們要想辦法看看后來的數(shù)據(jù)應(yīng)該放在哪里,就是給它找個新位置,這是常規(guī)方法,除此之外,我們是不是也可以聚焦到hash算法這塊,就是盡量減少沖突,讓得到的下標值能夠均勻分布。

好了,以上巴拉巴拉說一些理念,下面我們看看源碼中是怎么計算下標值得:

i = (n - 1) & hash

這是在源碼中第629行有這么一段,它就是計算我們上面說的下標值的,這里的n就是數(shù)組長度,默認的就是16,這個hash就是這里得到的值:

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

繼續(xù)看它:

i = (n - 1) & hash

這里是做位與運算,接著我們還需要先搞明白一個問題

為什么要進行取模運算以及位運算

要知道,我們最終是根據(jù)key通過哈希算法得到下標值,這個是怎么得到的呢?通常做法就是拿到key的hashcode然后與數(shù)組的容量做取模運算,為啥要做取模運算呢?

比如這里默認是一個長度為16的Node數(shù)組,我們現(xiàn)在要根據(jù)傳進來的key計算一個下標值出來然后把value放入到正確的位置,想一下,我們用key的hashcode與數(shù)組長度做取模運算,得到的下標值是不是一定在數(shù)組的長度范圍之內(nèi),也就是得到的下標值不會出現(xiàn)越界的情況。

要知道取模是怎么回事??!明白了這點,我們再來看:

i = (n - 1) & hash

這里就是計算下標的,為啥不是取模運算而是位與運算呢?使用位與運算的一方面原因就是它的性能比較好,另外一點就是這里有這么一個等式:

(n - 1) & hash  =  n % hash

因此,總結(jié)起來就是使用位與運算可以實現(xiàn)和取模運算相同的效果,而且位與運算性能更高!

接著,我們再看一個問題

為什么要減一做位運算

理解了這個問題,我們就快接近為什么容量是2的整數(shù)次冪的答案了,根據(jù)上面說的,這里的n-1是為了實現(xiàn)與取模運算相同的效果,除此之外還有很重要的原因在里面。

在此之前,我們需要看看什么是位與運算,因為我怕這塊知識大家之前不注意忘掉了,而它對理解我們現(xiàn)在所講的問題很重要,看例子:

比如拿5和3做位與運算,也就是5 & 3 = 1(操作的是二進制),怎么來的呢?

5轉(zhuǎn)換為二進制:0000 0000 0000 0000 0000 0000 0000 0101

3轉(zhuǎn)換為二進制:0000 0000 0000 0000 0000 0000 0000 0011

1轉(zhuǎn)換為二進制:0000 0000 0000 0000 0000 0000 0000 0001

所以啊,位與運算的操作就是:第一個操作數(shù)的的第n位于第二個操作數(shù)的第n位如果都是1,那么結(jié)果的第n位也為1,否則為0

看懂了吧,不懂得話可以去補補這塊的知識,后續(xù)我也會單獨發(fā)文詳細說說這塊。

我們繼續(xù)回到之前的問題,為什么做減一操作以及容量為啥是2的整數(shù)次冪,為啥嘞?

告訴你個秘密,2的整數(shù)次冪減一得到的數(shù)非常特殊,有啥特殊嘞,就是2的整數(shù)次冪得到的結(jié)果的二進制,如果某位上是1的話,那么2的整數(shù)次冪減一的結(jié)果的二進制,之前為1的后面全是1

啥意思嘞,可能有點繞,我們先看2的整數(shù)次冪啊,有2,4,8,16,32等等,我們來看,首先是16的二進制是: 10000 ,接著16減一得15,15的二進制是: 1111 ,再形象一點就是:

16轉(zhuǎn)換為二進制:0000 0000 0000 0000 0000 0000 0001 0000

15轉(zhuǎn)換為二進制:0000 0000 0000 0000 0000 0000 0000 1111

再對照我給你說的秘密,看看懂了不,可以再來個例子:

32轉(zhuǎn)換為二進制:0000 0000 0000 0000 0000 0000 0010 0000

31轉(zhuǎn)換為二進制:0000 0000 0000 0000 0000 0000 0001 1111

這會總該懂了吧,然后我們再看計算下標的公式:

(n - 1) & hash  =  n % hash

n是容量,它是2的整數(shù)次冪,然后與得到的hash值做位于運算,因為n是2的整數(shù)次冪,減一之后的二進制最后幾位都是1,再根據(jù)位與運算的特性,與hash位與之后,得到的結(jié)果是不是可能是0也可能是1,,也就是說最終的結(jié)果取決于hash的值,如此一來,只要輸入的hashcode值本身是均勻分布的,那么hash算法得到的結(jié)果就是均勻的。

啥意思?這樣得到的下標值就是均勻分布的啊,那沖突的幾率就減少啦。

而如果容量不是2的整數(shù)次冪的話,就沒有上述說的那個特性,這樣沖突的概率就會增大。

所以,明白了為啥容量是2的整數(shù)次冪了吧。

那為啥是16嘞?難道不是2的整數(shù)次冪都行嘛?理論上是都行,但是如果是2,4或者8會不會有點小,添加不了多少數(shù)據(jù)就會擴容,也就是會頻繁擴容,這樣豈不是影響性能,那為啥不是32或者更大,那不就浪費空間了嘛,所以啊,16就作為一個非常合適的經(jīng)驗值保留了下來!

出現(xiàn)哈希沖突怎么解決

我們上面也提到了,在添加數(shù)據(jù)的時候盡管為實現(xiàn)下標值的均勻分布做了很多努力,但是勢必還是會存在沖突的情況,那么該怎么解決沖突呢?

這就牽涉到哈希沖突的解決辦法了,了解了哈希沖突的解決辦法之后我們還要關(guān)注一個問題,那就是新的節(jié)點在插入到鏈表的時候,是怎么插入的?

回答開篇的問題

現(xiàn)在你應(yīng)該知道,當出現(xiàn)hash沖突,可以使用鏈表來解決,那么這里就有問題,新來的Node是應(yīng)該放在之前Node的前面還是后面呢?

Java8之前是頭插法,啥意思嘞,就是放在之前Node的前面,為啥要這樣,這是之前開發(fā)者覺得后面插入的數(shù)據(jù)會先用到,因為要使用這些Node是要遍歷這個鏈表,在前面的遍歷的會更快。

為什么使用尾插法?

但是在Java8及之后都使用尾插法了,就是放到后面,為啥這樣?

這里主要是一個鏈表成環(huán)的問題,啥意思嘞,想一下,使用頭插法是不是會改變鏈表的順序,你后來的就應(yīng)該在后面嘛,如果擴容的話,由于原本鏈表順序有所改變,擴容之后重新hash,可能導致的情況就是擴容轉(zhuǎn)移后前后鏈表順序倒置,在轉(zhuǎn)移過程中修改了原來鏈表中節(jié)點的引用關(guān)系。

這樣的話在多線程操作下就會出現(xiàn)死循環(huán),而使用尾插法,在相同的前提下就不會出現(xiàn)這樣的問題,因為擴容前后鏈表順序是不變的,他們之間的引用關(guān)系也是不變的。

關(guān)于擴容

下面我們繼續(xù)說HashMap的擴容,經(jīng)過上面的分析,我們知道第一次使用HashMap是創(chuàng)建一個默認長度為16的底層Node數(shù)組,如果滿了怎么辦,那就需要進行擴容了,也就是之前談及的resize方法,這個方法主要就是初始化和增加表的大小,關(guān)于擴容要知道這兩個概念:

  1. Capacity:HashMap當前長度。
  2. LoadFactor:負載因子,默認值0.75f。

這里怎么擴容的呢?首先是達到一個條件之后會發(fā)生擴容,什么條件呢?就是這個負載因子,比如HashMap的容量是100,負載因子是0.75,乘以100就是75,所以當你增加第76個的時候就需要擴容了,那擴容又是怎么樣步驟呢?

首先是創(chuàng)建一個新的數(shù)組,容量是原來的二倍,為啥是2倍,想一想為啥容量是2的整數(shù)次冪,這里擴容為原來的2倍不正好符號這個規(guī)則嘛。

然后會經(jīng)過重新hash,把原來的數(shù)據(jù)放到新的數(shù)組上,至于為啥要重新hash,那必須啊,你容量變了,相應(yīng)的hash算法規(guī)則也就變了,得到的結(jié)果自然不一樣了。

關(guān)于鏈表轉(zhuǎn)紅黑樹

在Java8之前是沒有紅黑樹的實現(xiàn)的,在jdk1.8中加入了紅黑樹,就是當鏈表長度為8時會將鏈表轉(zhuǎn)換為紅黑樹,為6時又會轉(zhuǎn)換成鏈表,這樣時提高了性能,也可以防止哈希碰撞攻擊。

HashMap增加新元素的主要步驟

下面我們分析一下HashMap增加新元素的時候都會做哪些步驟:

  1. 首先肯定時根據(jù)key值,通過哈希算法得到value應(yīng)該放在底層數(shù)組中的下標位置
  2. 根據(jù)這個下標定位到底層數(shù)組中的元素,當然,這里可能時鏈表,也可能時樹,知道為啥吧,給你個提醒,鏈表轉(zhuǎn)紅黑樹
  3. 拿到當前位置上的key值,與要放入的key比較,是否==或者equals,如果成立的話就替換value值,并且需要返回原來的值
  4. 當然,如果是樹的話就要循環(huán)樹中的節(jié)點,繼續(xù)==和equals的判斷,成立替換,否則添加到樹里
  5. 鏈表的話就是循環(huán)遍歷了,同樣的判斷,成立替換,否則就添加到鏈表的尾部

所以啊,這里面的重點就是判斷放入HashMap中的元素要不要替換當前節(jié)點的元素,那怎么判斷呢?總結(jié)起來只要滿足以下兩點即可替換:

1、hash值相等。

2、==或equals的結(jié)果為true。

感謝閱讀

好了,到了這里就差不多了,開篇就說過HashMap可以說是Java集合的精髓了,想要徹底搞懂真心不容易,但是我們所掌握的應(yīng)該足夠應(yīng)對平常的面試,關(guān)于HashMap更多的高級內(nèi)容,后續(xù)會繼續(xù)分享。

感謝大家的閱讀,如有錯誤之處歡迎指正!

想要閱讀更多精彩內(nèi)容,可以關(guān)注我的微信公眾號:Java技術(shù)zhai,這是我的私人公眾號,專注于Java原創(chuàng),主要涉及數(shù)據(jù)結(jié)構(gòu)與算法,計算機基礎(chǔ)以及Java核心知識的講解,期待你的參與。

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

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

  • 摘要 HashMap是Java程序員使用頻率最高的用于映射(鍵值對)處理的數(shù)據(jù)類型。隨著JDK(Java Deve...
    周二倩你一生閱讀 1,375評論 0 5
  • 1. 原理 HashMap 底層是數(shù)組 + 鏈表 + 紅黑樹。 數(shù)組我們很熟悉,支持隨機訪問,所以在最優(yōu)情況下,即...
    Java天天閱讀 425評論 0 0
  • 今天我們一行18人去了天津薊縣的九山頂,那里也有玻璃棧道。一行人頂著日曬沿著山路向玻璃棧道進軍,在路上有的人就開始...
    堅持的美好閱讀 348評論 1 0
  • 下輩子,換你追我好嗎?
    沁梅閱讀 103評論 0 0
  • 昨天老二的班主任告訴我,在美術(shù)課的時候好幾跑出去了,然后老師說告訴班主任去,美術(shù)老師去請班主任,班主任來了他才進教...
    棉花糖麻麻閱讀 203評論 0 0

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