Java容器面試總結


 

1、List,Set,Map三者的區別?

List:用於存儲一個有序元素的集合。

Set:用於存儲一組不重復的元素。

Map:使用鍵值對存儲。Map會維護與Key有關聯的值。兩個Key可以引用相同的對象,但Key不能重復,典型的Key是String類型,但也可以是任何對象。

補充:

  Stack用於存儲采用后進先出方式處理的對象。

  Queue用於存儲采用先進先出方式處理的對象。

  PriorityQueue用於存儲按照優先級順序處理的對象。

2、Arraylist 與 LinkedList 區別?

  • (相同點)是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;

  • 1. 底層數據結構: Arraylist 底層使用的是 Object 數組LinkedList 底層使用的是雙向鏈表(JDK1.6之前為循環鏈表,JDK1.7取消了循環。注意雙向鏈表和雙向循環鏈表的區別,下面有介紹到!)

  • 2. 插入和刪除是否受元素位置的影響: ① ArrayList 采用數組存儲,所以插入和刪除元素的時間復雜度受元素位置的影響。 比如:執行add(E e)方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種情況時間復雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element))時間復雜度就為 O(n-i)。因為在進行上述操作的時候集合中第 i 和第 i 個元素之后的(n-i)個元素都要執行向后位/向前移一位的操作。 ② LinkedList 采用鏈表存儲,所以插入、刪除元素時間復雜度不受元素位置的影響,都是近似 O(1)  而數組為近似 O(n)。

  • 3. 是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持快速隨機訪問就是通過元素的序號快速獲取元素對象(對應於get(int index) 方法)。

  • 4. 內存空間占用: ArrayList的空間浪費主要體現在在list列表的結尾會預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比ArrayList更多的空間(因為要存放直接后繼和直接前驅以及數據)。

雙向鏈表和雙向循環鏈表:

雙向鏈表: 包含兩個指針,一個prev指向前一個節點,一個next指向后一個節點。

雙向鏈表

雙向循環鏈表: 最后一個節點的 next 指向head,而 head 的prev指向最后一個節點,構成一個環。

雙向循環鏈表

3、ArrayList 與 Vector的區別?為什么要用Arraylist取代Vector呢?

Vector類的所有方法都是同步的。可以由兩個線程安全地訪問一個Vector對象、但是一個線程訪問Vector的話代碼要在同步操作上耗費大量的時間。

Arraylist不是同步的,所以在不需要保證線程安全時建議使用Arraylist。

4、說一說 ArrayList 的擴容機制

( ArrayList擴容發生在add(E e)方法調用的時候)

1.  以無參數構造方法創建 ArrayList 時,實際上初始化賦值的是一個空數組。當真正對數組進行添加元素操作時(add),才真正分配容量。即向數組中添加第一個元素時,數組容量擴為10。(默認初始容量DEFAULT_CAPACITY = 10

2. 當數組首次擴容的10個空間用完需要擴容后,會走grow方法來擴容(每次擴容為1.5倍)

private void grow(int minCapacity) {
          // 獲取到ArrayList中elementData數組的內存空間長度
          int oldCapacity = elementData.length;
         // 擴容至原來的1.5倍
         int newCapacity = oldCapacity + (oldCapacity >> 1);
         //然后檢查新容量是否大於最小需要容量,若還是小於最小需要容量,那么就把最小需要容量當作數組的新容量
         if (newCapacity - minCapacity < 0)
             newCapacity = minCapacity;
         //若預設值大於默認的最大值檢查是否溢出
         if (newCapacity - MAX_ARRAY_SIZE > 0)
             newCapacity = hugeCapacity(minCapacity);
         // 調用Arrays.copyOf方法將elementData數組指向新的內存空間時newCapacity的連續空間,並將elementData的數據復制到新的內存空間
         elementData = Arrays.copyOf(elementData, newCapacity);
     }

 從此方法中我們可以清晰的看出其實ArrayList擴容的本質就是計算出新的擴容數組的size后實例化,並將原有數組內容復制到新數組中去

3. 如果新容量大於 MAX_ARRAY_SIZE,執行)hugeCapacity()方法來比較 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大於最大容量,則新容量則為Integer.MAX_VALUE,否則,新容量大小則為 MAX_ARRAY_SIZE 即為 Integer.MAX_VALUE - 8。

https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md

