1.說說ArrayList
1.基本原理
ArrayList,原理就是底層基於陣列來實現。
01.基本原理:
陣列的長度是固定的,java裡面陣列都是定長陣列,比如陣列大小設定為100,此時你不停的往ArrayList裡面塞入這個資料,此時元素數量超過了100以後,此時就會發生一個陣列的擴容,就會搞一個更大的陣列,把以前的陣列複製到新的陣列裡面去。
這個陣列擴容+元素複製的過程,相對來說會慢一些
02.缺點:
01:不要頻繁的往arralist裡面去塞資料,導致他頻繁的陣列擴容,避免擴容的時候較差的效能影響了系統的執行。
02: 陣列來實現,陣列你要是往陣列的中間加一個元素,是不是要把陣列中那個新增元素後面的元素全部往後面挪動一位,所以說,如果你是往ArrayList中間插入一個元素,或者隨機刪除某個元素,效能比較差,會導致他後面的大量的元素挪動一個位置。
03.優點:
基於陣列來實現,非常適合隨機讀,你可以隨機的去讀陣列中的某個元素。
例如:list.get(10),相當於是在獲取第11個元素,這個隨機讀的效能是比較高的,隨機讀,list.get(2),list.get(20),隨機讀list裡任何一個元素。
因為基於陣列來實現,他在隨機獲取陣列裡的某個元素的時候,效能很高,他可以基於他底層對陣列的實現來快速的隨機讀取到某個元素,直接可以透過記憶體地址來定位某個元素。
04.常用場景:
ArrayList,常用,如果你不會頻繁的在裡面插入一些元素,不會導致頻繁的元素的位置移動、陣列擴容,就是有一批資料,查詢出來,灌入ArrayList中,後面不會頻繁插入元素了,主要就是遍歷這個集合,或者是透過索引隨機讀取某個元素。
如果果你涉及到了頻繁的插入元素到list中的話,儘量還是不要用ArrayList,陣列,定長陣列,長度是固定的,元素大量的移動,陣列的擴容+元素的複製。
05.場景示例:
開發系統的時候,大量的場景,需要一個集合,裡面可以按照順序灌入一些資料,ArrayList的話呢,他的最最主要的功能作用,就是說他裡面的元素是有順序的,我們在系統裡的一些資料,都是需要按照我插入的順序來排列的。
2.原始碼剖析
01.核心方法的剖析
我們們來啟動一個demo工程,在裡面寫寫集合的程式碼,跟進去看看各種集合的實現原理,直接可以看JDK底層的原始碼。
(1).示例程式碼:
public class ArrayListDemo {
public static void main(String[] args) {
ArrayList<String> sayLove =new ArrayList<String>();
sayLove.add("老婆,早上好");
sayLove.add("老婆,下午好");
sayLove.add("老婆,下班啦,我去找你");
sayLove.set(0,"老婆,早上好,我們一起吃早飯吧");
sayLove.add(2,"老婆,注意坐姿,不要久坐哦");
}
}
(2).建構函式分析:
預設的建構函式,直接初始化一個ArrayList例項的話,會將內部的陣列做成一個預設的空陣列,{},Object[],他有一個預設的初始化的陣列的大小的數值,是10,也就是我們可以認為他預設的陣列初始化的大小就是隻有10個元素。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//空陣列
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
private static final int DEFAULT_CAPACITY = 10;//初始容量 10
注意點:
ArrayList的話,玩兒好的話,一般來說,你應該都不是使用這個預設的建構函式,你構造一個ArrayList的話,基本上來說就是預設他裡面不會有太頻繁的插入、移除元素的操作,大體上他裡面有多少元素,你應該可以推測一下的。
基本上最好是給ArrayList構造的時候,給一個比較靠譜的初始化的陣列的大小,比如說,100個資料,1000,10000,避免陣列太小,往裡面塞入資料的時候,導致資料不斷的擴容,不斷的搞新的陣列。
ensureCapacityInternal(size + 1); // Increments modCount!!
你每次往ArrayList中塞入資料的時候,人家都會判斷一下,當前陣列的元素是否塞滿了,如果塞滿的話,此時就會擴容這個陣列,然後將老陣列中的元素複製到新陣列中去,確保說陣列一定是可以承受足夠多的元素的。
(3).add(E)方法
日常多表白,恩愛不懈怠。
分析如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
#2 ensureCapacityInternal(size + 1);
:判斷一下當前的陣列容量是不是滿了,如果滿了會進行擴容;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
1).第一次進入這個方法時,minCapacity=10
,預設值,而此時底層還是個空陣列,自然會進行陣列的擴容。擴容程式碼如下:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
透過計算,確認本次陣列增加的容量就是預設大小10,然後透過Arrays.copyOf
進行陣列的資料複製和建立新陣列。
#3 elementData[size++] = e;
,就是將新增資料資料放到index=0的位置上,並且size+1;
2).然後就返回新增成功了。
陣列的變化:
size=0,elementData={}
變成了size=1,elementData=["老婆,早上好"]
依次執行三次,就完成今天的sayLove日常了。
(4).set(index,E)方法
一起起床是靜好,一起吃早飯是餵飽。
sayLove.set(0,"老婆,早上好,我們一起吃早飯吧");
分析如下:
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
#2 rangeCheck(index);
:還記得我們那些年學Java的青蔥歲月嗎?都得喊一嗓子:角標越界,錯誤就在這裡了,熟悉的異常,值得的青蔥歲月。
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
#3 E oldValue = elementData(index);
:操作很簡單,返回原來位置上的值,將新值插入想插入的位置,並返回舊值。
就像老婆說,你去換下飲水機的水,換上新桶,拿下舊桶。
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
(5).add(index,E)方法
牽掛是一根線,每天想的是可以把你栓在身上。
sayLove.add(2,"老婆,注意坐姿,不要久坐哦");
分析如下:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
#2,#4,
還是熟悉的檢查角標是否越界以及檢查容量是否已滿。
#5,
呼叫native方法System.arraycopy
,進行陣列中元素的移位,引數如下所示:
index=2,size=3;
System.arraycopy(elementData, 新增的角標位=2, elementData, 往後移動的起始角標位=3,
要往後移動的個數=1);
elementeData這個陣列,從第2位開始(第3個元素),複製1個元素,到elementData這個陣列(還是原來的這個陣列),從第3位開始(第4個元素開始)。
#5,#6
,插入新值到指定位置,size+1。
完成指定位置插入資料的操作。
(6).remove(index)方法
程式猿說我下班了,絕對是違背了山盟海誓裡說的,“我絕對不會騙你”。
sayLove.remove(2);//加班,所以,不能去找媳婦了。撤回 “老婆,下班啦,我去找你” 這句話。
分析如下:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
#7 int numMoved = size - index - 1;
:numoved=4-2-1=1,要移動的個數,當前大於0,呼叫native方法,進行陣列元素移動,System.arraycopy(elementData, 3, elementData, 2,1);
,從當前陣列的index=3位置,共一個元素,從index=2位置開始複製。
把elementData陣列中,從index =3開始的元素,一共有1個元素,複製到elementData陣列中(原來的陣列裡),從index = 2開始,進行複製。
說到底,就是刪除位置後的所有元素都往前移位,然後將最後位置上的元素設定為null。so easy。
2.談談LinkedList
1.基本原理
底層是基於連結串列來實現的。
01.基本原理:
LinkedList,連結串列,一個節點掛著另外一個節點。LinkedList是基於雙向連結串列實現的。
02.優點:
往這個裡面中間插入一些元素,或者不停的往list裡插入元素,都沒關係,因為人家是連結串列,中間插入元素,不需要跟ArrayList陣列那樣子,挪動大量的元素的,不需要,人家直接在連結串列里加一個節點就可以了。
如果你不斷的往LinkedList中插入一些元素,大量的插入,就不需要像ArrayList陣列那樣還要去擴容啊什麼的,人家是一個連結串列,就是不斷的把新的節點掛到連結串列上就可以了。
LinkedList的優點,就是非常適合各種元素頻繁的插入裡面去。
03.缺點:
不太適合在隨機的位置,獲取某個隨機的位置的元素,比如LinkedList.get(10),這種操作,效能就很低,因為他需要遍歷這個連結串列,從頭開始遍歷這個連結串列,直到找到index = 10的這個元素為止。
04.常用場景:
適合,頻繁的在list中插入和刪除某個元素,然後尤其是LinkedList他其實是可以當做佇列來用的,這個東西的話呢,我們後面看原始碼的時候,可以來看一下,先進先出,在list尾部懟進去一個元素,從頭部拿出來一個元素。如果要在記憶體裡實現一個基本的佇列的話,可以用LinkedList。
05.場景示例:
系統開發中,凡是用到了記憶體佇列,用LinkedList,他裡面基於連結串列實現,天然就可以做佇列的資料結構,先進先出,連結串列來實現,特別適合頻繁的在裡面插入元素什麼的,也不會導致陣列擴容。
2.原始碼剖析
01.插入元素
在尾部插入元素、在頭部插入元素、在中間插入元素
add(),預設就是在佇列的尾部插入一個元素,在那個雙向連結串列的尾部插入一個元素
add(index, element),是在佇列的中間插入一個元素
addFirst(),在佇列的頭部插入一個元素
addLast(),跟add()方法是一樣的,也是在尾部插入一個元素
(1).示例程式碼:
public static void main(String[] args) {
LinkedList<String> housework=new LinkedList<String>();
housework.add("洗菜");
housework.add("切肉");
housework.add("炒菜");
housework.add(1,"給老婆倒杯水");
}
(2).add(E)方法
家務活要一件件做,幸福要一天天過。
分析如下:
public boolean add(E e) {
linkLast(e);
return true;
}
#2 linkLast(e);
:將元素直接插入到隊尾程式碼如下:
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++;
}
1). #2~#4
: 第一次last肯定是null,建立了一個Node節點,物件結構Node<preNode,元素,next節點>
;並且該節點賦值給了last變數。
2). #5~#8
: 第一次last節點是null,所以first節點也被賦值成建立的node節點。而如果是第二次進行add操作的話,就是會將新的Node節點掛在到前一個Node節點的next節點上去。
3).size+1,操作成功。
(3).add(index,E)方法
老婆的事情永遠可以插隊,優先順序No.1
housework.add(1,"給老婆倒杯水");
分析如下:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
#2 checkPositionIndex(index);
:對於指定位置的插入資料,我們都是需要進行角標是否越界的檢查的;
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
#4-#7
:如果正好是最後一個位置,就直接插入到隊尾;
如果不是,就要進行指定位置的插入,這裡走的是這個分支。
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++;
}
1).先根據index獲取到之前位置的node;
2).#3~#5
: 1. 新建立一個Node,新Node的pre節點設定為原Node節點的pre節點,next節點設定為原node節點。
- 將原來的Node節點的pre節點設定為新Node節點。
2).#6~#9
:如果原節點的pre節點是null,就把新Node節點設定為first節點;
否則就是將原Node節點的next節點設定為新建立Node節點。
3).size+1,然後就返回新增成功了。
繞來繞去,其實就像我準備栓老婆的繩子不夠長了,我就把繩子剪開,然後呢,拿一節新繩子,接上前半截繩子的頭,再接上後半截繩子的,打好倆個結,ok,繩子升級成功。愛情的繩子拉長啦。
02.獲取元素
(1).get(index)方法
經常翻看照片,一張張翻過的是,回憶的幸福,二哈的自己和氣質的媳婦。
示例程式碼:
public static void main(String[] args) {
LinkedList<String> loveStory=new LinkedList<String>();
loveStory.add("看電影");
loveStory.add("去野餐");
loveStory.add("摩天輪");
loveStory.add("去海邊");
loveStory.get(2);
}
我們來分析這一行程式碼:
loveStory.get(2);
分析如下:
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
#2 checkElementIndex;
:角標是否越界的檢查。
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
#3 node(index);
:這裡就極為關鍵了,LinkedList底層是如何遍歷查詢指定位置的元素呢?來看如下分析:
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
#4~#6
:
1).size/2,取了中間數,然後判斷我們要找的位置是在中間數的前部分還是後部分。
如果是前半部分就從first節點開始往後查詢,
如果是後半部分,就從last節點開始往前查詢,有點二分法查詢的感覺。
2).我們這裡index=2, 2>2不成立,走else分支,然後,我們可以看到他透過一個for迴圈進行遍歷,從最後一個節點開始,每次都查詢Node節點的prev節點,直到index+1的位置時,就跳出迴圈了,而index+1節點的prev節點不正是我們要獲取的index節點麼?完美!
03.刪除元素
(1).get(index)方法
既然是二哈屬性,總有惹媳婦生氣的時候,不好的回憶還是適當刪除吧!
示例程式碼:
public static void main(String[] args) {
LinkedList<String> loveStory=new LinkedList<String>();
loveStory.add("看電影");
loveStory.add("去野餐");
loveStory.add("摩天輪");
loveStory.add("惹老婆生氣");
loveStory.add("去海邊");
loveStory.get(2);
}
我們來分析這一行程式碼:
loveStory.remove(2);
分析如下:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
#2 checkElementIndex;
:角標是否越界的檢查。
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
#3 unlink(node(index));
:先遍歷找到index對應的Node節點,然後進行刪除工作。
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;
}
#3~#5
:獲取index對應的Node節點的prev節點和next節點;
#7~#12
:如果prev節點為null,就把Node節點的next節點設定為first節點;
否則就將prev節點的next節點設定為Node節點的next節點。
#14~#19
:如果next節點為null,就把Node節點的prev節點設定為last節點;
否則就將next節點的prev節點設定為Node節點的prev節點。
#21~#24
:將node節點的值設定為null,size-1,最終返回原來Node的元素值。
還記得前文說的愛情繩子理論吧,這裡我們只要是換成縮短繩子,剪開,接上的過程就是刪除指定位置Node的過程。
如果生活有煩惱,就把煩惱給刪掉,把生活接上去繼續幸福。
3).對比總結
(1)ArrayList:一般場景,都是用ArrayList來代表一個集合,不適合頻繁的往裡面插入和灌入大量的元素遍歷,或者隨機查,效能都很好。
(2)LinkedList:適合,頻繁的在list中插入和刪除某個元素,然後尤其是LinkedList他其實是可以當做佇列來用的,先進先出,在list尾部懟進去一個元素,從頭部拿出來一個元素。在記憶體裡實現一個基本的佇列的話,可以用LinkedList。
生活中的美好,從一行程式碼的執行開始,從朝夕相伴的愛情開始。
最後謝謝大家閱讀,有不足之處歡迎指出。