Java學習筆記7——集合框架

一、集合框架

拿一張網上搜索的集合框架的圖。

集合框架的示意圖

首先來整體講一下集合框架。
整個集合框架的接口都是源於Collection接口。Collection接口繼承了Iterable接口,使得所有繼承Collection接口的接口,在被實現之後,都可以使用迭代器Iterator訪問和遍歷。

public interface Iterable<T> {
    Iterator<T> iterator();
}

先說一下Collection接口下聲明的方法。
(1)獲得集合的大小:int size()
(2)判斷集合是否爲空:boolean isEmpty()
(3)是否包含指定的Object對象:boolean contains(Object o)
(4)獲取迭代器:Iterator< E > iterator()
(5)將集合轉爲Object數組(無參):Object[] toArray()
(6)使用泛型,對集合進行數組轉換:< T > T[] toArray(T[] a);
(7)增加元素:boolean add(E e)
(8)刪除元素:boolean remove(Object o)
(9)與目標集合進行比較,判斷是否相同:boolean containsAll(Collection< ? > c)
(10)往集合中增加一個集合:boolean addAll(Collection< ? extends E > c)
(11)刪除在目標集合中的指定集合內的元素:boolean removeAll(Collection< ? > c)
(12)取得兩個集合的交集:boolean retainAll(Collection< ? > c)
(13)清空集合:void clear()
(14)與集合進行比較:boolean equals(Object o)
(15)獲取哈希值:int hashCode()

List接口和Set接口繼承了Collection接口。而Map接口也是集合框架中的一個接口,但它的實現類是以鍵值對的形式作存儲,與其他兩個接口不一樣。

下面分別說一說:

1、List

List是一個有序的集合接口,可以類比順序表等線性表。
List接口是支持對元素進行增刪改查動作的(add,set,get,remove)。List是有索引存在的。所以實現List的類都可以通過get(int index)獲取對應的元素。
遍歷List有2種,迭代器迭代遍歷循環遍歷+get方法

實現List接口的常見類有ArrayList、LinkedList、Vector、Stack、CopyOnWriteArrayList等。

List特有的迭代器ListIterator
ListIterator< E > listIterator()

Iterator接口的方法:
(1)刪除集合中Iterator指向位置後面的元素:void remove()
(2)返回集合中Iterator指向位置後面的元素:E next()
(3)迭代器指向位置後面是否還有元素:boolean hasNext()

而ListIterator接口中新聲明的方法有:
(ListIterator繼承了Iterator)
(1)void add(E e)
將指定的元素插入列表,插入位置爲迭代器當前位置之前
(2)void set(E e)
從列表中將next()或previous()返回的最後一個元素返回的最後一個元素更改爲指定元素e
(3)E previous()
返回列表中ListIterator指向位置前面的元素
(4)boolean hasPrevious()
逆向遍歷列表,判斷列表迭代器前是否還有元素
(5)int previousIndex()
返回列表中ListIterator所需位置前面元素的索引
(6)int nextIndex()
返回列表中ListIterator所需位置後面元素的索引

ListIterator可以從頭或者從尾進行遍歷。ListIterator可以遍歷修改,而Iterator只能遍歷,不能修改。

//使用迭代器
Iterator<Object> it=list.iterator();
while(it.hasNext()){
    Object obj = it.next();
    System.out.println(obj);
}

//使用ListIterator
ListIterator<Object> listIt = list.listIterator();
while(listIt .hasNext()){
    Object obj = listIt .next();
    System.out.println(obj);
}

//使用foreach
for(Object obj : list){
    System.out.println(obj);
}

下面來看看實現List接口的類。

(1)ArrayList
ArrayList是數據結構中的數組類型。
默認構造方法是新建一個長度爲10的數組。

public ArrayList() {
    this(10);
}

其遍歷效率高,但增加刪除麻煩。
因爲ArrayList新建的時候,是通過創建新數組再複製老數組至新數組完成的,所以增加或刪除的時候效率低。

這裏列一下add的源碼

