java面試熱點:集合框架(二)

  • Set接口
    Set接口與List接口的重要區別就是它不支持重復的元素,至多可以包含一個null類型元素。Set接口定義的是數學意義上的“集合”概念。
    Set接口主要定義了以下方法:
boolean add(E e)
void clear()
boolean contains(Object o)
boolean isEmpty()
boolean equals(Object obj)
Iterator<E> iterator()
boolean remove(Object o)
boolean removeAll(Collection<?> c)
int size()
Object[] toArray()
<T> T[] toArray(T[] a)

關于set家族,有一下描述:

  1. Set接口并沒有顯式要求其中的元素是有序或是無序的。
  2. Set接口有一個叫做SortedSet的子接口,這個接口可以用來實現對Set元素的排序。
  3. SortedSet有叫NavigableSet的子接口,這個接口定義的方法可以在有序Set中進行查找和遍歷。
  4. Jdk類庫中實現了Set接口的類主要有:AbstractSet,HashSet,TreeSet,EnumSet,LinkedHashSet等等。其中HashSet與TreeSet都是AbstractSet的子類。
  5. Java官方文檔中提到,HashSet和TreeSet分別基于HashMap和TreeMap實現(我們在后面會簡單介紹HashMap和TreeMap),他們的區別在于Set<E>接口是一個對象的集(數學意義上的”集合“),Map<K, V>是一個鍵值對的集合。

  • Queue接口
  1. Queue接口是對隊列這種數據結構的抽象。
  2. 一般的隊列實現允許我們高效的在隊尾添加元素,在隊列頭部刪除元素(First in, First out)。
  3. Queue<E>接口還有一個名為Deque的子接口,它允許我們高效的在隊頭或隊尾添加/刪除元素,實現了Deque<E>的接口的集合類即為雙端隊列的一種實現(比如LinkedList就實現了Deque接口)。
  4. 實現Queue接口的類主要有:AbstractQueue, ArrayDeque, LinkedList,PriorityQueue,DelayQueue等等。
    Queue接口定義了以下方法:
boolean add(E e) //添加一個元素到隊列中,若隊列已滿會拋出一個IllegalStateException異常
E element() //獲取隊頭元素
boolean offer(E e) //添加一個元素到隊列中,若隊列已滿返回false
E peek() //獲取隊頭元素,若隊列為空返回null
E poll() //返回并移除隊頭元素,若隊列為空返回null
E remove() //返回并移除隊頭元素

add與offer,element與peek,remove與poll看似是三對兒功能相同的方法。它們之間的重要區別在于前者若操作失敗會拋出一個異常,后者若操作失敗會從返回值體現出來(比如返回false或null),我們可以根據具體需求調用它們中的前者或后者。


  • Map接口
    java官方文檔對它的定義如下:

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.The Map
interface provides three collection views, which allow a map’s contents to be viewed as a set of keys, collection of values, or set of key-value mappings. The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. Some map implementations, like the TreeMap
class, make specific guarantees as to their order; others, like the HashMap
class, do not.

大概意思是:一個把鍵映射到值的對象被稱作一個Map對象。映射表不能包含重復的鍵,每個鍵至多可以與一個值關聯。
Map接口提供了三個集合視圖(關于集合視圖的概念我們下面會提到):鍵的集合視圖、值的集合視圖以及鍵值對的集合視圖。
一個映射表的順序取決于它的集合視圖的迭代器返回元素的順序。一些Map接口的具體實現(比如TreeMap),保證元素有一定的順序,其它一些實現(比如HashMap)不保證元素在其內部有序
Map接口讓我們能夠根據鍵快速檢索到它所關聯的值。也就是利用這個特性,Struts2框架中用ContextMap作為容器封裝一次請求所需的所有數據。 我們先來看看Map接口定義了哪些方法:

void clear()
boolean containsKey(Object key) //判斷是否包含指定鍵
boolean containsValue(Object value) //判斷是否包含指定值
boolean isEmpty()
V get(Object key) //返回指定鍵映射的值
V put(K key, V value) //放入指定的鍵值對
V remove(Object key)
int size()
Set<Map.Entry<K,V>> entrySet() 
Set<K> keySet()
Collection<V> values()