5、HashMap的底層實現?怎么put、get?put中的resize?(重要!)

JDK1.8 之前 HashMap 底層是數組+鏈表結合在一起實現的(HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表), jdk8 對於鏈表長度超過 8 的鏈表將轉儲為紅黑樹

當新建一個HashMap的時候,就會初始化一個數組。Entry就是數組中的元素,每個 Map.Entry 其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了鏈表。

transient Entry[] table;
 
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;
    ……

  HashMap通過key的hashCode得到hash值,然后計算當前元素存放的位置((n - 1) & hash,n指的是數組的長度),如果當前位置存在元素的話,就利用equals方法判斷該元素與要存入的元素的key是否相等,如果相等就覆蓋其value值,不相等就通過拉鏈法解決沖突(就是將當前元素與該位置原有元素形成鏈表,並且當前元素位於鏈表的頭部)

對於存值和取值,使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。

 (1)put(key, value):

  1. 對key求hash值,然后找到該hash值對應數組中的存儲位置。
  2. 如果當前位置存在元素的話(不為空),就遍歷已存在的元素 判斷該元素與要存入的元素的key值是否相等,如果相等就覆蓋value值;如果不相等就將當前元素添加到到該位置的鏈表中(當前元素位於鏈表頭部)
  3. 如果鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,當前位置元素長度低於6,就把紅黑樹轉回鏈表
  4. 如果桶滿了(容量16*加載因子0.75),就需要 resize(擴容2倍后重排);
public V put(K key, V value) {
     // HashMap允許存放null鍵和null值。
     // 當key為null時,調用putForNullKey方法,將value放置在數組第一個位置。
     if (key == null)
         return putForNullKey(value);
     // 根據key的keyCode重新計算hash值。
     int hash = hash(key.hashCode());
     // 搜索指定hash值在對應table中的索引。
     int i = indexFor(hash, table.length);
     // 如果 i 索引處的 Entry 不為 null,通過循環不斷遍歷 e 元素的下一個元素。
     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         Object k;
         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             // 如果發現已有該鍵值,則存儲新的值,並返回原始值
             V oldValue = e.value;
             e.value = value;
             e.recordAccess(this);
             return oldValue;
        }
     }
     // 如果i索引處的Entry為null,表明此處還沒有Entry。
     modCount++;
     // 將key、value添加到i索引處。
     addEntry(hash, key, value, i);
     return null;
 }

為什么鏈表長度超過8,就把鏈表轉換為紅黑樹;桶中元素小於等於6,就將樹結構還原為鏈表?

  紅黑樹的平均查找長度是log(n),當長度為8時,查找長度為log(8)=3;鏈表的平均查找長度為n/2,當長度為8時,平均查找長度為8/2=4,這才有將鏈表轉換為紅黑樹的必要。

  還有選擇6和8的原因是:中間有個差值7可以防止鏈表和樹之間頻繁地轉換。假設鏈表個數超過8就將鏈表轉換為樹結構、小於8就將樹結構轉換成鏈表,那么如果一個HashMap不停地插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁地發生樹轉鏈表、鏈表轉樹,效率會很低。

(2)get(key)

  當調用get()方法,首先計算key的hashcode來找到桶位置(數組中對應位置),找到桶位置之后,調用key的equals()方法在對應位置的鏈表中找到需要的元素

      

(3)put中的resize(擴容)

  當HashMap中的元素個數超過 數組大小*負載因子(loadFactor)時,就會進行數組擴容(把數組大小擴大一倍),然后重新計算元素在數組中的位置。

  Entry<K,V>[] table的初始化長度length(數組大小默認值是16)loadFactor的默認值為0.75

  HashMap不是無限擴容的,當達到了實現預定的MAXIMUM_CAPACITY,就不再進行擴容。

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果當前的數組長度已經達到最大值,則不在進行調整
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //根據傳入參數的長度定義新的數組
    Entry[] newTable = new Entry[newCapacity];
    //按照新的規則,將舊數組中的元素轉移到新數組中
    transfer(newTable);
    table = newTable;
    //更新臨界值
    threshold = (int)(newCapacity * loadFactor);
}
//舊數組中元素往新數組中遷移
void transfer(Entry[] newTable) {
    //舊數組
    Entry[] src = table;
    //新數組長度
    int newCapacity = newTable.length;
    //遍歷舊數組
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);//放在新數組中的index位置
                e.next = newTable[i];//實現鏈表結構,新加入的放在鏈頭,之前的的數據放在鏈尾
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }

6、 HashMap、Hashtable、ConcurrentHashMap 的區別(重要!)

 (1)HashMap

實現了Map接口,實現了將唯一鍵隱射到特定值上。允許一個NULL鍵和多個NULL值。非線程安全

(2)HashTable

  類似於HashMap,但是不允許NULL鍵和NULL值,比HashMap慢,因為它是同步的。HashTable是一個線程安全的類,它使用synchronized來鎖住整張Hash表來實現線程安全,即每次鎖住整張表讓線程獨占。

(3)ConcurrentHashMap

  ConcurrentHashMap允許多個修改操作並發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的Hashtable,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以並發進行

Hashtable 和 HashMap不同點?

  • Hashtable和HashMap都實現了Map接口,但是Hashtable的實現是基於Dictionary抽象類。
  • 在HashMap中,null可以作為鍵,這樣的鍵只有一個,但可以有一個或多個鍵所對應的值為nullHashtable不允許NULL鍵和NULL值

當get()方法返回null值時,即可以表示HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。而在Hashtable中,無論是key還是value都不能為null。

  • 這兩個類最大的不同在於Hashtable是線程安全的,它的方法是同步了的,可以直接用在多線程環境中。而HashMap則不是線程安全的。在多線程環境中,需要手動實現同步機制。

Hashtable 和ConcurrentHashMap的不同?

(1)HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable的效率非常低下。(所有訪問HashTable的線程都必須競爭同一把鎖。)因為當一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法可能會進入阻塞或輪詢狀態。