public boolean add(E e) {
    ensureCapacity(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

通過計算add後的長度與現容量的大小作比較,必要時增加數組容量(新建一個更大的數組,然後將老數組複製至新數組),確保新加進來的數據是可以正常保存的。

public void ensureCapacity(int minCapacity) {
    modCount++;
    int oldCapacity = elementData.length;
    if (minCapacity > oldCapacity) {
        Object oldData[] = elementData;
        int newCapacity = (oldCapacity * 3)/2 + 1;
            if (newCapacity < minCapacity)
        newCapacity = minCapacity;
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
    }
}

ArrayList是線程不安全的(因爲ArrayList內部沒有使用同步)。在需要考慮多線程的情況下,可以通過Collections.synchronizedList(List i)函數返回一個線程安全的ArrayList類,或者使用Concurrent併發包下對應的集合類。

(2)LinkedList

LinkedList是基於雙向循環鏈表實現的,是鏈表結構,不同步。
因爲LinkedList是雙向循環鏈表,所以順序訪問效率高,但是隨機訪問效率低。
由於實現了Queue接口,因此也可以用於實現堆棧、隊列。

LinkedList接口

因爲LinkedList實現了List接口,所以其類中也有get和remove等方法根據索引值獲取值。它是通過讓索引與1/2的長度做比較,決定從哪邊開始遍歷,從而較快找到對應索引的元素。其他利用索引進行的操作也是如此。(通過調用entry(index)的方法)

public E get(int index) {
    return entry(index).element;
}
private Entry<E> entry(int index) {
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
    Entry<E> e = header;
    if (index < (size >> 1)) {
        for (int i = 0; i <= index; i++)
            e = e.next;
    } else {
        for (int i = size; i > index; i--)
            e = e.previous;
    }
    return e;
}

而Entry是LinkedList內部的一個私有靜態類。Entry類是一個鏈表的節點結構。包括了存儲的數據,節點前和節點後對應的節點。

在LinkedList中,也包含了隊列、堆棧相關操作的方法。
首先看一下LinkedList作爲鏈表的方法。
獲取LinkedList的第一個元素:E getFirst()
獲取LinkedList的最後一個元素:E getLast()
刪除LinkedList的第一個元素:E removeFirst()
刪除LinkedList的最後一個元素:E removeLast()
在起始位置增加元素:addFirst(E e)
在結束位置增加元素:addLast(E e)

下面是關於模擬隊列的操作:
返回第一個節點:E peek()
當大小爲0時,返回null

public E peek() {
    if (size==0)
        return null;
    return getFirst();
}

返回第一個節點:E element()
當大小爲0時,拋出異常(NoSuchElementException)

public E element() {
    return getFirst();
}

移除第一個元素:E poll()
移除第一個元素:E remove()
區別與上面的相同。

將元素添加到末端:boolean offer(E e)
將元素添加到第一個位置:boolean offerFirst(E e)
將元素添加到末端:boolean offerLast(E e)
返回第一個節點:E peekFirst()
返回最後一個節點:E peekLast()
刪除並返回第一個節點:E pollFirst()
刪除並返回最後一個節點:E pollLast()

模擬堆棧:
添加到第一個節點(進堆):void push(E e)
刪除並返回第一個節點:E pop()


從LinkedList的實現方式可以發現,LinkedList不會出現容量不足的問題(雙向鏈表特性)
因爲LinkedList也實現了Deque接口,所以LinkedList也可以被當做雙向隊列進行使用。

隊列方法        等效方法
add(e)          addLast(e)
offer(e)        offerLast(e)
remove()        removeFirst()
poll()          pollFirst()
element()       getFirst()
peek()          peekFirst()

也可以當做堆棧進行使用:

棧方法             等效方法
push(e)             addFirst(e)
pop()               removeFirst()
peek()              peekFirst()

(3)Vector(已過時)
因爲現在也不建議使用這個類了,所以就粗略講講就好了。
Vector實現了一個動態數組。和ArrayList和相似。

Vector是同步訪問的。
Vector包含了許多傳統的方法,這些方法不屬於集合框架。

爲什麼不建議使用Vector?
因爲Vector是線程安全的,所以它的性能是比ArrayList差很多的。而且現在也有更好的辦法可以解決ArrayList線程不安全的問題。所以就過時了。

(4)Stack(已過時)
堆棧類。(過時的我也不想看了= =。懶)
爲什麼不建議使用Stack?
因爲Stack繼承了Vector。原因不講了。

(5)CopyOnWriteArrayList
CopyOnWriteArrayList是一個ArrayList的線程安全的變體。其實這裏可以抽成CopyOnWrite容器進行討論,下面引用一下別人的文章段落來說說。

其實是一個CopyOnWrite的容器。當我們往一個容器添加元素的時候,不是直接往當前容器添加,而是先將當前容器進行復制,複製出一個新的容器,然後往新容器裏添加元素,添加完之後,再將原容器的引用指向新容器。這樣可以對CopyOnWrite容器進行併發的讀,也不需要加鎖。CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。

看看add方法。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

在複製容器的過程中,是加了鎖了,這樣才能保證不會複製多個容器。
而在讀的時候,是不需要加鎖的,就算有多個線程正在向CopyOnWriteArrayList添加數據,讀還是會讀到舊的數據,因爲寫的時候不會鎖住舊的CopyOnWriteArrayList。

CopyOnWrite併發容器用於讀多寫少的併發場景。

CopyOnWrite容器有很多優點,但是同時也存在兩個問題,即內存佔用問題和數據一致性問題。所以在開發的時候需要注意一下。
  內存佔用問題。因爲CopyOnWrite的寫時複製機制,所以在進行寫操作的時候,內存裏會同時駐紮兩個對象的內存,舊的對象和新寫入的對象(注意:在複製的時候只是複製容器裏的引用,只是在寫的時候會創建新對象添加到新容器裏,而舊容器的對象還在使用,所以有兩份對象內存)。如果這些對象佔用的內存比較大,比如說200M左右,那麼再寫入100M數據進去,內存就會佔用300M,那麼這個時候很有可能造成頻繁的Yong GC和Full GC。之前我們系統中使用了一個服務由於每晚使用CopyOnWrite機制更新大對象,造成了每晚15秒的Full GC,應用響應時間也隨之變長。
  針對內存佔用問題,可以通過壓縮容器中的元素的方法來減少大對象的內存消耗,比如,如果元素全是10進制的數字,可以考慮把它壓縮成36進制或64進制。或者不使用CopyOnWrite容器,而使用其他的併發容器,如ConcurrentHashMap。
  數據一致性問題。CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。


什麼場景下更適宜使用LinkedList,而不用ArrayList
(1) 你的應用不會隨機訪問數據。因爲如果你需要LinkedList中的第n個元素的時候,你需要從第一個元素順序數到第n個數據,然後讀取數據。
(2)你的應用更多的插入和刪除元素,更少的讀取數據。因爲插入和刪除元素不涉及重排數據,所以它要比ArrayList要快。


2、Set接口

Set接口中聲明的方法與List基本一致。但是實現Set接口的類中的元素則是無序的、唯一的(不可重複)。
實現Set接口的類有:HashSet,LinkedHashSet,TreeSet,CopyOnWriteArraySet等等。

遍歷Set的方式:

// 迭代遍歷:
Set<String> set = new HashSet<String>();
Iterator<String> it = set.iterator();
while (it.hasNext()) {
  String str = it.next();
  System.out.println(str);
}

// for循環遍歷:
for (String str : set) {
      System.out.println(str);
}

(1)HashSet
HashSet是不保證順序的,允許使用null元素。

HashSet底層結構是使用HashMap作存儲。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{

    private transient HashMap<E,Object> map;

    public HashSet() {
    map = new HashMap<E,Object>();
    }

    ……
}

而且HashSet實現不是同步的,涉及多個線程同時訪問的時候,必須保持外部同步。使用 Collections.synchronizedSet 方法來“包裝” Set。

Set s = Collections.synchronizedSet(new HashSet(...));

HashSet擁有List接口中的方法,用法一致,但實現方式不同。
下面舉例add方法。

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

PRESENT是類中定義的一個對象,個人理解其實就是用來充數佔位而已。而使用要增加的對象做鍵,存入HashMap中。這就是爲什麼HashSet是無序且唯一的原因。
其他的方法都是對內置的HashMap對象進行數據操作。具體操作等到後面說到HashMap的時候再詳細說。

如果需要將自定義對象放入HashSet中,需要重寫該對象對應類的equals方法和hashCode()方法。

(2)LinkedHashSet
LinkedHashSet繼承了HashSet,是使用哈希表和鏈表實現的。
LinkedHashSet底層使用LinkedHashMap來保存所有元素。這個與HashSet類似。

它定義了迭代順序,即按照將元素插入到 set 中的順序(插入順序)進行迭代。使得集合保證唯一性,看起來就像以插入順序保存一樣。
LinkedHashSet在迭代訪問Set中的全部元素時,性能比HashSet好,但是插入時性能稍微遜色於HashSet。

(3)TreeSet
emmmm….猜的沒錯。
還是老樣子。TreeSet的底層確實又是用TreeMap。TreeSet是基於TreeMap的NavigableSet接口(NavigableSet接口繼承了SortedSet接口)實現的。
所以,TreeSet是實現SortedSet的其中一個實現類。(另一個是ConcurrentSkipListSet)
因爲TreeSet實現了SortedSet接口,所以它能夠確保集合元素處於排序狀態。
TreeSet支持2種排序方式,自然排序和定製排序。這個取決於使用的構造方法。

構造一個包含指定 collection 元素的新 TreeSet,它按照其元素的自然順序進行排序。

public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}

自然排序使用要排序元素的CompareTo(Object obj)方法來比較元素之間大小關係,然後將元素按照升序排列。

構造一個新的空 TreeSet,它根據指定比較器進行排序。

public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<E,Object>(comparator));
}

