Java數據結構問題

目錄介紹

  • 3.0.0.1 在arrayList中System.arraycopy()和Arrays.copyOf()方法區別聯繫?System.arraycopy()和Arrays.copyOf()代碼說明?
  • 3.0.0.3 Collection集合和Map集合的區別?Map集合的特色?說下Map集合總體結構?
  • 3.0.0.4 Java集合框架中有哪些類?都有什麼特色?集合框架用到Collection接口,這個接口有何特色?
  • 3.0.0.5 ArrayList添加元素時如何擴容?如何添加元素到指定位置,該操做複製是深拷貝仍是淺拷貝?
  • 3.0.0.6 如何理解ArrayList的擴容消耗?Arrays.asList方法後的List能夠擴容嗎?ArrayList如何序列化?
  • 3.0.0.7 如何理解list集合讀寫機制和讀寫效率?什麼是CopyOnWriteArrayList,它與ArrayList有何不一樣?
  • 3.0.0.8 如何理解Java集合的快速失敗機制 「fail-fast」?出現這個緣由是什麼?有何解決辦法?
  • 3.0.0.9 LinkedList集合有何特色?LinkedList相比ArrayList效率如何?用什麼進行論證?
  • 3.0.1.0 HashSet和TreeSet的區別?是如何保證惟一值的,底層怎麼作到的?
  • 3.0.1.3 HashMap有哪些特色,簡單說一下?HashMap內部的結構是怎樣的?簡單說一下什麼是桶,做用是什麼?
  • 3.0.1.4 當有鍵值對插入時,HashMap會發生什麼 ? 對於查找一個key時,HashMap會發生什麼 ?
  • 3.0.1.5 HashMap和Hashtable的區別?HashMap在put、get元素的過程?體現了什麼數據結構?
  • 3.0.1.6 如何保證HashMap線程安全?底層怎麼實現的?HashMap是有序的嗎?如何實現有序?
  • 3.0.1.7 HashMap存儲兩個對象的hashcode相同會發生什麼?若是兩個鍵的hashcode相同,你如何獲取值對象?
  • 3.0.1.8 HashMap爲何不直接使用hashCode()處理後的哈希值直接做爲table的下標?
  • 3.0.1.9 爲何HashMap中String、Integer這樣的包裝類適合做爲K?若是要用對象最爲key,該如何操做?
  • 3.0.2.0 HashMap是如何擴容的?如何理解HashMap的大小超過了負載因子定義的容量?從新調整HashMap大小存在什麼問題嗎?
  • 3.0.2.1 HashMap是線程安全的嗎?多線程條件下put存儲數據會發生什麼狀況?如何理解它併發性?
  • 3.0.2.2 TreeMap集合結構有何特色?使用場景是什麼?將"aababcabcdabcde"打印成a(5)b(4)c(3)d(2)e(1)?
  • 3.0.2.3 說一下HashSet集合特色?如何存儲null值的?HashSet是如何去重操做?手寫產生10個1-20之間的隨機數要求隨機數不能重複案例?

好消息

  • 博客筆記大彙總【15年10月到至今】,包括Java基礎及深刻知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug彙總,固然也在工做之餘收集了大量的面試題,長期更新維護而且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計500篇[近100萬字],將會陸續發表到網上,轉載請註明出處,謝謝!
  • 連接地址:github.com/yangchong21…
  • 若是以爲好,能夠star一下,謝謝!固然也歡迎提出建議,萬事起於忽微,量變引發質變!全部博客將陸續開源到GitHub!

Java博客大彙總,全部筆記開源到GitHub上

  • 01.Java基礎[30篇]
  • 02.面向對象[15篇]
  • 03.數據結構[27篇]
  • 04.IO流知識[11篇]
  • 05.線程進程[9篇]
  • 06.虛擬機[5篇]
  • 07.類的加載[7篇]
  • 08.反射原理[12篇]
  • 09.Java併發[17篇]
  • 10.Java異常[11篇]
  • 11.枚舉與註解[5篇]
  • 12.設計模式[8篇]
  • 13.Java深刻[7篇]
  • 閱讀更多請點擊:Java博客彙總

