Java集合包

集合包是java中最經常使用的包,最經常使用的有Collection和Map接口的實現類,前者用於存放多個單對象,後者用於存放key-value形式的鍵值對。html

java集合包經常使用實現類結構圖以下所示(I表示接口)java

 

 

 

 

 

 

 

1. List

線性表,有序集合,元素能夠重複。程序員

1.1 ArrayList

動態數組,底層即數組,能夠用來容納任何對象,要求連續存放。ArrayList的重要成員是Object[] elementDate int Size表示有效元素的個數,數組的初始大小是10,擴容操做示意圖以下,以前(上面)這塊內存變成垃圾。 所以在初始化時儘可能指定初始容量,避免擴容產生的內存垃圾,影響性能。面試

public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
}// 指定初始化大小

特色及使用注意

(1)foreach中可否對其基本類型元素值(包括數字、字符串、布爾)進行賦值改變?算法

 不能,高級For在JDK 5.0開始引入,用其迭代代碼簡潔,在foreach中修改的只是元素的副本,並不會改變原值(反編譯以後可發現這一點),因此高級For循環能夠用來遍歷查詢,不可修改當前取回的元素自己。swift

參考連接:foreach的一個「奇怪」現象—實現原理分析數組

可是對象類型的元素有所不一樣,若是對對象數組進行遍歷,則能夠修改元素的狀態,在每一輪循環中均可以拿到對象引用對其狀態(成員變量、類變量)進行修改。緩存

(2)如何正確遍歷並刪除ArrayList元素值?安全

問題背景:給定字符串集合["a","b","b","c"],刪除其中元素"b"。數據結構

常見錯誤寫法1:

public static void remove(List<String> list) 
	{
		for (int i = 0; i < list.size(); i++) 
		{
			String s = list.get(i);
			if (s.equals("b")) 
			{
				list.remove(s);
			}
		}
	}
// 錯誤的緣由:這種最普通的循環寫法執行後會發現第二個「b」的字符串沒有刪掉。

常見錯誤寫法2:

public static void remove(List<String> list) 
	{
		for (String s : list)
		{
			if (s.equals("b")) 
			{
				list.remove(s);
			}
		}
	}
// 使用加強的for循環 在循環過程當中從List中刪除元素之後,繼續循環List時會報ConcurrentModificationException,但刪除以後立刻就跳出的也不會出現異常

思考及對策

爲什麼和出現錯誤1,怎麼解決?

public boolean remove(Object o) {
		if (o == null) {
			for (int index = 0; index < size; index++)
				if (elementData[index] == null) {
					fastRemove(index);
					return true;
				}
		} else {
			for (int index = 0; index < size; index++)
				if (o.equals(elementData[index])) {
					fastRemove(index);
					return true;
				}
		}
		return false;
	}

private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,numMoved);
        elementData[--size] = null; // Let gc do its work
    }

能夠看到會執行System.arraycopy方法,致使刪除元素時涉及到數組元素的移動。針對錯誤寫法1,在遍歷第一個字符串b時由於符合刪除條件,因此將該元素從數組中刪除,而且將後一個元素移動(也就是第二個字符串b)至當前位置,致使下一次循環遍歷時後一個字符串b並無遍歷到,因此沒法刪除。針對這種狀況能夠倒序刪除的方式來避免:

public static void remove(ArrayList<String> list) 
	{
		for (int i = list.size() - 1; i >= 0; i--) 
		{
			String s = list.get(i);
			if (s.equals("b")) 
			{
				list.remove(s);
			}
		}
	}

爲什麼會出現錯誤2怎麼解決?

final void checkForComodification() {
		if (modCount != expectedModCount)
			throw new ConcurrentModificationException();
	}

這裏會作迭代器內部修改次數檢查,由於上面的remove(Object)方法修改了modCount的值,因此纔會報出併發修改異常。要避免這種狀況的出現則在使用迭代器迭代時(顯示或for-each的隱式)不要使用ArrayList的remove,改成用Iterator的remove便可:

public static void remove(List<String> list) 
		{
		Iterator<String> it = list.iterator();
		while (it.hasNext()) 
		{
			String s = it.next();
			if (s.equals("b")) 
			{
				it.remove();
			}
		}
}

部分參考:ArrayList循環遍歷並刪除元素的常見陷阱如何正確遍歷刪除List中的元素,你會嗎?

(3)ArrayList非線程安全,如何解決?

Collections給出瞭解決方案,提供了synchronizedCollection方法,該方法返回一個線程安全容器。

*Java中經常使用的集合框架中的實現類HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap、TreeMap都是線程不安全的,若是有多個線程同時訪問它們,且同時有多個線程修改他們的時候,將會出現如讀髒數據等錯誤。Collections給出瞭解決方案,提供了synchronizedCollection方法來實現線程安全,該方法返回一個線程安全容器。

(4)contains方法contains()方法是經過將傳入的實際參數和集合中已有的元素進行equals()比較來實現的,Object類中的equals()方法比較的是兩個對象的地址,所以須要根據實際須要重寫equals()方法。