如果要定製排序,應該使用Comparator接口,實現 int compare(T o1,T o2)方法。

(4)CopyOnWriteArraySet
這個類跟上面講過的CopyOnWriteArrayList類似。它是線程安全的、無序的集合。可以看成是一個線程安全的HashSet。而需要注意的一點是,CopyOnWriteArraySet是通過CopyOnWriteArrayList(動態數組隊列)實現的。

private final CopyOnWriteArrayList<E> al;

public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

因爲寫操作需要複製一個新的數組,所以對於寫操作相關的開銷很大。

這裏引用一個例子,這個例子很好說明了CopyOnWriteArraySet的使用方式:

public class CollectionDemo {

    // TODO: set是HashSet對象時,程序會出錯。
    //private static Set<String> set = new HashSet<String>();
    private static Set<String> set = new CopyOnWriteArraySet<String>();
    public static void main(String[] args) {

        // 同時啓動兩個線程對set進行操作!
        new MyThread("ta").start();
        new MyThread("tb").start();
    }

    private static void printAll() {
        String value = null;
        Iterator iter = set.iterator();
        while(iter.hasNext()) {
            value = (String)iter.next();
            System.out.print(value+", ");
        }
        System.out.println();
    }

    private static class MyThread extends Thread {
        MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
                int i = 0;
            while (i++ < 10) {
                // “線程名” + "-" + "序號"
                String val = Thread.currentThread().getName() + "-" + (i%6);
                set.add(val);
                // 通過“Iterator”遍歷set。
                printAll();
            }
        }
    }
}

