摘要:概述主要來存放鍵值對。之前使用數(shù)組鏈表的形式,之后進行了改變,使用了數(shù)組鏈表或者紅黑樹的形式。如果為,則按照字段中保存的初始容量進行分配。并且之前在中的元素應(yīng)呆在原處或者移動到倍位置處。
概述
HashMap主要來存放鍵值對。JDK1.8之前使用數(shù)組+鏈表的形式,JDK1.8之后進行了改變,使用了數(shù)組+鏈表或者紅黑樹的形式。
小概念普及 關(guān)系運算簡介0 0 | 0 1 | 1 1 | |
---|---|---|---|
與 & | 0 | 0 | 1 |
或 | 0 | 1 | 1 |
異或 ^ | 0 | 1 | 0 |
非~ ~1=0 ~0=1
成員變量/** * The default initial capacity - MUST be a power of two. * 默認的初始容量,必須是2的次冪 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. * 最大容量 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. * 默認的負載因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. * 紅黑樹閾值,鏈表元素個數(shù)大于等于此值則轉(zhuǎn)化為紅黑樹 */ static final int TREEIFY_THRESHOLD = 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. * 普通鏈表閾值,紅黑樹元素個數(shù)小于等于此值則轉(zhuǎn)化為普通鏈表 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 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. * 桶中結(jié)構(gòu)轉(zhuǎn)化為紅黑樹對應(yīng)的table的最小大小 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) * 存儲數(shù)據(jù)的桶數(shù)組,數(shù)組大小總是2的次冪。 */ transient Node構(gòu)造方法[] table; /** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). * 存放具體元素的set */ transient Set > entrySet; /** * The number of key-value mappings contained in this map. * map中的key-value個數(shù) */ transient int size; /** * The number of times this HashMap has been structurally modified * Structural modifications are those that change the number of mappings in * the HashMap or otherwise modify its internal structure (e.g., * rehash). This field is used to make iterators on Collection-views of * the HashMap fail-fast. (See ConcurrentModificationException). * HashMap的擴容和修改次數(shù)計數(shù)器 用于判斷fail-fast */ transient int modCount; /** * The next size value at which to resize (capacity * load factor). * 下一次擴容的閾值,元素個數(shù)到達此閾值即擴容 * @serial */ // (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this // field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) // 此外,如果table還沒有被分配,則此值為初始容量或者0 int threshold; /** * The load factor for the hash table. * 負載因子 * @serial */ final float loadFactor;
/** * Constructs an empty HashMap with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public HashMap(int initialCapacity, float loadFactor) { //判斷初始容量是否小于0,小于0則報錯 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //判斷初始容量是否大于最大容量,大于則置為最大容量 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //判斷負載因子是否小于等于0 是否是一個數(shù)字 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //為負載因子賦值 this.loadFactor = loadFactor; //為擴容閾值賦值(tableSizeFor函數(shù)用于找到大于initialCapacity的最近的2的次冪) this.threshold = tableSizeFor(initialCapacity); } /** * Constructs an empty HashMap with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { //參數(shù)只有初始容量,使用默認負載因子,并調(diào)用另一個構(gòu)造方法 this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * Constructs an empty HashMap with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { //無參數(shù) 則只制定默認負載因子 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** * Constructs a new HashMap with the same mappings as the * specified Map. The HashMap is created with * default load factor (0.75) and an initial capacity sufficient to * hold the mappings in the specified Map. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */ public HashMap(Map extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } /** * Returns a power of two size for the given target capacity. * 根據(jù)傳入的值返回一個2的次冪 */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
可以看出以上的所有的初始化過程都沒有對talbe進行初始化。并且在傳入initialCapacity的構(gòu)造函數(shù)中對threshold進行了初始化,所以threshold除了記錄擴容閾值之外,還在HashMap初始化時記錄初始容量或直接置為0。
node的數(shù)據(jù)結(jié)構(gòu)/** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */ static class Nodehash計算方法implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node 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() { //key與value的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) { 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; } }
/** * Computes key.hashCode() and spreads (XORs) higher bits of hash * to lower. Because the table uses power-of-two masking, sets of * hashes that vary only in bits above the current mask will * always collide. (Among known examples are sets of Float keys * holding consecutive whole numbers in small tables.) So we * apply a transform that spreads the impact of higher bits * downward. There is a tradeoff between speed, utility, and * quality of bit-spreading. Because many common sets of hashes * are already reasonably distributed (so don"t benefit from * spreading), and because we use trees to handle large sets of * collisions in bins, we just XOR some shifted bits in the * cheapest possible way to reduce systematic lossage, as well as * to incorporate impact of the highest bits that would otherwise * never be used in index calculations because of table bounds. */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }put 普通的put方法
可見普通的put方法僅僅是接收了key value參數(shù)并調(diào)用了putVal方法
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * 在map中創(chuàng)建key與value的對應(yīng)關(guān)系,如果map中之前已經(jīng)存在key的對應(yīng)關(guān)系,則之前的對應(yīng)關(guān)系會被替換。 * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with key, or * null if there was no mapping for key. * (A null return can also indicate that the map * previously associated null with key.) */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }putAll方法
putAll是直接調(diào)用了putMapEntries方法
/** * Copies all of the mappings from the specified map to this map. * These mappings will replace any mappings that this map had for * any of the keys currently in the specified map. * 從傳入的map中復(fù)制所有的對應(yīng)關(guān)系到當(dāng)前map,如果一個key值在傳入的map和當(dāng)前map中皆有對應(yīng)關(guān)系, 則可能會覆蓋當(dāng)前map中的對應(yīng)關(guān)系會被覆蓋。 * @param m mappings to be stored in this map * @throws NullPointerException if the specified map is null */ public void putAll(Map extends K, ? extends V> m) { putMapEntries(m, true); }
/** * Implements Map.putAll and Map constructor * 把傳入的map加入本HashMap,用于Map.putAll或者構(gòu)造map * @param m the map 傳入的map * @param evict false when initially constructing this map, else * true (relayed to method afterNodeInsertion). 如果是初始化構(gòu)造時 使用為false,其余時候為true */ final void putMapEntries(Map extends K, ? extends V> m, boolean evict) { //獲取傳入map的大小 int s = m.size(); //如果傳入的map有元素則進入 if (s > 0) { //如果本HashMap尚未初始化 if (table == null) { // pre-size // float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } //如果本HashMap已經(jīng)初始化但是傳入map的大小大于了當(dāng)前的擴容閾值則調(diào)整map的大小 //注意此處是用傳入的map大小與當(dāng)前map的threshold進行比較 //理論上說應(yīng)該用當(dāng)前map的大小與傳入map的大小的和與threshold進行比較 //在jdk1.7版本中有如下一段注釋來解釋這個行為 /* * Expand the map if the map if the number of mappings to be added * is greater than or equal to threshold. This is conservative; the * obvious condition is (m.size() + size) >= threshold, but this * condition could result in a map with twice the appropriate capacity, * if the keys to be added overlap with the keys already in this map. * By using the conservative calculation, we subject ourself * to at most one extra resize. */ /*當(dāng)待加入的映射關(guān)系個數(shù)大于threshold時對map進行擴容。這是一個保守的方法,很顯然判斷條件應(yīng)該是(m.size()+size)>=threshold,不過這個條件可能會讓map的容量比實際需要容量大一倍,因為在傳入的map中可能會有和當(dāng)前map重復(fù)的key(重復(fù)的key會被覆蓋,所以實際容量會比m.size()+size小).所以使用保守的計算方法,最多進行一次額外的擴容。 */ else if (s > threshold) //調(diào)整map大小 resize(); //循環(huán)向添加當(dāng)前map添加原map中數(shù)據(jù) for (Map.Entry extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }put方法的最終函數(shù)putVal
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don"t change existing value 如果為true則不修改已經(jīng)存在的值 * @param evict if false, the table is in creation mode. 如果為false則進入創(chuàng)建模式(初始化) * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Nodeget[] tab; Node p; int n, i; //如果table沒有初始化或者tab的長度為0則初始化table if ((tab = table) == null || (n = tab.length) == 0) //初始化table并取得table的長度 n = (tab = resize()).length; //取得tab中放入的位置的值為p,如果p為null則hash沒有沖突 直接放入 //n為2的次冪 n-1為一個全1項 與hash與可得到一個小于n的比較均勻的分布值 //例如n為32 hash為40則有如下運算 //n :00000000000000000000000000100000 //n-1:00000000000000000000000000011111 //40 :00000000000000000000000000101000 //(n-1)&40:00000000000000000000000000001000 = 8 //故放入table[8]中 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //如果放入的位置當(dāng)前有值則進行鏈表或紅黑樹的插入 else { Node e; K k; //如果key與當(dāng)前p中key相同則找到了賦值的位置,的把p值直接賦值給e以供最后統(tǒng)一賦值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果p為一個樹節(jié)點 則進入樹節(jié)點處理流程 else if (p instanceof TreeNode) e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); //如果不為樹節(jié)點 則進入鏈表處理流程 else { //循環(huán)遍歷鏈表 for (int binCount = 0; ; ++binCount) { //如果遍歷到了鏈表的末尾 if ((e = p.next) == null) { //新建節(jié)點并插入到表尾 p.next = newNode(hash, key, value, null); //如果鏈表長度等于了樹化閾值則進行樹化 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //判斷當(dāng)前節(jié)點的key是否與傳入的key相同,相同則直接結(jié)束循環(huán)(找到了賦值的位置) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //判斷e是否為null 即是否是找到了key相同的歷史映射 如果在面直接插入了新映射此處e應(yīng)為null if (e != null) { // existing mapping for key //取得舊值 V oldValue = e.value; //如果onlyIfAbsent為false即修改已經(jīng)存在的值 或者oldValue為null則重新賦值 if (!onlyIfAbsent || oldValue == null) e.value = value; //HashMap中無用 afterNodeAccess(e); return oldValue; } } //修改技術(shù)增加 ++modCount; //調(diào)整size的值并判斷是否大于了擴容閾值 如果大于擴容閾值則進行擴容 if (++size > threshold) resize(); //HashMap中無用 afterNodeInsertion(evict); return null; }
/** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * *擴容More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * *
A return value of {@code null} does not necessarily * indicate that the map contains no mapping for the key; it"s also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */ public V get(Object key) { Node
e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node getNode(int hash, Object key) { Node [] tab; Node first, e; int n; K k; //如果table被初始化并且長度大于0且key中有值則進入判斷否則返回null if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //如果table中的值的key 與傳入的key相同則直接返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //如果table中的值key與傳入的key不同則進入后續(xù)的數(shù)據(jù)結(jié)構(gòu)進行判斷 if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode )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); } } return null; }
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * 初始化table大小或者對table大小進行翻倍。如果table為null,則按照threshold 字段中保存的初始容量進行分配。如果table不為null,由于使用的是翻倍增加策略,則對 table容量進行翻倍。并且之前在table中的元素應(yīng)呆在原處或者移動到2倍位置處。 * @return the table */ final Node[] resize() { //緩存當(dāng)前table為oldTab Node [] oldTab = table; //獲取oldTab的大小,為oldCap int oldCap = (oldTab == null) ? 0 : oldTab.length; //獲取當(dāng)前的擴容閾值 int oldThr = threshold; //初始化新的table大小與閾值為0 int newCap, newThr = 0; //如果oldCap大于0,即當(dāng)前map中的table存有值 if (oldCap > 0) { //判斷oldCap是否大于等于最大容量 if (oldCap >= MAXIMUM_CAPACITY) { //如果oldCap大于等于了最大容量則把擴容閾值賦為int的最大值 //給一個足夠大的值,以后盡量不再觸發(fā)擴容 threshold = Integer.MAX_VALUE; //由于oldCap已經(jīng)是最大值,故不再擴容 直接返回原table return oldTab; } //如果oldCap小于最大容量(即不滿足上一個判斷條件) //把oldCap的翻倍值賦給newCap并判斷newCap的容量是否小于最大容量(此處newCap可能大于最大容量) //如果newCap小于最大容量并且oldCap大于等于初始容量則把閾值翻倍 //todo 此處有個問題 為什么oldCap小于默認初始容量不行 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //此處因為容量翻了一倍,閾值是容量*負載因子 所以閾值翻一倍即可(為了減少計算進行的性能優(yōu)化,不然其實可以放到下面進行統(tǒng)一的閾值計算) newThr = oldThr << 1; // double threshold } //如果oldCap等于0,則證明table未初始化 //此時如果oldThr大于0 則證明threshold中存儲的是table的初始化長度(詳情參見構(gòu)造方法部分,帶有initialCapacity的構(gòu)造方法會把initialCapacity賦給threshold進行緩存) else if (oldThr > 0) // initial capacity was placed in threshold //把oldThr存的初始長度賦給newCap newCap = oldThr; //如果oldCap為0并且oldThr為0 則當(dāng)前map使用不帶參數(shù)的構(gòu)造方法創(chuàng)建,且未進行過put和get操作(因為使用過則table會被初始化) else { // zero initial threshold signifies using defaults //新容量為初始化容量 newCap = DEFAULT_INITIAL_CAPACITY; //新閾值為初始化容量*負載因子 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //如果新閾值為0 //表示在上面的第一個判斷中newCap大于了最大容量或者oldCap小于了DEFAULT_INITIAL_CAPACITY //即不滿足如下的條件:else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //或者進入了上面的第二個判斷( else if (oldThr > 0) ) if (newThr == 0) { //計算新的閾值 float ft = (float)newCap * loadFactor; //如果新容量小于最大容量并且計算的閾值小于最大容量則使用計算后的負載因子 //如果新容量大于等于了最大容量或者計算得到的閾值大于了最大容量則新閾值置為int的最大值(給一個足夠大的值,以后盡量不再觸發(fā)擴容) newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //更新當(dāng)前map的擴容閾值 threshold = newThr; //以下是table擴容并重新賦值的邏輯 @SuppressWarnings({"rawtypes","unchecked"}) //首先根據(jù)newCap創(chuàng)建新的table Node [] newTab = (Node [])new Node[newCap]; //把當(dāng)前map的table置為新創(chuàng)建的table table = newTab; //如果之前的table被初始化過(table可能存有值) if (oldTab != null) { //遍歷oldTab for (int j = 0; j < oldCap; ++j) { Node e; //把當(dāng)前位置的node賦給e,如果e不為null,表示當(dāng)前node中有值 if ((e = oldTab[j]) != null) { //把舊table中值置為null(防止內(nèi)存泄露) oldTab[j] = null; //如果e.next是null 表示當(dāng)前桶中只有一個值,把當(dāng)前值放入相應(yīng)位置即可 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //如果e為樹節(jié)點則進入樹節(jié)點重分配相關(guān)函數(shù) else if (e instanceof TreeNode) ((TreeNode )e).split(this, newTab, j, oldCap); //進入鏈表節(jié)點重分配邏輯 else { // preserve order //當(dāng)前節(jié)點的鏈表 Node loHead = null, loTail = null; //偏移后的鏈表 Node hiHead = null, hiTail = null; Node 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; }
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/74613.html
摘要:當(dāng)鏈表長度即將超過閥值,會把鏈表轉(zhuǎn)化為紅黑樹。然后再判斷是鏈表還是紅黑樹如果值相同,并且相同表示數(shù)組中第一個元素即為相同的將數(shù)組中第一個元素賦值給如果當(dāng)前元素類型為表示為紅黑樹,返回待存放的。 前提:學(xué)習(xí)HashMap的底層代碼之前,首先要對數(shù)據(jù)結(jié)構(gòu)要個大致的了解。其中重點了解數(shù)組,鏈表,樹的概念和用法。 一.圖示分析HashMap的結(jié)構(gòu) (1)圖示為JDK1.8之前的HashMap結(jié)...
摘要:之前,其內(nèi)部是由數(shù)組鏈表來實現(xiàn)的,而對于鏈表長度超過的鏈表將轉(zhuǎn)儲為紅黑樹。非線程安全,即任一時刻可以有多個線程同時寫,可能會導(dǎo)致數(shù)據(jù)的不一致。有時兩個會定位到相同的位置,表示發(fā)生了碰撞。 原文地址 HashMap HashMap 是 Map 的一個實現(xiàn)類,它代表的是一種鍵值對的數(shù)據(jù)存儲形式。 大多數(shù)情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。 HashM...
摘要:值得位數(shù)有的次方,如果直接拿散列值作為下標(biāo)訪問主數(shù)組的話,只要算法比較均勻,一般是很難出現(xiàn)碰撞的。但是內(nèi)存裝不下這么大的數(shù)組,所以計算數(shù)組下標(biāo)就采取了一種折中的辦法,就是將得到的散列值與數(shù)組長度做一個與操作。 hashMap簡單介紹 hashMap是面試中的高頻考點,或許日常工作中我們只需把hashMap給new出來,調(diào)用put和get方法就完了。但是hashMap給我們提供了一個絕佳...
閱讀 3274·2021-11-23 09:51
閱讀 939·2021-09-03 10:30
閱讀 3212·2021-08-31 09:40
閱讀 3278·2019-08-30 14:22
閱讀 902·2019-08-30 14:09
閱讀 2900·2019-08-30 13:21
閱讀 3232·2019-08-28 18:03
閱讀 2859·2019-08-26 13:44