1.2 LinkedList、Vector、Stack

ArrayList和LinkedList是List(線性表)的兩種典型實現:基於數組和基於雙向鏈表的線性表。通常而言,因爲數組以一塊連續內存區來保存全部的數組元素,因此數組在隨機訪問時性能最好,全部的內部以數組做爲底層實現的集合在隨機訪問時性能都比較好;而內部以鏈表做爲底層實現的集合在執行插入、刪除操做時有較好的性能。但整體來講,ArrayList的性能要更好,所以大部分使用時都應該考慮使用ArrayList。

  • 若是須要遍歷List集合元素,對於ArrayList和Vector(也是以數組形式存儲集合元素的一種集合類型)集合,應該使用隨機訪問方法(get)來遍歷集合元素;對於LinkedList集合,則應該採用迭代器(Iterator)來遍歷集合元素。
  • 若是須要常常執行插入、刪除操做來改變包含大量數據的List集合的大小,能夠考慮使用LinkedList集合。使用ArrayList和Vector集合可能須要常常從新分配內部數組的大小,效果較差。
  • 若是有多個線程須要同時訪問List集合的元素,開發者能夠考慮使用Collections將集合包裝成線程安全的集合。

Vector是基於synchronized機制實現的線程安全的ArrayList,但在插入元素容量擴充時機制略有不一樣,經過傳入的capacityIncrement來控制容量的擴充。

Stack繼承自Vector,在此基礎上實現了棧的彈出以及壓入和彈出操做,push、pop、peek(只返回,不出棧)

2. Map

鍵值對、鍵惟1、值不惟一

2.1 HashMap

底層實現

JDK7實現,數組+鏈表;JDK8實現,數組+紅黑樹。

可參考:JDK7與JDK8中HashMap的實現

存儲過程

當程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部。流程圖以下所示:

讀取實現

若是 HashMap 的每一個 bucket 裏只有一個 Entry 時,HashMap 能夠根據索引、快速地取出該 bucket 裏的 Entry;在發生「Hash 衝突」的狀況下,單個 bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每一個 Entry,直到找到想搜索的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那系統必須循環到最後才能找到該元素,時間複雜度取決於鏈表的長度,爲 O(n)。爲了下降這部分的開銷,在 Java8 中當鏈表中的元素超過了 8 個且數組容量大於64之後會將鏈表轉換爲紅黑樹(Hollis-HashMap中傻傻分不清楚的那些概念郭霖:面試必問的HashMap,你真的瞭解嗎?,在這些位置進行查找的時候能夠下降時間複雜度爲 O(logN)。

HashMap 在底層將 key-value 當成一個總體進行處理,這個總體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存全部的 key-value 對,當須要存儲一個 Entry 對象時,會根據 Hash 算法來決定其存儲位置;當須要取出一個 Entry 時,也會根據 Hash 算法找到其存儲位置,直接取出該 Entry。

性能選項

實際容量(capacity):大於initial capacity最小的2的n次方,如10<16

初始容量(initial capacity):能夠經過HashMap的構造函數指定

size:變量保存了該 HashMap 中所包含的 key-value 對的數量

threshold:該變量包含了 HashMap 能容納的 key-value 對的極限,它的值等於 HashMap 的容量乘以負載因子(load factor)

負載因子(load factor):決定了Hash表的最大填滿程度

當建立 HashMap 時,有一個默認的負載因子(load factor),其默認值爲 0.75,這是時間和空間成本上一種折衷:增大負載因子能夠減小 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的的操做(HashMap 的 get() 與 put() 方法都要用到查詢);減少負載因子會提升數據查詢的性能,但會增長 Hash 表所佔用的內存空間。掌握了上面知識以後,咱們能夠在建立 HashMap 時根據實際須要適當地調整 load factor 的值;若是程序比較關心空間開銷、內存比較緊張,能夠適當地增長負載因子;若是程序比較關心時間開銷,內存比較寬裕則能夠適當的減小負載因子。一般狀況下,程序員無需改變負載因子的值。

2.2 TreeMap

採用紅黑樹的數據結構來管理key-value對(紅黑樹的每一個節點就是一個key-value對)

存儲key-value對須要根據key排序,排序方式與TreeSet相同(兩種排序方式)。

判斷兩個key相等的標準是:經過compareTo()返回0即認爲這兩個key相等。

對於自定義類做爲key的狀況,最好作到:兩個key經過equals()方法比較返回true時,compareTo()方法也返回0。

兩種排序的比較器:

java.lang.Comparable,TreeMap使用無參構造函數,那麼容納的對象必須實現Comparable接口。

public int compareTo(T o);

java.util.Comparator,TreeSet在構造時使用Comparator做爲構造函數的參數。(兩種排序方式同時存在時,該方式優先權更高)

int compare(T o1, T o2);

2.3 LinkedHashMap

LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序HashMap+LinkedList,即它既使用HashMap操做數據結構,又使用LinkedList維護插入元素的前後順序,經過維護一個運行於全部條目的雙向鏈表,LinkedHashMap保證了元素迭代的順序。該迭代順序能夠是插入順序或者是訪問順序。

能夠經過LinkedHashMap實現LRU算法緩存。構造參數accessOrder爲true則全部的Entry按照訪問的順序排列,爲false則全部的Entry按照插入的順序排列,若是設置爲true,每次訪問都把訪問的那個數據移到雙向隊列的尾部去,那麼每次要淘汰數據的時候,雙向鏈表最頭的那個數據就是要淘汰的數據。

public class LRUCache extends LinkedHashMap
{
    public LRUCache(int maxSize)
    {
        super(maxSize, 0.75F, true);
        maxElements = maxSize;
    }

    protected boolean removeEldestEntry(java.util.Map.Entry eldest)
    {
        return size() > maxElements;
    }

    private static final long serialVersionUID = 1L;
    protected int maxElements;
}

2.4 Hashtable

Hashtable 是遺留類,不少映射的經常使用功能與 HashMap 相似,不一樣的是它承自 Dictionary 類,而且是線程安全的,任一時間只有一個線程能寫 Hashtable,併發性不如 ConcurrentHashMap,由於 ConcurrentHashMap 引入了分段鎖。Hashtable 不建議在新代碼中使用,不須要線程安全的場合能夠用 HashMap 替換,須要線程安全的場合能夠用 ConcurrentHashMap 替換。

3. Set

無序、元素不可重複

3.1 HashSet

HashSet的底層實現用到HashMap,HashSet操做的就是HashMap。(Java源碼就是先實現HashMap、TreeMap,而後包裝一個value爲null的Map集合來實現Set集合的)

存儲過程

兩個對象的equals()方法返回true,可是hashCode值不相等,這時HashSet會把這兩個對象存儲在Hash表的不一樣位置,兩個對象都可以添加成功,但這會與Set集合的規則相沖突。(這就是不重寫相應hashCode()方法的後果,也可閱讀參考:爲何重寫equals方法,必定要重寫HashCode方法?)

因此阿里Java規範這樣寫道:

HashSet如何判斷對象是不是相同

兩個對象經過equals()方法比較相等,而且兩個對象的hashCode方法的返回值也相等。

所以須要注意:

當把一個對象放入HashSet中時,若是須要重寫此對象對應類的equals()方法,則同時須要重寫其hashCode()方法。具體規則是:若是兩個對象經過equals()方法比較返回true時,則它們的hashCode值也應該相等(Object規範:相等的對象必須具備相等的hashcode)。

3.2 TreeSet

底層是TreeMap,集合中的元素處於排序狀態。採用紅黑樹的數據結構來存儲集合元素。TreeSet支持兩種排序方式:天然排序(默認狀況)和定製排序。

當把一個對象添加到TreeSet中時,TreeSet會調用該對象的compareTo(Object obj)方法與容器中的其它對象比較大小,而後根據紅黑樹結構找到它的存儲位置。若是compareTo(Object obj)返回0,意味着兩個對象相同將沒法添加到TreeSet集合中。

3.3 LinkedHashSet

LikedHashSet是HashSet的子類,它也是根據元素的HashCode值進來決定元素的存儲位置,但它可以同時使用鏈表來維護元素的添加次序,使得元素能以插入順序保存。

附:阿里Java規範-集合

List/Set/Map判斷對象相同方式的區別

import java.util.*;

class A {
    @Override
    public int hashCode() {
        return UUID.randomUUID().hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

public class Main {

    public static void main(String[] args) throws Exception {
        List<A> list = new ArrayList<>();
        A a1 = new A();
        A a2 = new A();
        list.add(a1);
        // list contains()方法是經過將傳入的實際參數和集合中已有的元素進行equals()比較來實現的
        System.out.println(list.contains(a2));
        Map<A, Object> map = new HashMap<>();
        map.put(a1, null);
        System.out.println(map.containsKey(a2));
        Set<A> set = new HashSet<>();
        set.add(a1);
        System.out.println(set.contains(a2));
    }
}
// output:
true
false
false

那麼HashSet是如何判斷對象是不是相同的呢?

兩個對象經過equals()方法比較相等,而且兩個對象的hashCode方法的返回值也相等。

所以須要注意:

當把一個對象放入HashSet中時,若是須要重寫此對象對應類的equals()方法,則同時須要重寫其hashCode()方法。具體規則是:若是兩個對象經過equals()方法比較返回true時,則它們的hashCode值也應該相等(Object規範:相等的對象必須具備相等的hashcode)。

Map與Set相似,List稍有不一樣,其contains()方法是經過將傳入的實際參數和集合中已有的元素進行equals()比較來實現的,Object類中的equals()方法比較的是兩個對象的地址,所以須要根據實際須要重寫equals()方法。

相關文章
相關標籤/搜索