Java集合-Collection
一、Collection繼承關係
由上圖可知Collection有三個子類,分別是Set、List、Queue。
特點:
Set:無序且值唯一
List:有序、值可重複
Queue:先進先出的線性表
二、Collection提供的方法
Collection提供了對集合的通用操作
三、Collection子類
1、Set
無序且值唯一。
Set子類有:
HashSet
底層資料結構是雜湊表(實際是hashMap),從建構函式可以看出在建立例項時會建立一個HashMap,該HashMap就是用來實際儲存元素的,除此之外在建立HashSet例項時我們可以指定其內部HashMap的容量和載入因子(預設大小為16,載入因子為0.75)
public HashSet() {
map = new HashMap<>();
}
再來看下增刪查資料是如何實現的:
- add操作
public boolean add(E e) {
//add是呼叫HashMap的put操作
return map.put(e, PRESENT)==null;
}
- remove操作
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
- contains操作
public boolean contains(Object o) {
return map.containsKey(o);
}
HashSet如何來保證元素唯一性? 1.依賴兩個方法:hashCode()和equals()。
TreeSet
TreeSet是一個非同步的非執行緒安全的二叉樹,底層資料結構是紅黑樹。(唯一,排序),其add , remove和contains操作的時間複雜度為log(n)
來看下預設建構函式:
public TreeSet() {
this(new TreeMap<E,Object>());
}
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
可以看出其內部預設是使用TreeMap儲存元素的,因為其內部元素是有序的,對於元素的排序有兩種方式自然排序和比較器排序,自然排序就是當comparator為空的時候,構建無參建構函式的時候預設的一種排序方式,比較器排序就是在建構函式中傳入comparator從而指定排序方式。
treeSet = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
});
TreeSet保證元素唯一性的是通過比較的返回值是否是0來決定
LinkedHashSet
Set介面的雜湊表和連結列表實現即保證插入順序,(FIFO插入有序,唯一)由連結串列保證元素有序由雜湊表保證元素唯一。linkedHashSet是一個非執行緒安全的集合。如果有多個執行緒同時訪問當前linkedhashset集合容器,並且有一個執行緒對當前容器中的元素做了修改,那麼必須要在外部實現同步
來看下其建構函式
public LinkedHashSet() {
super(16, .75f, true);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
LinkedHashSet父類為HashSet,然後在HashSet的建構函式中建立了LinkedHashMap例項,也就是說LinkedHashSet最終是使用LinkedHashMap來儲存元素。
Set小結
我們簡紹了三種Set在實際使用時可以根據需求選擇合適的,同時我們也看到這三種Set的實現最終都是通過Map來儲存元素的。
2、List
List連結串列是一種線性結構,其內部元素有序(插入有序)、不唯一,可以根據索引來查詢獲取資料。
ArrayList
底層通過陣列實現,查詢快增刪慢,執行緒不安全。來看下建構函式
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
可以看到儲存元素的是一個叫做elementData的陣列。
- add操作
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
我們看到在增加元素前會先呼叫 ensureCapacityInternal來確保陣列elementData有足夠的空間,如果空間不足會進行擴容操作。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//判斷是否需要擴容
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//list的size增加
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;
//MAX_ARRAY_SIZE的值為Integer.MAX_VALUE - 8表示最大可設定的值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 真正擴容操作是通過Arrays.copyOf來完成的
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 溢位
throw new OutOfMemoryError();
//所需最小容量大於MAX_ARRAY_SIZE則擴容為Integer.MAX_VALUE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
再來看下在指定位置插入元素的操作
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//檢查是否需要擴容
ensureCapacityInternal(size + 1);
//把插入位置後面所有元素後移一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//插入元素
elementData[index] = element;
size++;
}
- remove操作
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;
}
可以看到remove中是根據equals來判斷元素是否是要刪除的,具體移除操作是通過fastRemove來完成。
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; // clear to let GC do its work
}
總體來說ArrayList底層採用陣列儲存元素在元素增刪時通過copy陣列來實現元素移動,其增刪操作的時間複雜度為O(n)。
Vector
底層陣列實現,查詢快增刪慢,執行緒安全。建構函式
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
//這裡的capacityIncrement是指擴容時增加的容量
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底層也是陣列實現,所以在增刪資料時會涉及到陣列容量的變化,這跟ArrayList類似下面是Vector擴容的核心內容,可以看出其在容量不足時會增加capacityIncrement的容量,如果capacityIncrement<0則直接增加一倍的容量。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
Vector實現跟ArrayList類似最大的不同在於Vector是執行緒安全的。
stack
先進後出的結構,stack中peek函式是檢視棧頂元素但並不移除,pop是彈出棧頂元素。
其建構函式是空實現
public Stack() {
}
- push操作
public E push(E item) {
addElement(item);
return item;
}
public synchronized void addElement(E obj) {
modCount++;
//檢查是否需要擴容
ensureCapacityHelper(elementCount + 1);
//存入資料
elementData[elementCount++] = obj;
}
- pop操作
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized void removeElementAt(int index) {
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
//移動資料
System.arraycopy(elementData, index + 1, elementData, index, j);
}
elementCount--;
//將刪除位置置空
elementData[elementCount] = null; /* to let gc do its work */
}
- peek操作
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
public synchronized E elementAt(int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
}
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
LinkedList
底層雙連結串列實現,查詢慢增刪快,執行緒不安全,LinkedList同時實現了List, Deque兩個介面也就是說它既可以作為list也可作為deque使用。
既然是雙連結串列則會有節點的概念,我們來看下它的Node,這是LinkedList的一個內部類。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
- add操作
public boolean add(E e) {
linkLast(e);
return true;
}
//在表尾插入一個Node
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
- add(index,obj)
public void add(int index, E element) {
//檢查插入位置是否合法
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//插入連結串列
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
-
remove操作
public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { //先查詢到要刪除節點 for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { //移除節點 unlink(x); return true; } } } return false; } E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; //修改前驅指標 if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } //修改後繼指標 if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
可以看出LinkedList的資料操作大多都是連結串列的操作所以其特點是增刪快查詢慢,在類內部LinkedList維護了first和last兩個指標,這也是其能實現deque功能的基礎。在作為deque時offer表示在隊尾入隊一個元素,poll是出隊隊首一個元素,peek是檢視隊首元素但並不出隊。在作為deque時無法呼叫list相關介面方法。
3、Queue
佇列是一種先進先出的線性結構,不支援隨機訪問資料。
PriorityQueue
優先佇列是基於堆實現的,對內元素是有序的,offer,poll,remove和add等方法提供了O(log(n))的時間複雜度 ,而remove(obj)和contains方法的時間複雜度是線性時間,peek時間複雜度為O(1)。排序是通過自然排序和比較器排序實現的,採用哪種排序是通過建構函式確定的,其中自然排序要求元素實現compare函式,比較排序則需要在建構函式中指明排序規則。
預設建構函式
private static final int DEFAULT_INITIAL_CAPACITY = 11;
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
//可以看出內部採用陣列儲存
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
- offer
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
//擴容
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
//入隊
siftUp(i, e);
return true;
}
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
//這裡以分析siftUpComparable為例
siftUpComparable(k, x);
}
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
//找k位置的父節點的index
int parent = (k - 1) >>> 1;
//k位置的父節點
Object e = queue[parent];
//調整堆,大於父節點的就不動,小於父節點的就上浮
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
- poll操作
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
//調整堆
siftDown(0, x);
return result;
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
ArrayDeque
雙端佇列,底層陣列實現。
預設建構函式
public ArrayDeque() {
//陣列大小預設16
elements = new Object[16];
}
因為可以雙端運算元據所以其內部採用head和tail來儲存頭尾元素的index這樣就可以快鎖找到頭尾元素。ArrayDeque還規定elements的size必須是2的整數次冪,當我們設定容量大小不是2的整數次冪時會進行調整
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
private void allocateElements(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
if (numElements >= initialCapacity) {
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1; // Good luck allocating 2^30 elements
}
elements = new Object[initialCapacity];
}
allocateElements實現思路如下:
1.要明確2整數次冪使用二進位制的表現形式如下:0...010...0,中間有一個1,其它的都是0。
2.根據1的形式,計算使輸入任意的X,等式成立的Y。X的二進位制形式為????????,是一個未知數,這樣如何求得Y呢?方法很簡單,找到X最高位為1的位置:那麼X就是0..001???,這種形式了。那麼所求的Y就是0..010...0,其值就是比X最高位為1再高一位為1,其它位為0的值。
3.X的最高為1的那一位是未知的,如何求更高一位為1的Y呢?直接求是沒有辦法的,但是可以通過將X最高位為1後面所有位都變成1,再加1進位的方式辦到。就是0..001???變成0.001..1,使用這個+1就會變成所要的Y:0.010...0了。
4.如何保證X最高位為1後面都是1呢?這個就是上面位運算所實現的內容了。假設X是0..01???,左移一位就是0.001??,做或運算就變成了0..011??,是不是很巧妙,出現了兩位為1的就移動2位,獲得四位為1的值,這樣移動到16的時候就涵蓋了32位整數的所有範圍了。這個時候+1可能發生整數溢位,所以再左移一位保證在整數範圍內。
- addFirst
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
//(head - 1) & (elements.length - 1)的作用是確定head的index
elements[head = (head - 1) & (elements.length - 1)] = e;
//首尾指向同一位置 擴容至原先兩倍大小
if (head == tail)
doubleCapacity();
}
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // number of elements to the right of p
int newCapacity = n << 1;
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r);
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}
- pollFirst
public E pollFirst() {
final Object[] elements = this.elements;
final int h = head;
@SuppressWarnings("unchecked")
E result = (E) elements[h];
// Element is null if deque empty
if (result != null) {
elements[h] = null; // Must null out slot
head = (h + 1) & (elements.length - 1);
}
return result;
}