3.0.0.1 在arrayList中System.arraycopy()和Arrays.copyOf()方法區別聯繫?System.arraycopy()和Arrays.copyOf()代碼說明?

  • System.arraycopy()和Arrays.copyOf()方法區別?
    • 好比下面add(int index, E element)方法就很巧妙的用到了arraycopy()方法讓數組本身複製本身實現讓index開始以後的全部成員後移一個位置:
      /**
       * 在此列表中的指定位置插入指定的元素。 
       * 先調用 rangeCheckForAdd 對index進行界限檢查;而後調用 ensureCapacityInternal 方法保證capacity足夠大;
       * 再將從index開始以後的全部成員後移一個位置;將element插入index位置;最後size加1。
       */
      public void add(int index, E element) {
          rangeCheckForAdd(index);
          ensureCapacityInternal(size + 1); 
          //arraycopy()方法實現數組本身複製本身
          //elementData:源數組;index:源數組中的起始位置;elementData:目標數組;index + 1:目標數組中的起始位置; size - index:要複製的數組元素的數量;
          System.arraycopy(elementData, index, elementData, index + 1, size - index);
          elementData[index] = element;
          size++;
      }
      複製代碼
    • 如toArray()方法中用到了copyOf()方法
      /**
       *以正確的順序(從第一個到最後一個元素)返回一個包含此列表中全部元素的數組。 
       *返回的數組將是「安全的」,由於該列表不保留對它的引用。 (換句話說,這個方法必須分配一個新的數組)。
       *所以,調用者能夠自由地修改返回的數組。 此方法充當基於陣列和基於集合的API之間的橋樑。
       */
      public Object[] toArray() {
      //elementData:要複製的數組;size:要複製的長度
          return Arrays.copyOf(elementData, size);
      }
      複製代碼
    • 二者聯繫與區別
      • 看了上面的二者源代碼能夠發現copyOf()內部調用了System.arraycopy()方法
      • 技術博客大總結
      • 區別:
        • 1.arraycopy()須要目標數組,將原數組拷貝到你本身定義的數組裏,並且能夠選擇拷貝的起點和長度以及放入新數組中的位置
        • 2.copyOf()是系統自動在內部新建一個數組,並返回該數組。
  • System.arraycopy()和Arrays.copyOf()代碼說明?
    • 使用System.arraycopy()方法
      public static void main(String[] args) {
      	// TODO Auto-generated method stub
      	int[] a = new int[10];
      	a[0] = 0;
      	a[1] = 1;
      	a[2] = 2;
      	a[3] = 3;
      	System.arraycopy(a, 2, a, 3, 3);
      	a[2]=99;
      	for (int i = 0; i < a.length; i++) {
      		System.out.println(a[i]);
      	}
      }
      
      //結果:
      //0 1 99 2 3 0 0 0 0 0 
      複製代碼
    • 使用Arrays.copyOf()方法。技術博客大總結
      public static void main(String[] args) {
      	int[] a = new int[3];
      	a[0] = 0;
      	a[1] = 1;
      	a[2] = 2;
      	int[] b = Arrays.copyOf(a, 10);
      	System.out.println("b.length"+b.length);
      	//結果:
          //10
      }
      複製代碼
    • 得出結論
      • arraycopy() 須要目標數組,將原數組拷貝到你本身定義的數組裏或者原數組,並且能夠選擇拷貝的起點和長度以及放入新數組中的位置 copyOf() 是系統自動在內部新建一個數組,並返回該數組。

3.0.0.3 Collection集合和Map集合的區別?Map集合的特色?說下Map集合總體結構?

  • Collection集合和Map集合的區別
    • Map集合由兩列組成(雙列集合) , 而Collection集合由一列組成(單列集合) ; Map集合是夫妻對 , Collection孤狼
    • Collection集合中的Set集合能夠保證元素的惟一性 , 而Map集合中的鍵是惟一的
    • Collection集合的數據結構是對存儲的元素是有效的,而Map集合的數據結構只和鍵有關係,和值沒有關係。
  • Map集合的特色
    • 將鍵映射到值的對象
    • 一個映射不能包含重複的鍵
    • 每一個鍵最多隻能映射到一個值
  • 說下Map集合總體結構?
    • image
  • 總體結構介紹
    • HashMap 等其餘 Map 實現則是都擴展了AbstractMap,裏面包含了通用方法抽象。不一樣 Map的用途,從類圖結構就能體現出來,設計目的已經體如今不一樣接口上。
    • Hashtable 比較特別,做爲相似 Vector、Stack 的早期集合相關類型,它是擴展了 Dictionary 類的,類結構上與 HashMap 之類明顯不一樣。
    • 大部分使用 Map 的場景,一般就是放入、訪問或者刪除,而對順序沒有特別要求,HashMap 在這種狀況下基本是最好的選擇。HashMap的性能表現很是依賴於哈希碼的有效性,請務必掌握hashCode 和 equals 的一些基本約定,好比:博客
      • equals 相等,hashCode 必定要相等。
      • 重寫了 hashCode 也要重寫 equals。
      • hashCode 須要保持一致性,狀態改變返回的哈希值仍然要一致。
      • equals 的對稱、反射、傳遞等特性。

3.0.0.4 Java集合框架中有哪些類?都有什麼特色?集合框架用到Collection接口,這個接口有何特色?

  • 可將Java集合框架大體可分爲Set、List、Queue 和Map四種體系php

    • Set:表明無序、不可重複的集合,常見的類如HashSet、TreeSet
    • List:表明有序、可重複的集合,常見的類如動態數組ArrayList、雙向鏈表LinkedList、可變數組Vector
    • Map:表明具備映射關係的集合,常見的類如HashMap、LinkedHashMap、TreeMap
    • Queue:表明一種隊列集合
  • 1. Listjava

    • ArrayList:基於動態數組實現,支持隨機訪問。
    • Vector:和 ArrayList 相似,但它是線程安全的。
    • LinkedList:基於雙向鏈表實現,只能順序訪問,可是能夠快速地在鏈表中間插入和刪除元素。不只如此,LinkedList 還能夠用做棧、隊列和雙向隊列。
  • 2. Setgit

    • TreeSet:基於紅黑樹實現,支持有序性操做,例如根據一個範圍查找元素的操做。可是查找效率不如 HashSet,HashSet 查找的時間複雜度爲 O(1),TreeSet 則爲 O(logN)。
    • HashSet:基於哈希表實現,支持快速查找,但不支持有序性操做。而且失去了元素的插入順序信息,也就是說使用 Iterator 遍歷 HashSet 獲得的結果是不肯定的。
    • LinkedHashSet:具備 HashSet 的查找效率,且內部使用雙向鏈表維護元素的插入順序。
  • 3.Mapgithub

    • TreeMap:基於紅黑樹實現。
    • HashMap:基於哈希表實現。
    • HashTable:和 HashMap 相似,但它是線程安全的,這意味着同一時刻多個線程能夠同時寫入 HashTable 而且不會致使數據不一致。它是遺留類,不該該去使用它。如今可使用 ConcurrentHashMap 來支持線程安全,而且 ConcurrentHashMap 的效率會更高,由於 ConcurrentHashMap 引入了分段鎖。博客
    • LinkedHashMap:使用雙向鏈表來維護元素的順序,順序爲插入順序或者最近最少使用(LRU)順序。
  • 4. Queue面試

    • LinkedList:能夠用它來實現雙向隊列。
    • PriorityQueue:基於堆結構實現,能夠用它來實現優先隊列。
  • Java帶有一組接口和類,使得操做成組的對象更爲容易,這就是集合框架算法

    • 集合框架主要用到的是Collection接口,Collection是將其餘對象組織到一塊兒的一個對象,提供了一種方法來存儲、訪問和操做其元素
    • List、Set和Queue是Collection的三個主要的子接口。此外,還有一個Map接口用於存儲鍵值對
    接口說明 描述
    Collection Collection是最基本的集合接口,一個 Collection 表明一組Object,Java不提供直接繼承自Collection的類,只提供繼承於它的子接口
    List List接口是一個有序的Collection,使用此接口可以精確的控制每一個元素插入的位置,可以經過索引來訪問List中的元素,並且容許有相同的元素
    Set Set具備與Collection徹底同樣的接口,只是行爲上不一樣,Set不保存重複的元素
    Queue Queue經過先進先出的方式來存儲元素,即當獲取元素時,最早得到的元素是最早添加的元素,依次遞推
    SortedSet 繼承於Set保存有序的集合
    Map 將惟一的鍵映射到值
    Map.Entry 描述在一個Map中的一個元素(鍵/值對),是一個Map的內部類
    SortedMap 繼承於Map,使Key保持在升序排列

3.0.0.5 ArrayList添加元素時如何擴容?如何添加元素到指定位置,該操做複製是深拷貝仍是淺拷貝?

  • ArrayList添加元素時如何擴容
    • 經過add方法添加元素,其操做是將指定的元素追加到此列表的末尾。博客
    • 它的實現其實最核心的內容就是ensureCapacityInternal。這個函數其實就是自動擴容機制的核心。依次來看一下他的具體實現。
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 擴展爲原來的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 若是擴爲1.5倍還不知足需求,直接擴爲需求值
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    複製代碼
    • 當增長數據的時候,若是ArrayList的大小已經不知足需求時,那麼就將數組變爲原長度的1.5倍,以後的操做就是把老的數組拷到新的數組裏面。例如,默認的數組大小是10,也就是說當咱們add10個元素以後,再進行一次add時,就會發生自動擴容,數組長度由10變爲了15具體狀況以下所示。
  • 如何添加元素到指定位置,該操做拷貝是深拷貝仍是淺拷貝?
    • 在指定索引處添加一個元素,先對index進行界限檢查,;而後調用 ensureCapacityInternal 方法保證capacity足夠大,再將從index開始以後的全部成員後移一個位置;將element插入index位置;最後size加1。博客
    • 能夠看出它比add(index)方法還要多一個System.arraycopy。arraycopy()這個實現數組之間複製的方法必定要看一下,下面就用到了arraycopy()方法實現數組本身複製本身。該System.arraycopy()拷貝確實是淺拷貝,不會進行遞歸拷貝,因此產生的結果是基本數據類型是值拷貝,對象只是引用拷貝。
    • arraycopy()須要目標數組,將原數組拷貝到你本身定義的數組裏,並且能夠選擇拷貝的起點和長度以及放入新數組中的位置
    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    複製代碼
  • 測試一下System.arraycopy()是深拷貝仍是淺拷貝?
    public static void main(String[] args) {
        ArrayList<String> listStr = new ArrayList<>();
        for(int i = 0 ; i < 3 ;i++){
            listStr.add(i+"");
        }
        //clone一次
        ArrayList<String> listStrCopy = (ArrayList<String>) listStr.clone();
        //修改clone後對象的值
        listStrCopy.remove(2);
        listStrCopy.add(100+"");
        for (int i = 0; i < listStr.size(); i++) {
            System.out.println(listStr.get(i).toString());
            System.out.println(listStrCopy.get(i).toString());
        }
    }
    實驗結果,能夠看出修改對原始數據沒有改變,是複製了值
    0
    0
    1
    1
    2
    100
    複製代碼

3.0.0.6 如何理解ArrayList的擴容消耗?Arrays.asList方法後的List能夠擴容嗎?ArrayList如何序列化?

  • 如何理解ArrayList的擴容消耗
    • 因爲ArrayList使用elementData = Arrays.copyOf(elementData, newCapacity);進行擴容,而每次都會從新建立一個newLength長度的數組,因此擴容的空間複雜度爲O(n),時間複雜度爲O(n)
    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
    複製代碼
  • Arrays.asList方法後的List能夠擴容嗎?
    • 不能,asList返回的List爲只讀的。其緣由爲:asList方法返回的ArrayList是Arrays的一個內部類,而且沒有實現add,remove等操做
  • List怎麼實現排序?
    • 實現排序,可使用自定義排序:list.sort(new Comparator(){...})
    • 或者使用Collections進行快速排序:Collections.sort(list)
  • ArrayList如何序列化?
    • ArrayList 基於數組實現,而且具備動態擴容特性,所以保存元素的數組不必定都會被使用,那麼就不必所有進行序列化。
    • 技術博客大總結
    • 保存元素的數組 elementData 使用 transient 修飾,該關鍵字聲明數組默認不會被序列化。
    transient Object[] elementData; // non-private to simplify nested class access
    複製代碼
    • ArrayList 實現了 writeObject() 和 readObject() 來控制只序列化數組中有元素填充那部份內容。
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;
        s.defaultReadObject();
        s.readInt(); // ignored
        if (size > 0) {
            ensureCapacityInternal(size);
            Object[] a = elementData;
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }
    
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        int expectedModCount = modCount;
        s.defaultWriteObject();
        s.writeInt(size);
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    複製代碼
    • 序列化時須要使用 ObjectOutputStream 的 writeObject() 將對象轉換爲字節流並輸出。而 writeObject() 方法在傳入的對象存在 writeObject() 的時候會去反射調用該對象的 writeObject() 來實現序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理相似。
    ArrayList list = new ArrayList();
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(list);
    複製代碼

