本文使用Java 8+
前言
第一章 集合與數組概念
以往想要存儲多個對象,我們想到用StringBuffered、數組來存儲多個對象。但是在寫程序的時候我們並不知道將需要多少個對象,或者是否需要更復雜的方式來存儲對象,結果發展數組尺寸是固定的,這也太過於受限了吧,一點都不靈活,太難了,數組能開放點嗎?
隨後Java團隊爲了解決數組長度不可變,就提供了這麼一個集合(Collection),其中基本的類型有 List 、 Set 、 Queue 和 Map。用Java集合類都可以自動地調整自己的大小。簡直就要飛天了吧。因此,與數組不同是,在編程時,可以將任意數量的對象放置在集合中,而不用關心集合應該有多大。
小結:數組和集合區別
長度區別:數組長度固定;集合的長度可變(靈活易擴展)
存儲內容區別:數組存儲的是同一種類型的元素;集合可以存儲不同類型的元素(但我們開發者考慮安全問題一般使用範型)
存儲數據的類型區別:數組可以存儲基本數據類型,也可以存儲引用類型;集合只能存儲引用類型(如存儲int,它會自動裝箱成Integer)
第二章 集合框架
1、集合的兩大類
集合按照其存儲結構可以分爲兩大類,分別是:
-
單列集合
java.util.Collection
,collection是單列集合類的根接口,它還有兩個子接口分別是List
和set
接口。List
接口的特點:元素有序、元素可重複。(主要實現類有ArrayList、LinkedList、vector)Set
接口的特點:元素無序,且不可重複。(主要實現類有HashSet、TreeSet、LinkedHashSet)
-
雙列集合
java.util.Map
集合本身是一個工具,它存放在java.util包中。
2、集合創建
List<Apple> apples = new ArrayList<>();
List<String> s = new ArrayList<>();
注意: ArrayList 已經被向上轉型爲了 List ,使用接口的目的是,如果想要改變具體實現,只需在創建時修改它就行了,就像下面這樣:
List<Apple> apples = new LinkedList<>();
注意:向上轉型並非總是有效,因爲某些具體類有額外的功能,就比如 LinkedList 具有 List 接口中未包含的額外方法,因此,就不能將它們向上轉型爲更通用的接口。
3、集合爲何需要泛型
使用 Java 5 之前的集合的一個主要問題是編譯器允許你向集合中插入不正確的類型,早期的Java使用Object來代表任意類型的,但是向下轉型有強轉的問題,這樣就不太好了,程序不太安全。
Java泛型設計原則:在編譯期防止將錯誤類型的對象放置到集合中,運行時期就不會出現ClassCastException異常。
細節:在 Java 7 之前,必須要在兩端都進行類型聲明
ArrayList apples = new ArrayList();
泛型小結:有了泛型,代碼就會更加簡潔不用強制轉換、程序更加健壯、可讀性和穩定性。
第三章 List接口
List 接口在 Collection 的基礎上添加了許多方法,允許在 List 的中間插入和刪除元素。
1、List接口主要實現類
- ArrayList:擅長隨機訪問元素,但在 List 中間插入和刪除元素時速度較慢。
- LinkedList:隨機訪問來說相對較慢,但它具有比 ArrayList 更大的特徵集,它通過代價較低的在 List 中間進行的插入和刪除操作,提供了優化的順序訪問。
- vector,老版本,這裏就不多說了,現在都到Java14
2、迭代器
2.1 爲什麼要迭代器
我們都知道集合無非就是存儲
和獲取
的這樣一個過程。畢竟,保存事物是集合最基本的工作,就比如List,插入是add()
獲取是get()
,那麼會發現要使用集合,必須對集合的確切類型編程。突然有一天,如果原本是對 List 編碼的,但是後來發現如果能夠將相同的代碼應用於 Set 會更方便,此時應該怎麼做?想到可能編寫一段通用代碼,但是它是用於不同類型的集合,那麼如何才能不重寫代碼就可以應用於不同類型的集合?這時就使用迭代器。
2.2 什麼是迭代器
迭代器(也是一種設計模式)的概念實現了這種抽象。迭代器是一個對象,它的工作就是遍歷並選擇序列中的每個對象,也就是提供了一種訪問容器對象中的各個元素,並遍歷出來。程序員不需要關心容器底層結構,就可以完美對容器進行遍歷。
2.3 使用迭代器注意事項
(1)使用 iterator()
方法要求集合返回一個 Iterator。 Iterator 將準備好返回序列中的第一個元素。
(2)使用 next()
方法獲得序列中的下一個元素。
(3)使用 hasNext()
方法檢查序列中是否還有元素。
(4)使用 remove()
方法將迭代器最近返回的那個元素刪除。
2.4 迭代器遍歷的三種方法
List<String> list = new ArrayList<>();
list.add("1、kobe");
list.add("2、詹姆斯");
list.add("3、字母哥");
list.add("4、濃眉");
list.add("5、喬丹");
第一種:for循環遍歷(推薦)
for (Iterator it = list.iterator();it.hasNext();) {
System.out.println(it.next());
}
第二種:聲明週期就在大括號內(高併發推薦)
List<String> list = new ArrayList<>();
list.add("1、kobe");
list.add("2、詹姆斯");
{
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
第三種:通過迭代器遍歷
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
3、ListIterator是什麼
ListIterator是繼承自Iterator
ListIterator 是一個更強大的 Iterator 子類型,它只能由各種 List 類生成。 Iterator 只能向前移動,而 ListIterator 可以雙向移動。
ListIterator迭代器包含的方法有:
add(E e): 將指定的元素插入列表,插入位置爲迭代器當前位置之前
hasNext():以正向遍歷列表
hasPrevious():如果以逆向遍歷列表,列表迭代器前面還有元素,則返回 true,否則返回false
next():返回列表中ListIterator指向位置後面的元素
nextIndex():返回列表中ListIterator所需位置後面元素的索引
set(E e):從列表中將next()或previous()返回的最後一個元素返回的最後一個元素更改爲指定元素e
previous():返回列表中ListIterator指向位置前面的元素
previousIndex():返回列表中ListIterator所需位置前面元素的索引
代碼演示:
List<String> list = new ArrayList<>();
list.add("kobe");
list.add("詹姆斯");
list.add("字母哥");
list.add("濃眉");
list.add("喬丹");
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.println(listIterator.next() + "," + listIterator.nextIndex());
}
最終輸出:
kobe, 1
詹姆斯,2
字母哥,3
濃眉, 4
喬丹, 5
第四章 List接口常見面試題
說一下List、Set、Map三者的區別?
* List是有序可重複
* Set是無序不可重複
* Map它是採用鍵值對來存儲,兩個Key可以引用相同的對象,但Key不能重複,典型的Key是String類型,但也可以是任何對象。
Arraylist 與 LinkedList 區別?
1. Arraylist底層使用的是數組;LinkedList底層使用的是雙向鏈表,數據結構(JDK1.6之前爲循環鏈表,JDK1.7取消了循環。(面試官有可能讓你聊聊什麼單鏈表和雙鏈表)
2. Arraylist擅長隨機訪問元素,快速隨機訪問就是通過元素的序號快速獲取元素對象(對應於get(int index)方法)。LinkedList`隨機訪問來說相對較慢,但它具有比ArrayList更大的特徵集。
3. ArrayList採用數組存儲,所以插入和刪除元素的時間複雜度受元素位置的影響。比如:執行add(E e) 方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種情況時間複雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element))時間複雜度就爲 O(n-i)。因爲在進行上述操作的時候集合中第 i 和第 i 個元素之後的(n-i)個元素都要執行向後位/向前移一位的操作。
4. LinkedList採用鏈表存儲,所以對於`add(E e)`方法的插入,刪除元素時間複雜度不受元素位置的影響,近似 O(1),如果是要在指定位置`i`插入和刪除元素的話((add(int index, E element)`) 時間複雜度近似爲`o(n))`因爲需要先移動到指定位置再插入。
補充單鏈表、雙鏈表、雙向循環鏈表
單鏈表:一個節點指向下一個節點(操作鏈表要時刻記住的是:節點中指針域指向的就是一個節點了!)
雙向鏈表:包含兩個指針,一個prev指向前一個節點,一個next指向後一個節點。
雙向循環鏈表:最後一個節點的 next 指向head,而 head 的prev指向最後一個節點,構成一個環。
說一下ArrayList 與 Vector 區別呢?爲什麼要用Arraylist取代Vector呢?
Vector類的所有方法都是同步的。可以由兩個線程安全地訪問一個Vector對象、但是一個線程訪問Vector的話代碼要在同步操作上耗費大量的時間。
Arraylist不是同步的,所以在不需要保證線程安全時建議使用Arraylist。
ArrayList的大小是如何自動增加的?
1、添加元素時,首先進行判斷是否大於默認容量10
2、如果,小於默認容量,直接在原來基礎上+1,元素添加完畢
3、如果,大於默認容量,則需要進行擴容,擴容核心是grow()方法
3.1 擴容之前,首先創建一個新的數組,且舊數組被複制到新的數組中
這樣就得到了一個全新的副本,我們在操作時就不會影響原來數組了
3.2 然後通過位運算符將新的容量更新爲舊容量的1.5陪(原來長度的一半再加上原長度也就是每次擴容是原來的1.5倍)
3.3 如果新的容量-舊的容量<=0,就拿新的容量-最大容量長度如果<=0的,那麼最終容量就是擴容後的容量
什麼情況下你會使用ArrayList?什麼時候你會選擇LinkedList?
1. 當你遇到訪問元素比插入或者是刪除元素更加頻繁的時候,你應該使用ArrayList
2. 插入或者是刪除元素更加頻繁,或者你壓根就不需要訪問元素的時候,你會選擇LinkedList
由上面問題,引入下一個問題:ArrayList插入和刪除效率爲什麼這麼低?
因爲在ArrayList中增加或者刪除某個元素,通常會調用System.arraycopy方法,這是一種極爲消耗資源的操作,因此,在頻繁的插入或者是刪除元素的情況下,LinkedList的性能會更加好一點。
第五章 ArrayList源碼剖析
1、核心屬性源碼:
//ArrayList初始容量爲10
private static final int DEFAULT_CAPACITY = 10;
//用於空實例的共享空數組實例
private static final Object[] EMPTY_ELEMENTDATA = {};
//用於默認大小空實例的共享空數組實例。
//我們把它從EMPTY_ELEMENTDATA數組中區分出來,以知道在添加第一個元素時容量需要增加多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//保存ArrayList數據的數組
transient Object[] elementData;
//該屬性設置容量大小,ArrayList 所包含的元素個數
private int size;
2、構造方法(驗證核心屬性)
//1、構造具有指定初始容量的空列表
public ArrayList(int initialCapacity) {
//如果指定容量大於0,那麼數組就進行初始化成對應的容量
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果初始容量爲0,它會默認給定一個空的數組(EMPTY_ELEMENTDATA)
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//2、構造一個初始容量爲10的空列表。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//3、按照集合的迭代器返回的順序構造一個包含指定集合元素的列表。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
3、ArrayList的擴容機制
//在使用ensureCapacity操作添加大量元素之前,應用程序可以增加ArrayList實例的容量。
//這可能會減少增量重新分配的數量。
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
4、ArrayList擴容的核心方法。
//oldCapacity舊容量,newCapacity新容量
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//將新容量更新爲舊容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
//返回此列表中的元素數。
public int size() { return size; }
5、add方法(重點)
具體實現步驟分爲:首先判斷是否需要擴容、再插入元素
當添加元素時容量長度小於默認容量的長度10時,源碼解析
//1、添加元素,e獲取你添加的元素
public boolean add(E e) {
modCount++;//在之前的元素自增
add(e, elementData, size);
return true;
}
//2、判斷是否需要擴容
private void add(E e, Object[] elementData, int s) {
//判斷s長度是否等於默認容量長度10
//如果大於默認容量10,則走擴容elementData = grow();
//如果小於默認容量10,則走直接添加elementData[s] = e;
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
//小於默認容量長度10,則size等於原來的+1
size = s + 1;
}
//3、最終返回size = s + 1;
public boolean add(E e) {
modCount++;
add(e, elementData, size);
//最終的返回
return true;
}
當容量長度大於默認容量的長度10時,源碼解析
//1、添加元素
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
//2、判斷是否需要擴容
private void add(E e, Object[] elementData, int s) {
//判斷s長度是否等於默認容量長度10
//如果大於默認容量10,則走擴容elementData = grow();
//如果小於默認容量10,則走直接添加elementData[s] = e;
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
//3、默認容量+1
private Object[] grow() {
return grow(size + 1);
}
//4、minCapacity=默認容量size+1
private Object[] grow(int minCapacity) {
//要複製的數組;拷貝的新數組長度
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));//進行擴容了
}
//5、實現擴容
//用來確定是否需要容量,最後調用grow()來擴容
//假設當添加11個元素時,minCapacity=11,而默認數組爲10,此時需要進行擴容
private int newCapacity(int minCapacity) {
// overflow-conscious code
//5.1 舊容量10
//擴容1.5倍
int oldCapacity = elementData.length;
//5.2新的容量=舊容量+(oldCapacity >> 1)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//5.3判斷,如果新的容量-最小容量<=0(也就是擴容後的容量-minCapacity)
//minCapacity(最小容量=默認的容量10+你存入元素得到的長度)
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
//5.4新的容量減去MAX_ARRAY_SIZE <= 0的話,就等於擴容後的長度
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
//6、得到最小容量(就是假如你存入長度爲11,則最終minCapacity=11的意思)
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
//7、newLength擴容後的長度、original原始的
@SuppressWarnings("unchecked")
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
//8、數組的拷貝複製
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//Math.min進行計算
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
//9、a=默認容量10,b=擴容後的容量
public static int min(int a, int b) {
return (a <= b) ? a : b;
}
//10、再回到System.arraycopy(original, 0, copy, 0,
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//複製
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
//最終返回複製所得到的
return copy;
}
//11、複製結束return (T[]) copyOf(original, newLength,original.getClass());
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
//12、grow()方法擴容得到最終的長度
private Object[] grow() {
return grow(size + 1);
}
//13、
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
//13.1 elementData = grow();
elementData = grow();
//13.2 e是存入的元素
elementData[s] = e;
//13.3最終長度爲size = s + 1;
size = s + 1;
}
//14、結束
public boolean add(E e) {
modCount++;
add(e, elementData, size);
//返回
return true;
}
小結:
1、添加元素時,首先進行判斷是否大於默認容量10
2、如果,小於默認容量,直接在原來基礎上+1,元素添加完畢
3、如果,大於默認容量,則需要進行擴容,擴容核心是grow()方法
3.1 擴容之前,首先創建一個新的數組,且舊數組被複制到新的數組中
這樣就得到了一個全新的副本,我們在操作時就不會影響原來數組了
3.2 然後通過位運算符將新的容量更新爲舊容量的1.5陪
3.3 如果新的容量-舊的容量<=0,就拿新的容量-最大容量長度如果<=0的,那麼最終容量就是擴容後的容量
總結:添加(add)方法的實現,添加時首先檢查數組的容量是否滿足
- ArrayList首次擴容,容量爲10,不滿足則擴容到到原來的1.5倍
- 第一次擴容後,容量還是小於minCapacity(就是我們添加的容量如15,則minCapacity=15),就將容量擴容爲minCapacity
- 如果容量足夠就直接添加,不夠就需要進行擴容
- 擴容過程的空間複雜度是O(2n),因爲需要拷貝一個次舊的數組
- ArrayList是基於動態數組實現的,在增刪時候,需要數組的拷貝複製
本篇文章如有錯的地方,歡迎在評論指正。喜歡在微信看技術文章,可以微信搜索「MarkerJava」,回覆【Java】【大數據】【Spring全家桶】【電子書籍】即可獲得精品全套視頻,還有更多資料,建議後臺留言或者直接私信我。
另,如果覺得這本篇文章寫得不錯,有點東西的話,各位人才記得來個三連【點贊+關注+分享】。