Map接口的具體實現類主要有:AbstractMap,EnumMap,HashMap,LinkedHashMap,TreeMap。HashTable。

  • 我們看一下HashMap的官方定義:

HashMap<K, V>是基于哈希表這個數據結構的Map接口具體實現,允許null鍵和null值。這個類與HashTable近似等價,區別在于HashMap不是線程安全的并且允許null鍵和null值。由于基于哈希表實現,所以HashMap內部的元素是無序的。HashMap對與get與put操作的時間復雜度是常數級別的(在散列均勻的前提下)。對HashMap的集合視圖進行迭代所需時間與HashMap的capacity(bucket的數量)加上HashMap的尺寸(鍵值對的數量)成正比。因此,若迭代操作的性能很重要,不要把初始capacity設的過高(不要把load factor設的過低)。

有兩個因素會影響一個HashMap對象的性能:intial capacity(初始容量)和load factor(負載因子)。intial capacity就是HashMap對象剛創建時其內部的哈希表的“桶”的數量(請參考哈希表的定義)。load factor等于maxSize / capacity,也就是HashMap所允許的最大鍵值對數與桶數的比值。增大load factor可以節省空間但查找一個元素的時間會增加,減小load factor會占用更多的存儲空間,但是get與put的操作會更快。當HashMap中的鍵值對數量超過了maxSize(即load factor與capacity的乘積),它會再散列,再散列會重建內部數據結構,桶數(capacity)大約會增加到原來的兩倍。
HashMap的構造器如下:

HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map<? extends K,? extends V> m) //創建一個新的HashMap,用m的數據填充

常用方法如下:

void clear()
boolean containsKey(Object key)
boolean containsValue(Object value)
V get(Object key)
V put(K key, V value)
boolean isEmpty()
V remove(Object key)
int size()
Collection<V> values()
Set<Map.Entry<K,V>> entrySet()
Set<K> keySet()

它們的功能都很直觀,更多的使用細節可以參考Java官方文檔,這里就不貼上來了。這里簡單地提一下WeakHashMap,它與HashMap的區別在于,存儲在其中的key是“弱引用”的,也就是說,當不再存在對WeakHashMap中的鍵的外部引用時,相應的鍵值對就會被回收。關于WeakHashMap和其他類的具體使用方法及注意事項,大家可以參考官方文檔。

  • 下面我們來簡單地介紹下另一個Map接口的具體實現——TreeMap。
    它的官方定義是這樣的:

TreeMap<K, V>一個基于紅黑樹的Map接口實現。TreeMap中的元素的有序的,排序的依據是存儲在其中的鍵的natural ordering(自然序,也就是數字從小到大,字母的話按照字典序)或者根據在創建TreeMap時提供的Comparator對象,這取決于使用了哪個構造器。TreeMap的containsKey, get, put和remove操作的時間復雜度均為log(n)。

TreeMap有以下構造器:

TreeMap() //使用自然序對其元素進行排序
TreeMap(Comparator<? super K> comparator) //使用一個比較器對其元素進行排序
TreeMap(Map<? extends K,? extends V> m) //構造一個與映射表m含有相同元素的TreeMap,用自然序進行排列
TreeMap(SortedMap<K,? extends V> m) //構造一個與有序映射表m含有相同元素及元素順序的TreeMap

它的常見方法如下:

K ceilingKey(K key)
void clear()
Comparator<? super K> comparator() //返回使用的比較器,若按自然序則返回null
boolean containsKey(Object key)
boolean containsValue(Object value)
NavigableSet<K> descendingKeySet() //返回一個包含在TreeMap中的鍵的逆序的NavigableSet視圖
NavigableMap<K,V> descendingMap()
Set<Map.Entry<K,V>> entrySet()
Map.Entry<K,V> firstEntry() //返回鍵最小的鍵值對
Map.Entry<K,V> floorEntry(K key) //返回一個最接近指定key且小于等于它的鍵對應的鍵值對
K floorKey(K key)
V get(Object key)
Set<K> keySet()
Map.Entry<K,V> lastEntry() //返回與最大的鍵相關聯的鍵值對
K lastKey()