3.0.0.7 如何理解list集合讀寫機制和讀寫效率?什麼是CopyOnWriteArrayList,它與ArrayList有何不一樣?

  • 讀寫機制
    • ArrayList在執行插入元素是超過當前數組預約義的最大值時,數組須要擴容,擴容過程須要調用底層System.arraycopy()方法進行大量的數組複製操做;在刪除元素時並不會減小數組的容量(若是須要縮小數組容量,能夠調用trimToSize()方法);在查找元素時要遍歷數組,對於非null的元素採起equals的方式尋找。
    • LinkedList在插入元素時,須建立一個新的Entry對象,並更新相應元素的先後元素的引用;在查找元素時,需遍歷鏈表;在刪除元素時,要遍歷鏈表,找到要刪除的元素,而後從鏈表上將此元素刪除便可。
    • Vector與ArrayList僅在插入元素時容量擴充機制不一致。對於Vector,默認建立一個大小爲10的Object數組,並將capacityIncrement設置爲0;當插入元素數組大小不夠時,若是capacityIncrement大於0,則將Object數組的大小擴大爲現有size+capacityIncrement;若是capacityIncrement<=0,則將Object數組的大小擴大爲現有大小的2倍。博客
  • 讀寫效率
    • ArrayList對元素的增長和刪除都會引發數組的內存分配空間動態發生變化。所以,對其進行插入和刪除速度較慢,但檢索速度很快。
    • LinkedList因爲基於鏈表方式存放數據,增長和刪除元素的速度較快,可是檢索速度較慢。
  • 什麼是CopyOnWriteArrayList,它與ArrayList有何不一樣?
    • CopyOnWriteArrayList是ArrayList的一個線程安全的變體,其中全部可變操做(add、set等等)都是經過對底層數組進行一次新的複製來實現的。相比較於ArrayList它的寫操做要慢一些,由於它須要實例的快照。
    • CopyOnWriteArrayList中寫操做須要大面積複製數組,因此性能確定不好,可是讀操做由於操做的對象和寫操做不是同一個對象,讀之間也不須要加鎖,讀和寫之間的同步處理只是在寫完後經過一個簡單的"="將引用指向新的數組對象上來,這個幾乎不須要時間,這樣讀操做就很快很安全,適合在多線程裏使用,絕對不會發生ConcurrentModificationException ,所以CopyOnWriteArrayList適合使用在讀操做遠遠大於寫操做的場景裏,好比緩存。
    • 技術博客大總結

3.0.0.8 如何理解Java集合的快速失敗機制 「fail-fast」?出現這個緣由是什麼?有何解決辦法?

  • Java集合的快速失敗機制 「fail-fast」
    • java集合的一種錯誤檢測機制,當多個線程對集合進行結構上的改變的操做時,有可能會產生 fail-fast 機制。
      • 例如:假設存在兩個線程(線程一、線程2),線程1經過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程序就會拋出 ConcurrentModificationException 異常,從而產生fail-fast機制。
  • 出現這個緣由是什麼?
    • 迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount 變量。集合在被遍歷期間若是內容發生變化,就會改變modCount的值。每當迭代器使用hashNext()/next()遍歷下一個元素以前,都會檢測modCount變量是否爲expectedmodCount值,是的話就返回遍歷;不然拋出異常,終止遍歷。
  • 有何解決辦法:博客
    • 1.在遍歷過程當中,全部涉及到改變modCount值得地方所有加上synchronized。
    • 2.使用CopyOnWriteArrayList來替換ArrayList
  • 看源碼以下所示
    • modCount 用來記錄 ArrayList 結構發生變化的次數。結構發生變化是指添加或者刪除至少一個元素的全部操做,或者是調整內部數組的大小,僅僅只是設置元素的值不算結構發生變化。
    • 在進行序列化或者迭代等操做時,須要比較操做先後 modCount 是否改變,若是改變了須要拋出 ConcurrentModificationException。
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();
    
        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);
    
        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
    
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    複製代碼