運行結果:

ta-1, tb-1, 
ta-1, tb-1, ta-2, 
ta-1, tb-1, ta-2, ta-3, 
ta-1, tb-1, ta-2, ta-3, ta-4, 
ta-1, ta-1, tb-1, tb-1, 
ta-2, ta-1, ta-3, tb-1, ta-4, ta-2, ta-5, 
ta-3, ta-4, ta-5, ta-1, tb-2, 
tb-1, ta-1, ta-2, tb-1, ta-3, ta-2, ta-4, ta-3, ta-5, ta-4, tb-2, ta-5, ta-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, 
ta-1, tb-2, tb-1, ta-0, ta-2, ta-3, ta-4, ta-5, tb-3, 
tb-2, ta-0, tb-3, 
ta-1, ta-1, tb-1, tb-1, ta-2, ta-2, ta-3, ta-4, ta-5, ta-3, tb-2, ta-4, ta-0, ta-5, tb-3, 
tb-2, ta-0, ta-1, tb-3, tb-1, tb-4, 
ta-2, ta-3, ta-1, ta-4, tb-1, ta-5, ta-2, tb-2, ta-3, ta-0, ta-4, tb-3, ta-5, tb-4, 
tb-2, ta-0, tb-3, tb-4, tb-5, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 

3、Map接口