建議讀者先了解下紅黑樹這個數據結構的原理及實現(可參考算法(第4版) (豆瓣)),然后再去看官方文檔中關于這個類的介紹,這樣學起來會事半功倍。

  • 再簡單地介紹下NavigableMap<K, V>這個接口:

實現了這個接口的類支持一些navigation methods,比如lowerEntry(返回小于指定鍵的最大鍵所關聯的鍵值對),floorEntry(返回小于等于指定鍵的最大鍵所關聯的鍵值對),ceilingEntry(返回大于等于指定鍵的最小鍵所關聯的鍵值對)和higerEntry(返回大于指定鍵的最小鍵所關聯的鍵值對)。一個NavigableMap支持對其中存儲的鍵按鍵的遞增順序或遞減順序的遍歷或訪問。NavigableMap<K, V>接口還定義了firstEntry、pollFirstEntry、lastEntry和pollLastEntry等方法,以準確獲取指定位置的鍵值對。

總的來說,NavigableMap<K, V>接口正如它的名字所示,支持我們在映射表中”自由的航行“,正向或者反向迭代其中的元素并獲取我們需要的指定位置的元素。TreeMap實現了這個接口。


  • 視圖(View)與包裝器
    Java中的集合視圖是用來查看集合中全部或部分數據的一個”窗口“,只不過通過視圖我們不僅能查看相應集合中的元素,對視圖的操作還可能會影響到相應的集合。比如TreeMap和HashMap的keySet()方法就會返回一個相應映射表對象的視圖。通過使用視圖可以獲得其他的實現了Map接口或Collection接口的對象。
    也就是說,keySet方法返回的視圖是一個實現了Set接口的對象,這個對象中又包含了一系列鍵對象。
  • 輕量級包裝器
    Arrays.asList方法包裝了Java數組的集合視圖(實現了List接口)。請看以下代碼:
public static void main(String[] args) {
  String[] strings = {"first", "second", "third"};
  List<String> stringList = Arrays.asList(strings);
  String s1 = stringList.get(0);
  System.out.println(s1);
  stringList.add(0, "new first");
}

注意:以上代碼會編譯成功,但是在運行時會拋出一個UnsupportedOperationException異常,原因是調用了改變列表大小的add方法。Arrays.asList方法返回的封裝了底層數組的集合視圖不支持對改變數組大小的方法(如add方法和remove方法)的調用(但是可以改變數組中的元素)。

  • 子范圍
    很多集合類型建立一個稱為子范圍(subrange)的集合視圖。例如以下代碼抽出group中的第10到19個元素(從0開始計數)組成一個子范圍:
List subgroup = group.subList(10, 20); //group為一個實現了List接口的集合

List接口所定義的操作都可以應用于子范圍,包括那些會改變列表大小的方法,比如以下方法會把subgroup列表清空,同時group中相應的元素也會從列表中移除:

subgroup.clear();

對于實現了SortedSet<E>接口的有序集或是實現了SortedMap<K, V>接口的有序映射表,我們也可以為他們創建子范圍。SortedSet接口定義了以下三個方法:

SortedSet<E> subSet(E from, E to); 
SortedSet<E> headSet(E to);
SortedSet<E> tailSet(E from);

SortedMap也定義了類似的方法:

SortedMap<K, V> subMap(K from, K to);
SortedMap<K, V> headMap(K to);
SortedMap<K, V> tailMap(K from);
  • 不可修改的視圖
    Collections類中的一些方法可以返回不可修改視圖(unmodifiable views):
Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
  • 同步視圖
    若集合可能被多個線程并發訪問,那么我們就需要確保集合中的數據不會被破壞。Java類庫的設計者使用視圖機制來確保常規集合的線程安全。比如,我們可以調用以下方法將任意一個實現了Map接口的集合變為線程安全的:
    Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
  • 集合視圖的本質