3.0.0.9 LinkedList集合有何特色?LinkedList相比ArrayList效率如何?用什麼進行論證?

  • LinkedList集合有何特色
    • LinkedList 同時實現了 List 接口和 Deque 接口,因此既能夠將 LinkedList 當作一個有序容器,也能夠將之看做一個隊列(Queue),同時又能夠看做一個棧(Stack)。雖然 LinkedList 和 ArrayList 同樣都實現了 List 接口,但其底層是經過雙向鏈表來實現的,因此 LinkedList 插入和刪除元素的效率都要比 ArrayList 高,所以隨機訪問的效率也要比 ArrayList 低。
    • 簡單總結一下
      • 實現了List接口和Deque接口,是雙端鏈表
      • 支持高效的插入和刪除操做
      • 不是線程安全的
    • 若是想使LinkedList變成線程安全的
      • 能夠調用靜態類
      List list =Collections.synchronizedList(new LinkedList(...));
      複製代碼
  • LinkedList相比ArrayList效率如何
    • LinkedList 相比 ArrayList 添加和移除元素的效率會高些,但隨機訪問元素的效率要比 ArrayList 低,這裏我也來作個測試,看下二者之間的差距
    • 分別向 ArrayList 和 LinkedList 存入同等數據量的數據,而後各自移除 100 個元素以及遍歷 10000 個元素,觀察二者所用的時間。博客
    public static void main(String[] args) {
    
            List<String> stringArrayList = new ArrayList<>();
            for (int i = 0; i < 300000; i++) {
                stringArrayList.add("leavesC " + i);
            }
            //開始時間
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) {
                stringArrayList.remove(100 + i);
            }
            //結束時間
            long endTime = System.currentTimeMillis();
            System.out.println("移除 ArrayList 中的100個元素,所用時間:" + (endTime - startTime) + "毫秒");
    
            //開始時間
            startTime = System.currentTimeMillis();
            for (int i = 0; i < 10000; i++) {
                stringArrayList.get(i);
            }
            //結束時間
            endTime = System.currentTimeMillis();
            System.out.println("遍歷 ArrayList 中的10000個元素,所用時間:" + (endTime - startTime) + "毫秒");
    
    
            List<String> stringLinkedList = new LinkedList<>();
            for (int i = 0; i < 300000; i++) {
                stringLinkedList.add("leavesC " + i);
            }
            //開始時間
            startTime = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) {
                stringLinkedList.remove(100 + i);
            }
            //結束時間
            endTime = System.currentTimeMillis();
            System.out.println("移除 LinkedList 中的100個元素,所用時間:" + (endTime - startTime) + "毫秒");
            //開始時間
            startTime = System.currentTimeMillis();
            for (int i = 0; i < 10000; i++) {
                stringLinkedList.get(i);
            }
            //結束時間
            endTime = System.currentTimeMillis();
            System.out.println("遍歷 LinkedList 中的10000個元素,所用時間:" + (endTime - startTime) + "毫秒");
        }
    複製代碼
    • 能夠看出來,二者之間的差距仍是很是大的,所以,在使用集合時須要根據實際狀況來判斷到底哪種數據結構才更加適合。

3.0.1.0 HashSet和TreeSet的區別?是如何保證惟一值的,底層怎麼作到的?

  • HashSet
    • 不能保證元素的排列順序;使用Hash算法來存儲集合中的元素,有良好的存取和查找性能;經過equal()判斷兩個元素是否相等,並兩個元素的hashCode()返回值也相等
  • TreeSet
    • 是SortedSet接口的實現類,根據元素實際值的大小進行排序;採用紅黑樹的數據結構來存儲集合元素;支持兩種排序方法:天然排序(默認狀況)和定製排序。前者經過實現Comparable接口中的compareTo()比較兩個元素之間大小關係,而後按升序排列;後者經過實現Comparator接口中的compare()比較兩個元素之間大小關係,實現定製排列