Map接口的實現類的特點是以鍵值對(key-value)的形式進行存儲。

public interface Map< K, V > {...}

常見的實現類有:HashMap,Hashtable,LinkedHashMap,TreeMap,Attributes,Properties等等。

下面先介紹一下Map接口中一些聲明的主要方法。
int size():返回map集合的大小。
boolean isEmpty():判斷集合是否爲空。
boolean containsKey(Object key):判斷是否包含某個key。
boolean containsValue(Object value):判斷是否包含某個value。
V get(Object key):通過key獲取值。
V put(K key, V value):向map集合中新增元素。
V remove(Object key):根據key移除對應元素。
void clear():清空map集合。
Set< K > keySet():獲取key的集合。
還有equals和hashCode兩個常見的方法,就不列舉了。

遍歷Map的方法:

Map<Object, Object> map = new HashMap<>();
// 遍歷鍵
for(Object obj : map.keySet()){
    System.out.println("key = " + obj);
}

// Map中的值遍歷
for(Object obj : map.values()){
    System.out.println("value = " + obj);
}

// 在for-each循環中使用entries來遍歷
for (Map.Entry<Object , Object > entry : map.entrySet()) {
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());  
}  

// 使用Iterator遍歷
Iterator<Map.Entry<Object , Object >> entries = map.entrySet().iterator();
while (entries.hasNext()) {  
    Map.Entry<Integer, Integer> entry = entries.next();  
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());  
}  

// 鍵值遍歷
for (Object key : map.keySet()) { 
    Object value = map.get(key);  
    System.out.println("Key = " + key + ", Value = " + value);  

}

(1)HashMap

HashMap它實現了Map接口,所以存儲的元素也是鍵值對映射的結構,並允許使用null值和null鍵,其內元素是無序的,如果要保證有序,可以使用LinkedHashMap。HashMap是線程不安全的。

HashMap的構造方法。

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
}

HashMap中key和value都允許爲null。key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中。

下面是put方法:

public V put(K key, V value) {
    // 處理key爲null,HashMap允許key和value爲null  
    if (key == null)
        return putForNullKey(value);
    // 得到key的哈希碼
    int hash = hash(key.hashCode());
    // 通過哈希碼計算出bucketIndex  
    int i = indexFor(hash, table.length);
    // 取出bucketIndex位置上的元素,並循環單鏈表,判斷key是否已存在
    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;
        }
    }
    // key不存在時,加入新元素
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

還有關於HashMap的深入理解的文章《HashMap之深入理解》

看完上面的文章之後,自己初步總結的結論是:
HashMap在得到哈希值的時候,先是使用了hashCode計算出key的hash值,然後再調用本身的hash方法做更細緻的計算,以降低hash衝突發生的概率(自認爲一個好的哈希函數可以降低哈希衝突發生的次數)。在發生哈希衝突的時候,HashMap採用鏈表解決衝突。將新的節點(addEntry)加到對應位置的表頭(第一個位置)。
HashMap另一個是及時擴容,通過哈希表容量*負載因子計算出一個臨界值,到達這個臨界值的時候,將做擴容操作,然後將老數組複製後給新數組。所以合理利用哈希表長度和負載因子可以有效提供HashMap的性能。擴容之後,元素對應的位置一般會發生改變。

另一個問題是,HashMap的方法是不同步的(線程不安全的),解決這個問題的方法可以有:
一是使用Hashtable;二是使用Collections類的synchronizedMap方法得到一個封裝對象。

(2)Hashtable
Hashtable與HashMap類似,包括底層實現,哈希衝突的處理,擴容等等,但是Hashtable是線程安全的,HashMap是線程不安全的。