集合視圖本身不包含任何數據,它只是對相應接口的包裝。集合視圖所支持的所有操作都是通過訪問它所關聯的集合類實例來實現的。我們來看看HashMap的keySet方法的源碼:

public Set<K> keySet() {
  Set<K> ks;
  return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
} 

final class KeySet extends AbstractSet<K> {
  public final int size() { 
    return size; 
  }
  public final void clear() { 
    HashMap.this.clear(); 
  }
  public final Iterator<K> iterator() { 
    return new KeyIterator(); 
  }
  public final boolean contains(Object o) { 
    return containsKey(o); 
  }
  public final boolean remove(Object key) {
    return removeNode(hash(key), key, null, false, true) != null;
  }
  public final Spliterator<K> spliterator() {
    return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
  }
  public final void forEach(Consumer<? super K> action) {
    Node<K,V>[] tab;
    if (action == null) throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
      int mc = modCount;
      for (int i = 0; i < tab.length; ++i) {
        for (Node<K,V> e = tab[i]; e != null; e = e.next)
          action.accept(e.key);
        }
        if (modCount != mc) throw new ConcurrentModificationException();
      }
  }
}

可以看到,實際上keySet()方法返回一個內部final類KeySet的實例。我們可以看到KeySet類本身沒有任何實例變量。我們再看KeySet類定義的size()實例方法,它的實現就是通過直接返回HashMap的實例變量size。還有clear方法,實際上調用的就是HashMap對象的clear方法。

keySet方法能夠讓你直接訪問到Map的鍵集,而不需要復制數據或者創建一個新的數據結構,這樣做往往比復制數據到一個新的數據結構更加高效。

  • Collections類
    Collections類與Collection接口的區別:Collection是一個接口,而Collections是一個類(可以看做一個靜態方法庫)。下面我們看一下官方文檔對Collections的描述:

Collections類包含了大量用于操作或返回集合的靜態方法。它包含操作集合的多態算法,還有包裝集合的包裝器方法等等。這個類中的所有方法在集合或類對象為空時均會拋出一個NullPointerException。

  • 說下面試經常問的HsahMap和HashTable的區別:
  1. 正如上文所說,HashMap<K,V>是基于哈希表這個數據結構的具體實現,其中鍵和值都是對象,并且不能包含重復鍵,但可以包含重復值。HashMap允許null key和null value,而hashtable不允許。
  2. HashMap是Hashtable的輕量級實現(非線程安全的實現),他們都完成了Map接口,主要區別在于HashMap允許空(null)鍵值(key),由于非線程安全,效率
    上可能高于Hashtable。
    HashMap允許將null作為一個entry的key或者value,而Hashtable不允許。
    HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因為contains方法容易讓人引起誤解。
  3. Hashtable繼承自Dictionary類,而HashMap是Java1.2引進的Map interface的一個實現。
  4. 最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多個線程訪問Hashtable時,不需要自己為它的方法實現同步,而HashMap 就必須為之提供外同步。
  5. Hashtable和HashMap采用的hash/rehash算法都大概一樣,所以性能不會有很大的差異。

總結

關于Java集合框架,我們首先應該把握住幾個核心的接口,請看下圖:


集合大家庭.jpg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 概述 Java集合框架由Java類庫的一系列接口、抽象類以及具體實現類組成。我們這里所說的集合就是把一組對象組織到...
    absfree閱讀 1,285評論 0 10
  • Collection ├List │├LinkedList │├ArrayList │└Vector │└Stac...
    AndyZX閱讀 893評論 0 1
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,779評論 18 399
  • 標簽(空格分隔): Java集合框架 問題思考 什么是集合框架? 為什么用集合框架? 怎么用集合框架? 問題解決 ...
    outSiderYN閱讀 698評論 0 13
  • 看這個電影前,已經看過一些影評,說是死亡教育。。。也有人推薦說特別感人等等。不知道是不是沒有進入狀態,有很感人之處...
    我乃靜靜閱讀 884評論 2 2