3.0.1.3 HashMap有哪些特色,簡單說一下?HashMap內部的結構是怎樣的?簡單說一下什麼是桶,做用是什麼?

  • HashMap有哪些特色,簡單說一下?
    • 幾個關鍵的信息:
      • 基於Map接口實現、容許null鍵/值、是非同步(這點很重要,多線程注意)、不保證有序(好比插入的順序)、也不保證序不隨時間變化。
    • 如何理解容許null鍵/值?
      • 容許插入最多一條keynull的記錄,容許插入多條valuenull的記錄。
    • 如何理解不保證有序?
      • HashMap 不保證元素順序,根據須要該容器可能會對元素從新哈希,元素的順序也會被從新打散,所以在不一樣時間段迭代同一個 HashMap 的順序可能會不一樣。
    • 如何理解非同步?
      • HashMap 非線程安全,即任一時刻有多個線程同時寫 HashMap 的話可能會致使數據的不一致
  • HashMap內部的結構是怎樣的
    • HashMap 內部的結構,它能夠看做是數組(Node[] table)和鏈表結合組成的複合結構,數組被分爲一個個桶(bucket),經過哈希值決定了鍵值對在這個數組的尋址;哈希值相同的鍵值對,則以鏈表形式存儲,你能夠參考下面的示意圖。
    • 這裏須要注意的是,若是鏈表大小超過閾值(TREEIFY_THRESHOLD, 8,圖中的鏈表就會被改造爲樹形結構。
  • 結構圖以下所示
    • image

3.0.1.4 當有鍵值對插入時,HashMap會發生什麼 ? 對於查找一個key時,HashMap會發生什麼 ?

  • 當有鍵值對插入時,HashMap會發生什麼 ?
    • 首先,鍵的哈希值被計算出來,而後這個值會賦給 Entry 類中對應的 hashCode 變量。
    • 而後,使用這個哈希值找到它將要被存入的數組中「桶」的索引。
    • 若是該位置的「桶」中已經有一個元素,那麼新的元素會被插入到「桶」的頭部,next 指向上一個元素——本質上使「桶」造成鏈表。
  • 對於查找一個key時,HashMap會發生什麼 ?
    • 鍵的哈希值先被計算出來
    • 在 mHashes[] 數組中二分查找此哈希值。這代表查找的時間複雜度增長到了 O(logN)。
    • 一旦獲得了哈希值所對應的索引 index,鍵值對中的鍵就存儲在 mArray[2index] ,值存儲在 mArray[2index+1]。博客
    • 這裏的時間複雜度從 O(1) 上升到 O(logN),可是內存效率提高了。當咱們在 100 左右的數據量範圍內嘗試時,沒有耗時的問題,察覺不到時間上的差別,但咱們應用的內存效率得到了提升。

3.0.1.5 HashMap和Hashtable的區別?HashMap在put、get元素的過程?體現了什麼數據結構?

  • HashMap
    • 基於AbstractMap類,實現了Map、Cloneable(能被克隆)、Serializable(支持序列化)接口; 非線程安全;容許存在一個爲null的key和任意個爲null的value;採用鏈表散列的數據結構,即數組和鏈表的結合;初始容量爲16,填充因子默認爲0.75,擴容時是當前容量翻倍,即2capacity
  • Hashtable
    • 基於Map接口和Dictionary類;線程安全,開銷比HashMap大,若是多線程訪問一個Map對象,使用Hashtable更好;不容許使用null做爲key和value;底層基於哈希表結構;初始容量爲11,填充因子默認爲0.75,擴容時是容量翻倍+1,即2capacity+1
    • HashTable裏使用的是synchronized關鍵字,這實際上是對對象加鎖,鎖住的都是對象總體,當Hashtable的大小增長到必定的時候,性能會急劇降低,由於迭代時須要被鎖定很長的時間。
  • HashMap在put、get元素的過程
    • 向Hashmap中put元素時,首先判斷key是否爲空,爲空則直接調用putForNullKey(),不爲空則計算key的hash值獲得該元素在數組中的下標值;若是數組在該位置處沒有元素,就直接保存;若是有,還要比較是否存在相同的key,存在的話就覆蓋原來key的value,不然將該元素保存在鏈頭,先保存的在鏈尾。
    • 從Hashmap中get元素時,計算key的hash值找到在數組中的對應的下標值,返回該key對應的value便可,若是有衝突就遍歷該位置鏈表尋找key相同的元素並返回對應的value
  • 體現了什麼數據結構
    • HashMap採用鏈表散列的數據結構,即數組和鏈表的結合,在Java8後又結合了紅黑樹,當鏈表元素超過8個將鏈表轉換爲紅黑樹
    • 技術博客大總結

3.0.1.6 如何保證HashMap線程安全?底層怎麼實現的?HashMap是有序的嗎?如何實現有序?

  • 使用ConcurrentHashMap可保證線程安全
    • ConcurrentHashMap是線程安全的HashMap,它採起鎖分段技術,將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問。在JDK1.8中對ConcurrentHashmap作了兩個改進:
      • 取消segments字段,直接採用transient volatile HashEntry<K,V>[] table保存數據,將數組元素做爲鎖,對每一行數據進行加鎖,可減小併發衝突的機率
      • 數據結構由「數組+單向鏈表」變爲「數組+單向鏈表+紅黑樹」,使得查詢的時間複雜度能夠下降到O(logN),改進必定的性能。
    • 通俗一點解釋:ConcurrentHashMap引入了分割(Segment),能夠理解爲把一個大的Map拆分紅N個小的HashTable,在put方法中,會根據hash(paramK.hashCode())來決定具體存放進哪一個Segment,若是查看Segment的put操做,咱們會發現內部使用的同步機制是基於lock操做的,這樣就能夠對Map的一部分(Segment)進行上鎖,這樣影響的只是將要放入同一個Segment的元素的put操做,保證同步的時候,鎖住的不是整個Map(HashTable就是這麼作的),相對於HashTable提升了多線程環境下的性能,所以HashTable已經被淘汰了。技術博客大總結
  • 使用LinkedHashMap可實現有序
    • HashMap是無序的,而LinkedHashMap是有序的HashMap,默認爲插入順序,還能夠是訪問順序,基本原理是其內部經過Entry維護了一個雙向鏈表,負責維護Map的迭代順序

3.0.1.7 HashMap存儲兩個對象的hashcode相同會發生什麼?若是兩個鍵的hashcode相同,你如何獲取值對象?

  • HashMap存儲兩個對象的hashcode相同會發生什麼?
    • 錯誤回答:由於hashcode相同,因此兩個對象是相等的,HashMap將會拋出異常,或者不會存儲它們。
    • 正確回答:兩個對象就算hashcode相同,可是它們可能並不相等。若是不明白,能夠先看看個人這篇博客:Hash和HashCode深刻理解。回答「由於hashcode相同,因此它們的bucket位置相同,‘碰撞’會發生。由於HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。
  • HashMap1.7和1.8的區別
    • 在JDK1.6,JDK1.7中,HashMap採用數組+鏈表實現,即便用鏈表處理衝突,同一hash值的鏈表都存儲在一個鏈表裏。可是當位於一個鏈表中的元素較多,即hash值相等的元素較多時,經過key值依次查找的效率較低。
    • JDK1.8中,HashMap採用位數組+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換爲紅黑樹,這樣大大減小了查找時間。
  • 若是兩個鍵的hashcode相同,你如何獲取值對象?
    • 當調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,而後獲取值對象。固然若是有兩個值對象儲存在同一個bucket,將會遍歷鏈表直到找到值對象。
    • 在沒有值對象去比較,如何肯定肯定找到值對象的?由於HashMap在鏈表中存儲的是鍵值對,找到bucket位置以後,會調用keys.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象。
    • 技術博客大總結

3.0.1.8 HashMap爲何不直接使用hashCode()處理後的哈希值直接做爲table的下標?

  • 不直接使用hashCode()處理後的哈希值
    • hashCode()方法返回的是int整數類型,其範圍爲-(2^31)~(2^31-1),約有40億個映射空間,而HashMap的容量範圍是在16(初始化默認值)~2 ^ 30,HashMap一般狀況下是取不到最大值的,而且設備上也難以提供這麼多的存儲空間,從而致使經過hashCode()計算出的哈希值可能不在數組大小範圍內,進而沒法匹配存儲位置;
  • HashMap是使用了哪些方法來有效解決哈希衝突的
    • 1.使用鏈地址法(使用散列表)來連接擁有相同hash值的數據;
    • 2.使用2次擾動函數(hash函數)來下降哈希衝突的機率,使得數據分佈更平均;
    • 3.引入紅黑樹進一步下降遍歷的時間複雜度,使得遍歷更快;
  • 如何解決匹配存儲位置問題
    • HashMap本身實現了本身的hash()方法,經過兩次擾動使得它本身的哈希值高低位自行進行異或運算,下降哈希碰撞機率也使得數據分佈更平均;
    • 在保證數組長度爲2的冪次方的時候,使用hash()運算以後的值與運算(&)(數組長度 - 1)來獲取數組下標的方式進行存儲,這樣一來是比取餘操做更加有效率,二來也是由於只有當數組長度爲2的冪次方時,h&(length-1)纔等價於h%length,三來解決了「哈希值與數組大小範圍不匹配」的問題;
  • 爲何數組長度要保證爲2的冪次方呢?
    • 只有當數組長度爲2的冪次方時,h&(length-1)纔等價於h%length,即實現了key的定位,2的冪次方也能夠減小衝突次數,提升HashMap的查詢效率;
    • 技術博客大總結
    • 若是 length 爲 2 的次冪 則 length-1 轉化爲二進制一定是 11111……的形式,在於 h 的二進制與操做效率會很是的快,並且空間不浪費;若是 length 不是 2 的次冪,好比 length 爲 15,則 length - 1 爲 14,對應的二進制爲 1110,在於 h 與操做,最後一位都爲 0 ,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費至關大,更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!這樣就會形成空間的浪費。

3.0.1.9 爲何HashMap中String、Integer這樣的包裝類適合做爲K?若是要用對象最爲key,該如何操做?

  • 爲何HashMap中String、Integer這樣的包裝類適合做爲K?
    • String、Integer等包裝類的特性可以保證Hash值的不可更改性和計算準確性,可以有效的減小Hash碰撞的概率
      • 都是final類型,即不可變性,保證key的不可更改性,不會存在獲取hash值不一樣的狀況
      • 內部已重寫了equals()、hashCode()等方法,遵照了HashMap內部的規範(不清楚能夠去上面看看putValue的過程),不容易出現Hash值計算錯誤的狀況;
  • 想要讓本身的Object做爲K應該怎麼辦呢?
    • 重寫hashCode()和equals()方法
      • 重寫hashCode()是由於須要計算存儲數據的存儲位置,須要注意不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提升性能,這樣雖然能更快但可能會致使更多的Hash碰撞;
      • 重寫equals()方法,須要遵照自反性、對稱性、傳遞性、一致性以及對於任何非null的引用值x,x.equals(null)必須返回false的這幾個特性,目的是爲了保證key在哈希表中的惟一性;
  • 總結
    • 採用合適的equals()和hashCode()方法的話,將會減小碰撞的發生,提升效率。不可變性使得可以緩存不一樣鍵的hashcode,這將提升整個獲取對象的速度,使用String,Interger這樣的wrapper類做爲鍵是很是好的選擇。
    • 技術博客大總結
  • 若是要用對象最爲key,該如何操做?
    • 須要重寫hashCode()和equals()方法,實例代碼以下所示:
    public class Key {
    
        private final String name;
        private final int width;
        private final int heifht;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Key key = (Key) o;
            if (width != key.width) {
                return false;
            }
            if (heifht != key.heifht) {
                return false;
            }
            return name != null ? name.equals(key.name) : key.name == null;
        }
    
        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            result = 31 * result + width;
            result = 31 * result + heifht;
            return result;
        }
    }
    複製代碼