(2)ConcurrentHashMap使用鎖分段技術,首先將數據分成一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。從而有效地提高了並發訪問效率。

    Hashtable中采用的鎖機制是一次鎖住整個hash表,從而同一時刻只能由一個線程對其進行操作;而ConcurrentHashMap中則是 一次鎖住一個桶。ConcurrentHashMap默認將hash表分為16個桶,諸如get,put,remove等常用操作只鎖當前需要用到的桶。 這樣,原來只能一個線程進入,現在卻能同時有16個寫線程執行,並發性能的提升是顯而易見的。(16個線程指的是寫線程,而讀操作大部分時候都不需要用到鎖。)

  • 底層數據結構: JDK1.7的 ConcurrentHashMap 底層采用 分段的數組+鏈表 實現,JDK1.8 采用的數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是采用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的;
  • 實現線程安全的方式(重要): ① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器里不同數據段的數據,就不會存在鎖競爭,提高並發訪問率。 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,並發控制使用 synchronized 和 CAS 來操作。(JDK1.6以后 對 synchronized鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是為了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低。

HashTable:

JDK1.7的ConcurrentHashMap:

JDK1.8的ConcurrentHashMap(TreeBin: 紅黑二叉樹節點 Node: 鏈表節點):

ConcurrentHashMap線程安全的具體實現方式/底層具體實現?

JDK1.7(上面有示意圖):

  首先將數據分為一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據時,其他段的數據也能被其他線程訪問。

  ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。

  一個 ConcurrentHashMap 里包含一個 Segment 數組。Segment 的結構和HashMap類似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素,每個 Segment 守護着一個HashEntry數組里的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment的鎖。

JDK1.8 (上面有示意圖):

  ConcurrentHashMap取消了Segment分段鎖,采用CAS和synchronized來保證並發安全。數據結構跟HashMap1.8的結構類似,數組+鏈表/紅黑二叉樹。Java 8在鏈表長度超過一定閾值(8)時將鏈表(尋址時間復雜度為O(N))轉換為紅黑樹(尋址時間復雜度為O(log(N)))

  synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不沖突,就不會產生並發,效率又提升N倍。

7、HashMap 和 HashSet區別

  HashSet 底層就是基於HashMap實現的。(HashSet 的源碼非常非常少,因為除了 clone()writeObject()readObject()是 HashSet 自己不得不實現之外,其他方法都是直接調用 HashMap 中的方法。

HashMap HashSet
實現了Map接口 實現Set接口
存儲鍵值對 僅存儲對象
調用 put()向map中添加元素 調用 add()方法向Set中添加元素
HashMap使用鍵(Key)計算Hashcode HashSet使用成員對象來計算hashcode值,對於兩個對象來說hashcode可能相同,所以equals()方法用來判斷對象的相等性,

8、HashSet如何檢查重復

  當你把對象加入HashSet時,HashSet會先計算對象的hashcode值來判斷對象加入的位置,同時也會與其他加入的對象的hashcode值作比較,如果沒有相符的hashcode,HashSet會假設對象沒有重復出現。但是如果發現有相同hashcode值的對象,這時會調用equals()方法來檢查hashcode相等的對象是否真的相同。如果兩者相同,HashSet就不會讓加入操作成功。

hashCode() 與equals() 的相關規定:

  1. 如果兩個對象相等,則hashcode一定也是相同的;
  2. 兩個對象相等,對兩個equals方法返回true;
  3. 兩個對象有相同的hashcode值,它們也不一定是相等的;
  4. 綜上,equals方法被覆蓋過,則hashCode方法也必須被覆蓋
  5. hashCode()的默認行為是對堆上的對象產生獨特值。如果沒有重寫hashCode(),則該class的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數據)。

==與equals的區別:

  1. ==是判斷兩個變量或實例是不是指向同一個內存空間,equals是判斷兩個變量或實例所指向的內存空間的值是不是相同。
  2. ==是指對內存地址進行比較,equals()是對字符串的內容進行比較。
  3. ==指引用是否相同,equals()指的是值是否相同。

 9、comparable 和 Comparator的區別

  • comparable接口實際上是出自java.lang包,它有一個 compareTo(Object obj)方法用來排序
  • comparator接口實際上是出自 java.util 包,它有一個compare(Object obj1, Object obj2)方法用來排序

內置比較器接口:Comparable<T>----------compareTo(Object obj)方法用來排序:

  compareTo(Object obj):該方法用於比較此對象與指定對象的順序。如果該對象小於、等於或大於指定對象,則分別返回負整數、零或正整數。 根據不同類的實現返回不同,大部分返回1,0和-1三個數。

public class Student implements Comparable<Student>{
    private int id;
    private String name;
    private double score;
    @Override
    public int compareTo(Student o) {
        //指定排序規則
        return  +(this.id - o.id);
    }
}
--------------------------------------------------------------------
public void Test() {
        List<Student> studentList = new ArrayList<Student>();
        studentList.add(new Student(1, "劉文傑", 100));
        studentList.add(new Student(2, "劉潔", 80));
        studentList.add(new Student(3, "李雪", 90));
        studentList.add(new Student(4, "黃育航", 59.4));

        Collections.sort(studentList);

        for (Student student : studentList) {
            System.out.println(student.getId() + ":" + student.getName() + ":"+ student.getScore());
        }
    }

外置比較器接口:Comparator<T>接口-----------compare(T o1,T o2)方法用來排序:

  compare(T o1,T o2):比較用來排序的兩個參數。返回如果是正整數,則交換兩個對象的位置。

public class StudentScoreDescComparator implements Comparator<Student> {
    @Override    
    public int compare(Student o1, Student o2) {
        if(o1.getScore() > o2.getScore()){
            return -1;
        }else{
            return 1;
        }
    }
}
---------------------------------------------------------------------
public void Test() {
        List<Student> studentList = new ArrayList<Student>();
        studentList.add(new Student(1, "劉文傑", 100));
        studentList.add(new Student(2, "劉潔", 80));
        studentList.add(new Student(3, "李雪", 90));
        studentList.add(new Student(4, "諸葛山珍", 59.4));
        Collections.sort(studentList, new StudentScoreDescComparator());
        for (Student student : stuList) {
            System.out.println(student.getId() + ":" + student.getName() + ":"+ student.getScore());
        }
}

10、如何選用集合?

  當我們需要根據鍵值獲取到元素值時就選用Map接口下的集合:

  • 需要排序時選擇TreeMap;
  • 不需要排序時就選擇HashMap;
  • 需要保證線程安全就選用ConcurrentHashMap。

  當我們只需要存放元素值時,就選擇實現Collection接口的集合:

  • 需要保證元素唯一時選擇實現Set接口的集合比如TreeSet或HashSet;
  • 不需要就選擇實現List接口的比如ArrayList或LinkedList,然后再根據實現這些接口的集合的特點來選用。

 

參考https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM