關於Java中的集合你應該知道的一切
1.集合的分類
我們可以從一張類圖來了解集合整個的情況,圖中虛線框為介面,實線框為類,加重的實線框為比較重要的類。
2.集合相關概念
2.1集合和陣列的區別
集合:集合類存放於java.util包中,集合中存放的是物件的引用,長度可以發生改變,可在多數情況下使用。
陣列:可以儲存有限個型別相同的變數的集合,把具有相同型別的若干元素按無序的形式組織起來的一種形式,陣列的長度是固定的,不適合在元素的數量不確定的情況下使用。
2.2集合中各個集合的簡介
從圖中可以看出集合中重要且常用的集合就那幾種,List、Set、Map是這個集合體系中最主要的三個介面。 List和Set繼承自Collection介面。 Map也屬於集合系統,但和Collection介面不同,他和Collection是依賴關係。
介面名稱 | 用法 | 功能特點 |
---|---|---|
List | ArrayList、LinkedList和Vector是三個主要的實現類,使用時可直接建立ArrayList、LinkedList和Vector的物件。 | 有序且允許元素重複 |
Set | HashSet和TreeSet是兩個主要的實現類,使用方法同上 | Set 只能通過遊標來取值,並且集合中的元素無序但是不能重複的。 |
Map | HashMap、TreeMap和Hashtable是Map的三個主要的實現類,使用方法同上 | Map 是鍵值對集合。其中key列就是一個集合,key不能重複,但是value可以重複 |
實現List介面的實體類說明
1. ArrayList
- ArrayList底層的資料結構
ArrayList的繼承關係
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractList<E>
↳ java.util.ArrayList<E>
public class ArrayList<E> extends AbstractList<E >
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
ArrayList 是一個數組佇列,相當於 動態陣列。與Java中的陣列相比,它的容量能動態增長。它繼承於AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些介面.
RandomAccess:是一個空介面,提供了隨機訪問的功能,當我們的List實現了此介面後,就會為該List提供提供快速訪問功能,我們可以通過元素的序號快速獲得元素物件
Cloneable:實現了Cloneable介面後,即覆蓋了clone這個函式,能被克隆
Serializable:實現了Serializable這些介面後即表示ArrayList支援序列化,能夠通過序列化操作去傳輸。
ArrayList相信大家都使用過,ArrayList中的操作是執行緒不安全的,執行緒不安全就是不提供資料訪問保護,有可能出現多個執行緒先後更改資料造成所得到的資料是髒資料。所以建議在單執行緒中使用ArrayList,多執行緒中我們可以使用Vector。看一下ArrayList的建構函式
/**
initialCapacity是ArrayList的預設容量大小。
初始化elementData陣列大小
*/
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
/**
預設建構函式
*/
public ArrayList() {
super();
//未指定陣列大小,則elementData為空陣列EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
}
/**
建一個包含collection的ArrayList
*/
public ArrayList(Collection<? extends E> c) {
//將collection轉化為陣列,賦值給elementData
elementData = c.toArray();
//將elementData陣列的長度賦值給size
size = elementData.length;
//返回若不是Object[]將呼叫Arrays.copyOf方法將其轉為Object[]
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
從ArrayList的原始碼中可以看出,ArrayList包含了兩個重要的物件:elementData 和 size。也就是說
(1) elementData 是”Object[]型別的陣列”,它儲存了新增到ArrayList中的元素。實際上,elementData是個動態陣列,我們能通過建構函式 ArrayList(int initialCapacity)來執行它的初始容量為initialCapacity;如果通過不含引數的建構函式ArrayList()來建立ArrayList,則elementData預設建立一個空陣列。elementData陣列的大小會根據ArrayList容量的增長而動態的增長,具體的增長方式,請參考原始碼分析中的ensureCapacity()函式。
(2) size 則是動態陣列的實際大小。
也就是說ArrayList的底層資料結構是動態陣列,他是先確定ArrayList的容量,若當前容量不足以容納當前的元素個數時,然後通過Arrays.copyOf()重新建立一個數組,將原來的陣列copy進去,設定新的容量,然後賦值給elementData。
- ArrayList內部主要原始碼分析
首先來看類中的宣告
// 序列版本號
private static final long serialVersionUID = 8683452581122892189L;
//預設集合的長度
private static final int DEFAULT_CAPACITY = 10;
// 預設空陣列
private static final Object[] EMPTY_ELEMENTDATA = {};
// 儲存ArrayList中資料的陣列
transient Object[] elementData;
// ArrayList中實際元素的數量
private int size;
接著看add的方法:
public boolean add(E e) {
// 擴容檢查
ensureCapacityInternal(size + 1);
//新增單個元素
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
//倘若指定的index大於陣列的長度,報出陣列下標越界的異常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
// 擴容檢查
ensureCapacityInternal(size + 1);
//將index位置後面的陣列元素統一後移一位,把index位置空出來
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
//擴容
private void ensureCapacityInternal(int minCapacity) {
//判斷elementData是否為預設空陣列
if (elementData == EMPTY_ELEMENTDATA) {
//倘若elementData為空陣列,找出預設集合的長度和minCapacity中最大的一個賦值給minCapacity
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//判斷是否要擴容
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//記錄修改陣列的次數
modCount++;
//判斷是否需要擴容,並擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//記錄下原來的陣列容量
int oldCapacity = elementData.length;
//獲得新的擴容後的容量:原來的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//當陣列的長度過大後會呼叫hugeCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//拷貝到新的陣列,指定大小,返回後賦值給elementData,完成擴容
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList類中還有很多其他方法,比較簡單,這裡就不贅述了。
- ArrayList的應用場景
型別 | 內部結構 | 順序遍歷速度 | 隨機遍歷速度 | 追加代價 | 插入代價 | 刪除代價 | 佔用記憶體 |
---|---|---|---|---|---|---|---|
ArrayList | 動態陣列 | 高 | 高 | 中 | 高 | 高 | 低 |
所以很明顯,當我們的實際需求中需要查詢或者遍歷的時候,使用ArrayList最好,如果有大量的插入刪除操作儘量避免使用它。
2. LinkedList
- LinkedList底層的資料結構
LinkedList的繼承關係
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractList<E>
↳ java.util.AbstractSequentialList<E>
↳ java.util.LinkedList<E>
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}
從繼承關係上我們可以看到不實現RandomAccess介面,不支援隨機訪問。LinkedList繼承自AbstractSequentialList,這個抽象類實現了最基本的順序訪問功能
Deque介面:可以充當一般的雙端佇列或者棧
自jdk1.7之後,LinkedList底層使用的是不帶頭結點的普通的雙向連結串列,增加了兩個節點指標first和last分別指向首尾節點。如圖:
注意:倘若size=0,則first和last指向同一空元素
- LinkedList內部原始碼分析
/**
* 頭部新增
*/
private void linkFirst(E e) {
//獲取頭結點
final Node<E> f = first;
//新建一個節點,尾部指向之前的頭元素的first
final Node<E> newNode = new Node<>(null, e, f);
//first指向新建的節點
first = newNode;
//如果之前連結串列為null、新建的節點也就是最後一個節點
if (f == null)
last = newNode;
else
//如果不為null,原來的頭節點的頭部指向現在新建的頭節點
f.prev = newNode;
//連結串列的長度++
size++;
//連結串列修改的次數++
modCount++;
}
/**
*尾部新增
*/
void linkLast(E e) {
//獲取尾節點
final Node<E> l = last;
//新建一個節點,頭部指向之前的尾節點last
final Node<E> newNode = new Node<>(l, e, null);
//last指向新建的節點
last = newNode;
//假如之前的last指向null,新建的節點也是頭節點
if (l == null)
first = newNode;
else
//如果不為null,原來的尾節點的尾部指向新建的尾節點
l.next = newNode;
//連結串列的長度++
size++;
//連結串列修改次數++
modCount++;
}
/**
* 在指定節點之前插入某個元素,這裡假定指定節點不為null
*/
void linkBefore(E e, Node<E> succ) {
//獲取指定節點 succ 前面的一個節點
final Node<E> pred = succ.prev;
//新建一個節點,頭部指向succ節點前面的節點,尾部指向succ,資料為e
final Node<E> newNode = new Node<>(pred, e, succ);
//succ的節點頭部指向新建節點
succ.prev = newNode;
//假如succ的前一個節點為null,讓first指向新建的節點
if (pred == null)
first = newNode;
else
//否則讓原先succ前面的節點的尾部指向新建節點
pred.next = newNode;
size++;
modCount++;
}
/**
* 刪除頭結點,返回頭結點上的資料,既定first不為null
*/
private E unlinkFirst(Node<E> f) {
// 獲取頭結點的資料
final E element = f.item;
//獲取頭結點的下一個節點
final Node<E> next = f.next;
//將頭結點置null
f.item = null;
//尾部也指向null
f.next = null; // help GC
//讓first指向頭結點的下一個節點
first = next;
//頭節點後面的節點為 null,說明移除這個節點後,連結串列裡沒節點了
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
/**
* 刪除尾部節點並返回資料,假設不為空
*/
private E unlinkLast(Node<E> l) {
//獲取尾節點的資料
final E element = l.item;
//獲取尾節點的前一個節點
final Node<E> prev = l.prev;
//值置null
l.item = null;
l.prev = null; // help GC
//讓last指向尾節點的前一個節點
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
/**
*刪除某個指定節點
*/
E unlink(Node<E> x) {
//假設 x 不為空
final E element = x.item;
//獲取指定節點前面、後面的節點
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果前面沒有節點,說明 x 是第一個
if (prev == null) {
first = next;
} else {
//前面有節點,讓前面節點跨過 x 直接指向 x 後面的節點
prev.next = next;
x.prev = null;
}
//如果後面沒有節點,說 x 是最後一個節點
if (next == null) {
last = prev;
} else {
//後面有節點,讓後面的節點指向 x 前面的
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
展示一下新增的過程,紅色代表已經做出修改,是無效的
展示一下刪除的過程,紅色代表已經做出修改,是無效的
ListedList還有很多方法,但是都較為簡單,只要理解了上面的幾個重要的方法,其他的你都可以融會貫通。
- LinkedList的應用場景
型別 | 內部結構 | 順序遍歷速度 | 隨機遍歷速度 | 追加代價 | 插入代價 | 刪除代價 | 佔用記憶體 |
---|---|---|---|---|---|---|---|
LikedList | 雙端連結串列 | 中 | 不支援 | 高 | 高 | 高 | 中 |
所以很明顯,ListedList基於雙端連結串列,新增/刪除元素只會影響周圍的兩個節點,開銷很低;只能順序遍歷,無法按照索引獲得元素,因此查詢效率不高;沒有固定容量,不需要擴容;需要更多的記憶體, 每個節點中需要多儲存前後節點的資訊,佔用空間更多些。
注意: linkedList 和 ArrayList 一樣,不是同步容器。所以需要外部做同步操作,或者直接用 Collections.synchronizedList 方法包一下,最好在建立時就包一下:
List l = Collections.synchronizedList(new LinkedList(…));
LinkedList的迭代器都是 fail-fast 的: 如果在併發環境下,其他執行緒使用迭代器以外的方法修改資料,會導致 ConcurrentModificationException異常,所以遍歷是最好使用迭代器進行。
3. Vector
- Vectort底層的資料結構
Vector的繼承關係
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractList<E>
↳ java.util.Vector<E>
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
看過前面的ArrayList和LinkedList之後,相信大家已經對RandomAccess,和Cloneable,還有Serializable介面很熟悉了,這裡不再贅述。
接著來看構造方法和宣告。
//儲存元素的陣列
protected Object[] elementData;
//元素的個數
protected int elementCount;
//擴容增量
protected int capacityIncrement;
//序列化標識
private static final long serialVersionUID = -2767605614048989439L;
//capacityIncrement這個變數,需要在構造器中指定這個值(預設為0,可以手動指定)
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
//手動指定Vector的大小
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
//預設大小為10
public Vector() {
this(10);
}
//初始化一個集合進來
public Vector(Collection<? extends E> c) {
//將集合轉化為陣列賦值給elementData
elementData = c.toArray();
//獲取陣列的長度賦值給elementCount
elementCount = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
通過常量和構造方法我們可以看出Vector的底層也是個動態陣列,並且它的結構程式碼和ArrayList相似。我們這裡主要看擴容和保證執行緒同步的原始碼。
- Vector內部原始碼分析
//為了保證同步,只有一個執行緒操作,方法前面加了synchronized來修飾
public synchronized void ensureCapacity(int minCapacity) {
if (minCapacity > 0) {
//修改陣列的次數
modCount++;
//判斷是否擴容
ensureCapacityHelper(minCapacity);
}
}
/**
*
*/
private void ensureCapacityHelper(int minCapacity) {
// 當傳進來的長度減去原先elementData大於0時,開始擴容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
*
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//擴容的方法
private void grow(int minCapacity) {
// 將elementData原先的長度賦值給oldCapacity
int oldCapacity = elementData.length;
//新的長度等於原先的長度加上(擴容增量>0的話就是擴容增量,否則原先的長度)
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//當陣列的長度過大後會呼叫hugeCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//拷貝到新的陣列,指定大小,返回後賦值給elementData,完成擴容
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
從方法命名上來看,Vector跟ArrayList還是很類似的,但是兩者的grow方法有點小區別:
Vector的擴容是基於capacityIncrement的,也就是所謂的擴容增量,如果該值不為0,那麼每次擴容後的大小就是在原始容量加上擴容增量。如果未設定capacityIncrement,那麼直接擴容為原來的兩倍。
當然,前提是擴容後大小得大於等於所需要的最小容量minCapacity且不能超過MAX_ARRAY_SIZE,同時還要防止溢位(會丟擲異常)
接下來看一下幾個方法
public synchronized void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (elementCount < oldCapacity) {
elementData = Arrays.copyOf(elementData, elementCount);
}
}
//獲得陣列的長度
public synchronized int capacity() {
return elementData.length;
}
//獲得集合的長度
public synchronized int size() {
return elementCount;
}
//判null的方法
public synchronized boolean isEmpty() {
return elementCount == 0;
}
可以發現,這些方法都加上了synchronized關鍵字,也就是說Vector是一個執行緒安全的類。
Vector除了ListIterator和iterator兩種迭代方式之外,還有獨特的迭代方式,那就是elements方法,這個方法通過匿名內部類的方式構造一個Enumeration物件,並實現了hasMoreElements和nextElement方法,類似迭代器的hasNext和next方法
public Enumeration<E> elements() {
//匿名內部類方式構造一個Enumeration物件
return new Enumeration<E>() {
int count = 0;
public boolean hasMoreElements() {
return count < elementCount;
}
public E nextElement() {
synchronized (Vector.this) {
if (count < elementCount) {
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
};
}
- Vectort的應用場景
Vector在實際的開發中使用較少,Vector所有方法都是同步,有效能損失。並且Vector會在你不需要進行執行緒安全的時候,強制給你加鎖,導致了額外開銷,所以慢慢被棄用了。
- #### 實現Set介面的實現類說明
1. HashSet
- HashSet 底層的資料結構
首先來看HashSet的繼承關係
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractSet<E>
↳ java.util.HashSet<E>
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
可以看到HashSet實現了Set介面和Cloneable,Serializable介面,關於Cloneable,Serializable介面不再多說,這裡HashSet繼承AbstactSet這個中間抽象類,並且這個抽象類又繼承自AbstractCollection,AbstractCollection其實更像是實現List,Set的共同的方法,而AbstactSet和AbstactList更像是提供給Set、List各自特有方法的實現。接著來看:
//序列化標識
static final long serialVersionUID = -5024744406713321676L;
//底層使用HashMap來儲存HashSet中所有元素。
private transient HashMap<E,Object> map;
//定義一個虛擬的Object物件作為HashMap的value
private static final Object PRESENT = new Object();
可以看到HashSet的底層實現是基於HasMap的,它不保證set 的迭代順序,特別是它不保證該順序恆久不變。且允許使用null元素,HashSet的實現較為的簡單,其相關的操作都是通過直接呼叫底層HashMap的相關方法來完成
-
HashSet 內部原始碼分析
-
1.建構函式
/*
預設建構函式,實際底層會初始化一個空的HashMap,並使用預設初始容量為16和載入因子0.75。
*/
public HashSet() {
map = new HashMap<>();
}
/**
構造一個包含指定collection中的元素的新set。
* 實際底層使用預設的載入因子0.75和足以包含指定
* collection中所有元素的初始容量來建立一個HashMap。
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
/**
以指定的initialCapacity和loadFactor構造一個空的HashSet。
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
/**
以指定的initialCapacity構造一個空的HashSet。
*/
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
/**
以指定的initialCapacity和loadFactor構造一個新的空連結雜湊集合。
此建構函式為包訪問許可權,不對外公開,實際只是是對LinkedHashSet的支援
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
- 從建構函式中可以看到,基本都是為了構造一個HashMap來儲存資料
- 2.常用方法
//如果set中尚未包含指定元素,則呼叫map的put方法,其中value是一個靜態的Object物件
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//如果指定元素存在於此 set 中,則將其移除
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
//從此 set 中移除所有元素
public void clear() {
map.clear();
}
//判斷set中是否含有指定元素,如果有,返回true
public boolean contains(Object o) {
return map.containsKey(o);
}
//實際呼叫HashMap的clone()方法,獲取HashMap的淺表副本,並設定到 HashSet中
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
}
//返回對此set中元素進行迭代的迭代器。返回元素的順序並不是特定的
public Iterator<E> iterator() {
return map.keySet().iterator();
}
- 可見HashSet中的元素,只是存放在了底層HashMap的key上, value使用一個static final的Object物件標識。
- 3.保證儲存物件的唯一性
Set是一個不包含重複物件的集合,且最多隻有null元素,如何保證其唯一且不重複呢,看一下建構函式的addAll(c); 追原始碼發現最後進入AbstractCollection中addAll中
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
從上面程式碼中可以看到,原始碼中也是通過迴圈一個一個add進去的,那我們看一下HashSet的add方法。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
發現呼叫了map的put方法,進入HashMap的put方法看看:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
for (HashMapEntry<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;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
可以看到for迴圈中,遍歷table中的元素,如果hash碼值不相同,說明是一個新元素,存;如果沒有元素和傳入物件(也就是add的元素)的hash值相等,那麼就認為這個元素在table中不存在,將其新增進table;如果hash碼值相同,且equles判斷相等,說明元素已經存在,不存;如果hash碼值相同,且equles判斷不相等,說明元素不存在,存;如果有元素和傳入物件的hash值相等,那麼,繼續進行equles()判斷,如果仍然相等,那麼就認為傳入元素已經存在,不再新增,結束,否則仍然新增;
從上面我們可以看到通過雜湊演算法,對key產生雜湊碼,通過雜湊碼和equals方法保證其唯一,也就是說,要想保證物件在Set中的唯一,需要重寫hashCode和equals方法。
- HashSet 小結
HashSet是Set介面典型實現,它按照Hash演算法來儲存集合中的元素,具有很好的存取和查詢效能。且主要具有以下特點:
(1)不保證set的迭代順序
(2)HashSet不是同步的,如果多個執行緒同時訪問一個HashSet,要通過程式碼來保證其同步
(3)集合元素值可以是null,且只能有一個
(4)當向HashSet集合中存入一個元素時,HashSet會呼叫該物件的hashCode()方法來得到該物件的hashCode值,然後根據該值確定物件在HashSet中的儲存位置
(5)在Hash集合中,不能同時存放兩個相等的元素,而判斷兩個元素相等的標準是兩個物件通過equals方法比較相等並且兩個物件的HashCode方法返回值也相等。
2. TreeSet
- TreeSet底層的資料結構
首先來看TreeSet的繼承關係
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractSet<E>
↳ java.util.TreeSet<E>
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
TreeSet繼承自AbstractSet,所以他是一個Set的集合,具有Set的屬性和方法
TreeSet實現了NavigaableSet介面,意味著它支援一系列的導航方法,比如查詢與指定目標最匹配項
//NavigableMap物件,TreeMap實現了NavigableMap介面
private transient NavigableMap<E,Object> m;
//靜態的PRESENT物件,代表TreeMap中的value
private static final Object PRESENT = new Object();
從上述繼承關係以及常量宣告中看到,TreeSet的底層是基於TreeMap的key來儲存的,而Value值全部為預設值PRESENT。
- TreeSet內部原始碼分析
首先來看TreeSet的建構函式
//內部私有建構函式不對外公開,初始化NavigableMap物件m
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
/**
預設建構函式,建立空的TreeMap的物件
*/
public TreeSet() {
this(new TreeMap<E,Object>());
}
/**
帶比較器的建構函式。
*/
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
/**
建立TreeSet
*/
public TreeSet(Collection<? extends E> c) {
// 呼叫預設構造器建立一個TreeSet,底層以 TreeMap 儲存集合元素
this();
//將集合c中的全部元素都新增到TreeSet中
addAll(c);
}
/**
建立TreeSet,並將s中的全部元素都新增到TreeSet中
*/
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
從原始碼中也可以看到TreeSet中的其他方法也比較簡單,和HashSet中的有點類似,我們主要來看重點的方法:
public boolean addAll(Collection<? extends E> c) {
// 判斷是否傳入的集合引數c是否為SortedSet或其子類且c不為空(c.size()>0),
//如果是則會呼叫addAllForTreeSet方法,否則會直接返回addAll方法的結果
if (m.size()==0 && c.size() > 0 &&
c instanceof SortedSet &&
m instanceof TreeMap) {
SortedSet<? extends E> set = (SortedSet<? extends E>) c;
TreeMap<E,Object> map = (TreeMap<E, Object>) m;
Comparator<?> cc = set.comparator();
Comparator<? super E> mc = map.comparator();
if (cc==mc || (cc != null && cc.equals(mc))) {
map.addAllForTreeSet(set, PRESENT);
return true;
}
}
return super.addAll(c);
}
//從TreeMap中找到了該方法
void addAllForTreeSet(SortedSet<? extends K> set, V defaultVal) {
try {
buildFromSorted(set.size(), set.iterator(), null, defaultVal);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
//該方法的作用即是線上性時間內對資料進行排序
private void buildFromSorted(int size, Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
this.size = size;
root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
it, str, defaultVal);
}
當使用一個TreeMap集合作為引數構造一個TreeSet的時候,TreeSet會將Map中的元素先排序,然後將排序後的元素add到TreeSet中。也就是說TreeSet中的元素都是排過序的,另外正因為存在排序過程,所以TreeSet不允許插入null值,因為null值不能排序
- TreeSet小結
1、TreeSet不能有重複的元素;
2、TreeSet具有排序功能;
3、TreeSet中的元素必須實現Comparable介面並重寫compareTo()方法,TreeSet判斷元素是否重複 、以及確定元素的順序 靠的都是這個方法;
對於java類庫中定義的類,TreeSet可以直接對其進行儲存,如String,Integer等,因為這些類已經實現了Comparable介面);
對於自定義類,如果不做適當的處理,TreeSet中只能儲存一個該型別的物件例項,否則無法判斷是否重複。
4、TreeSet依賴TreeMap。
5、TreeSet相對HashSet,TreeSet的優勢是有序,劣勢是相對讀取慢。根據不同的場景選擇不同的集合。
3. LinkedHashSet
- LinkedHashSet底層的資料結構
LinkedHashSet集合同樣是根據元素的hashCode值來決定元素的儲存位置,但是它同時使用連結串列維護元素的次序。這樣使得元素看起 來像是以插入順 序儲存的,也就是說,當遍歷該集合時候,LinkedHashSet將會以元素的新增順序訪問集合的元素。LinkedHashSet在迭代訪問Set中的全部元素時,效能比HashSet好,但是插入時效能稍微遜色於HashSet。
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractSet<E>
↳ java.util.HashSet<E>
↳ java.util.LinkedHashSet<E>
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
可以看到LinkedHashSet繼承自HashSet,那麼HashSet的屬性和方法他都有,他比較簡單,我們主要看一下他的建構函式
- LinkedHashSet內部原始碼分析
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
從上面的建構函式看到,都是呼叫HashSet的構造器,而HashSet中有一個重要的方法:
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
也就是說在父類 HashSet 中,專為 LinkedHashSet 提供的構造方法如下,該方法為包訪問許可權,並未對外公開。由上述原始碼可見,LinkedHashSet 通過繼承HashSet,底層使用LinkedHashMap,以很簡單明瞭的方式來實現了其自身的所有功能。
實現Map介面的實現類說明
1. TreeMap
- TreeMap底層的資料結構
首先來看TreeMap的繼承關係
java.lang.Object
↳ java.util.AbstractMap<E>
↳ java.util.AbstractMap<E>
↳ java.util.TreeMap<E>
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
- 可以看到
- TreeMap繼承自AbstractMap,所以它是一個Map,即是一個key-value的集合
- TreeMap實現了NavigableMap,表示其支援一系列的導航方法,比如返回有序的key集合
- TreeMap實現了Cloneable和Serializable介面,即表示它能被克隆也支援序列化
//比較器,通過comparator介面我們可以對TreeMap的內部排序進行精密的控制
private final Comparator<? super K> comparator;
//TreeMap紅-黑節點,為TreeMap的內部類
private transient TreeMapEntry<K,V> root = null;
/**
* 樹中的條目數
*/
private transient int size = 0;
/**
*對樹進行結構修改的次數
*/
private transient int modCount = 0;
//TreeMap的靜態內部類,“紅黑樹的節點”對應的類。
static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
K key;//鍵
V value;//值
TreeMapEntry<K,V> left =