3.0.2.0 HashMap是如何擴容的?如何理解HashMap的大小超過了負載因子定義的容量?從新調整HashMap大小存在什麼問題嗎?

  • HashMap是爲啥要擴容
    • 當鏈表數組的容量超過初始容量*加載因子(默認0.75)時,再散列將鏈表數組擴大2倍,把原鏈表數組的搬移到新的數組中。爲何須要使用加載因子?爲何須要擴容呢?由於若是填充比很大,說明利用的空間不少,若是一直不進行擴容的話,鏈表就會愈來愈長,這樣查找的效率很低,擴容以後,將原來鏈表數組的每個鏈表分紅奇偶兩個子鏈表分別掛在新鏈表數組的散列位置,這樣就減小了每一個鏈表的長度,增長查找效率。
  • 如何理解HashMap的大小超過了負載因子(load factor)定義的容量?
    • 默認的負載因子大小爲0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)同樣,將會建立原來HashMap大小的兩倍的bucket數組,來從新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫做rehashing,由於它調用hash方法找到新的bucket位置。
  • 從新調整HashMap大小存在什麼問題嗎?技術博客大總結
    • 當多線程的狀況下,可能產生條件競爭。當從新調整HashMap大小的時候,確實存在條件競爭,由於若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。

3.0.2.1 HashMap是線程安全的嗎?多線程條件下put存儲數據會發生什麼狀況?如何理解它併發性?

  • HashMap是非線程安全的,那麼測試一下,先看下測試代碼
    private HashMap map = new HashMap();
    private void test(){
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    map.put(new Integer(i), i);
                }
                LogUtils.d("yc-----執行結束----t1");
            }
        };
        //省略一部分線程代碼,和t1同樣
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }
    複製代碼
  • 就是啓了6個線程,不斷的往一個非線程安全的HashMap中put/get內容,put的內容很簡單,key和value都是從0自增的整數(這個put的內容作的並很差,以至於後來干擾了我分析問題的思路)。對HashMap作併發寫操做,我原覺得只不過會產生髒數據的狀況,但反覆運行這個程序,會出現線程t一、t2被卡住的狀況,多數狀況下是一個線程被卡住另外一個成功結束,偶爾會6個線程都被卡住。博客
  • 多線程條件下put存儲數據會發生什麼狀況
    • CPU利用率太高通常是由於出現了出現了死循環,致使部分線程一直運行,佔用cpu時間。問題緣由就是HashMap是非線程安全的,多個線程put的時候形成了某個key值Entry key List的死循環,問題就這麼產生了。
    • 當另一個線程get 這個Entry List 死循環的key的時候,這個get也會一直執行。最後結果是愈來愈多的線程死循環,最後致使卡住。咱們通常認爲HashMap重複插入某個值的時候,會覆蓋以前的值,這個沒錯。可是對於多線程訪問的時候,因爲其內部實現機制(在多線程環境且未做同步的狀況下,對同一個HashMap作put操做可能致使兩個或以上線程同時作rehash動做,就可能致使循環鍵表出現,一旦出現線程將沒法終止,持續佔用CPU,致使CPU使用率居高不下),就可能出現安全問題了。
  • 如何解決HashMap多線程併發問題?
    • 多線程下直接使用ConcurrentHashMap,解決了這個問題。