當然此處經常會有一個問題會遇到:
Hashtable和HashMap的區別
①繼承的父類不同。Hashtable基於Dictionary類,而HashMap是基於AbstractMap。
②Hashtable的鍵和值都不允許爲null值;而HashMap允許鍵和值都是null。
③Hashtable的方法是同步的,線程安全;HashMap的方法是不同步的,線程不安全。
④hash值不同。哈希值的使用不同,Hashtable直接使用對象的hashCode。而HashMap重新計算hash值。
⑤內部實現使用的數組初始化和擴容方式不同。
Hashtable在不指定容量的情況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量一定要爲2的整數次冪,而HashMap則要求一定爲2的整數次冪。
Hashtable擴容時,將容量變爲原來的2倍+1,而HashMap擴容時,將容量變爲原來的2倍。
Hashtable和HashMap它們兩個內部實現方式的數組的初始大小和擴容的方式。HashTable中hash數組默認大小是11,增加的方式是 old*2+1。

(3)LinkedHashMap
LinkedHashMap繼承了HashMap類,與HashMap有着同樣的存儲結構,但它又增加了一個雙向鏈表的頭結點,將所有添加到LinkedHashmap的節點一一串成了一個雙向循環鏈表,所以LinkedHashMap也保留了節點插入的順序,可以使節點的輸出順序與輸入順序相同。

public class LinkedHashMap<K,V>
    extends HashMap<K,V> implements Map<K,V>{

    private transient Entry<K,V> header;
    ...
}

其構造函數

// 默認構造方法
public LinkedHashMap() {
    super();
    accessOrder = false;
}

// 指定集合的初始容量大小的構造方法
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

// 指定集合初始容量大小和負載因子的構造方法
public LinkedHashMap(int initialCapacity, float loadFactor){
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

// 指定集合容量大小、負載因子、排序方式的構造方法
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

// 傳入Map的構造方法
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super(m);
    accessOrder = false;
}

其中accessOrder返回false代表是基於插入順序,返回true代表是基於訪問順序。

LinkedHashMap內部類Entry定義了其前後節點的引用,構成了一個雙向鏈表的結構基礎。

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    Entry<K,V> before, after;
    ...
}

在LinkedHashMap中,put方法未重寫,而是重寫了父類HashMap中put方法中的調用方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的雙向鏈接列表的實現。

LinkedHashMap 能夠做到按照插入順序或者訪問順序進行迭代。

(4)TreeMap
TreeMap是基於紅黑樹實現。該映射根據其鍵的自然順序進行排序,或者根據創建映射時提供的 Comparator 進行排序,具體取決於使用的構造方法。TreeMap是非同步的。

// 默認構造函數。使用該構造函數,TreeMap中的元素按照自然排序進行排列。
public TreeMap() {
    comparator = null;
}

// 創建的TreeMap包含Map
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}

// 指定Tree的比較器
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

// 創建的TreeSet包含SortedMap
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}
// 這是TreeMap很重要的成員變量
private final Comparator<? super K> comparator;
private transient Entry<K,V> root = null;
private transient int size = 0;

這是TreeMap內部類Entry的結構,內部結構是樹。

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left = null;
    Entry<K,V> right = null;
    Entry<K,V> parent;
    boolean color = BLACK;

    ...
}

此處關於紅黑樹的內容不做描述(因爲我還沒去看= =以後補上),因爲TreeMap是基於紅黑樹算法實現的,所以此處就暫時到這裏了。先附上一篇文章。
《Java提高篇(二七)—–TreeMap》


最後附上一個問題:

重寫equals要滿足幾個條件
自反性:對於任何非空引用值 x,x.equals(x) 都應返回 true。
對稱性:對於任何非空引用值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true。
傳遞性:對於任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,並且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回 true。
一致性:對於任何非空引用值 x 和 y,多次調用 x.equals(y) 始終返回 true 或始終返回 false,前提是對象上 equals 比較中所用的信息沒有被修改。
對於任何非空引用值 x,x.equals(null) 都應返回 false。


其實集合框架的內容很多也很細,本來想做一些詳細的學習和介紹,但這裏只是做一個簡單的瞭解和描述。也是自己學習Java中集合框架的一個起點吧,再接再厲。因爲第一次寫,沒什麼經驗,結構和內容應該是比較混亂的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章