3.0.2.2 TreeMap集合結構有何特色?使用場景是什麼?將"aababcabcdabcde"打印成a(5)b(4)c(3)d(2)e(1)?

  • TreeMap集合結構特色
    • 鍵的數據結構是紅黑樹,可保證鍵的排序和惟一性
    • 排序分爲天然排序和比較器排序,若是使用的是天然排序,對元素有要求,要求這個元素須要實現 Comparable 接口
    • 線程是不安全的效率比較高
    public TreeMap(): 天然排序
    public TreeMap(Comparator<? super K> comparator):  使用的是比較器排序
    複製代碼
  • 使用場景是什麼?
    • 以前已經學習過HashMap和LinkedHashMap了,HashMap不保證數據有序,LinkedHashMap保證數據能夠保持插入順序,而若是咱們但願Map能夠保持key的大小順序的時候,咱們就須要利用TreeMap了。博客
  • 將"aababcabcdabcde"打印成a(5)b(4)c(3)d(2)e(1)?
    • "aababcabcdabcde",獲取字符串中每個字母出現的次數要求結果:a(5)b(4)c(3)d(2)e(1)
    • "aababcabcdabcde" 按照鍵值對的形式存儲到TreeMap集合中
    • 分析:博客
      • 1,遍歷字符串,獲取每個字符,而後將當前的字符做爲鍵 , 上map集合中查找對應的值
      • 2,若是返回的值不是null 對值進行+1 , 在把當前的元素做爲鍵 , 值是+1之後的結果存儲到集合中
      • 3,若是返回的是是null , 不存在 , 就把當前遍歷的元素做爲鍵 , 1 做爲值,添加到集合中
    • 代碼以下
      public static void main(String[] args) {
          // 定義字符串
          String s = "aababcabcdabcde" ;
          // 建立TreeMap集合對象
          TreeMap<Character , Integer> tm = new TreeMap<Character , Integer>() ;
          // 遍歷字符串
          for(int x = 0 ; x < s.length() ; x++) {
              // 獲取當前索引出對應的字符
              char ch = s.charAt(x) ;
              // 找值
              Integer value = tm.get(ch) ;
              // 判斷
              if(value == null) {
                  tm.put(ch, 1) ;
              }else {
                  value += 1 ;
                  tm.put(ch, value) ;
              }
          }       
          // 遍歷Map集合按照指定的形式拼接字符串
          StringBuilder sb = new StringBuilder() ;
          Set<Entry<Character,Integer>> entrySet = tm.entrySet() ;
          for(Entry<Character,Integer> en : entrySet) {
              // 獲取鍵
              Character key = en.getKey() ;
              // 獲取值
              Integer value = en.getValue() ;
              // a(5)b(4)c(3)d(2)e(1)
              // 拼接
              sb.append(key).append("(").append(value).append(")") ;
          }
          // 把sb轉換成String
          String result = sb.toString() ;
          // 輸出
          System.out.println(result);
      }
      複製代碼

3.0.2.3 說一下HashSet集合特色?如何存儲null值的?HashSet是如何去重操做?手寫產生10個1-20之間的隨機數要求隨機數不能重複案例?

  • 說一下HashSet集合特色?
    • HashSet 實現了 Set 接口,不容許插入重複的元素,容許包含 null 元素,且不保證元素迭代順序,特別是不保證該順序恆久不變
    • HashSet 的代碼十分簡單,去掉註釋後的代碼不到兩百行。HashSet 底層是經過 HashMap 來實現的。
  • 案例測試
    • HashSet是根據hashCode來決定存儲位置的,是經過HashMap實現的,因此對象必須實現hashCode()方法,存儲的數據無序不能重複,能夠存儲null,可是隻能存一個。
  • 如何存儲null值的?
  • HashSet是如何去重操做?
    • 在向 HashSet 添加元素時,HashSet 會將該操做轉換爲向 HashMap 添加鍵值對,若是 HashMap 中包含 key 值與待插入元素相等的鍵值對(hashCode() 方法返回值相等,經過 equals() 方法比較也返回 true),則待添加的鍵值對的 value 會覆蓋原有數據,但 key 不會有所改變,所以若是向 HashSet 添加一個已存在的元素時,元素不會被存入 HashMap 中,從而實現了 HashSet 元素不重複的特徵。博客
  • 產生10個1-20之間的隨機數要求隨機數不能重複案例
    /**
     * 產生10個1-20之間的隨機數,要求不能重複
     * 分析:
     *         1: 建立一個HashSet集合對象 , 做用: 存儲產生的隨機數
     *         2: 生成隨機數 , 把隨機數添加到集合中
     *         3: 使用循環,當集合的長度大於等於10退出循環 , 小於10就一直循環
     */
    // 建立一個HashSet集合對象 , 做用: 存儲產生的隨機數
    HashSet<Integer> hs = new HashSet<Integer>() ;
    while(hs.size() < 10) {
        // 使用Random類
        Random random = new Random() ;
        int num = random.nextInt(20) + 1 ;
        // 把num添加到集合中
        hs.add(num) ;
    }
    // 遍歷
    for(Integer i : hs) {
        System.out.println(i);
    }
    複製代碼

其餘介紹

01.關於博客彙總連接

02.關於個人博客

相關文章
相關標籤/搜索