資料結構與集合之(1)ArrayList 與 Arrays
資料結構是指邏輯意義上的資料組織方式及其處理方式。
從 直接前驅 和 直接後繼 個數的維度來看,大體可以將資料結構分為以下四類:
(1)線性結構
0 至 1 個直接前驅 和 直接後繼。線性結構包括 順序表、連結串列、棧、佇列等。
(2)樹結構
0 至 1 個直接前驅 和 0 至 n 個直接後繼(n 大於或等於2 )
(3)圖結構
0 至 n 個直接前驅 和 直接後繼(n 大於或等於 2)
(4)雜湊結構
沒有直接前驅和後繼。雜湊結構通過某種特定的雜湊函式將索引 和 儲存的值關聯起來,它是一種查詢效率非常高的資料結構。
文章目錄
一、ArrayList
1、欄位
private static final long serialVersionUID = 8683452581122892189L;
//預設容量
private static final int DEFAULT_CAPACITY = 10;
//無參構造 或 傳入長度為0 時,是空陣列
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//儲存ArrayList元素的陣列緩衝區。
transient Object[] elementData;
// non-private to simplify nested class access
//陣列長度
private int size;
//最大陣列長度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
2、構造方法
先來看 ArrayList 的構造方法:
(1)有參構造:
 注意:如果傳入了初始容量,是直接按照初始容量構建陣列的。·
public ArrayList(int initialCapacity) {
//值大於0時,根據構造方法的引數值,忠實地建立一個多大的陣列
if (initialCapacity > 0) {
//按傳入初始長度構造陣列
this.elementData = new Object[initialCapacity];
}
else if (initialCapacity == 0) {
//如果傳入長度為0 ,為空陣列
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
(2)無參構造
 注意:如果是無參構造,預設的陣列是空陣列,(我老記得是預設容量是10 。QAQ )
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
//預設是空陣列
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
 如果是無參構造,開始建立的是個空陣列,當呼叫 add方法,開始新增元素時,首先會檢查容量是否夠用,這時才把 預設的容量 10 賦給minCapacity ,然後會呼叫 grow 方法擴容,這時 才擴容成長度為 10 的陣列。
3、擴容方法 grow()
既然是陣列,容量不夠時就需要擴容,ArrayList擴容機制 1.5倍,Vector擴容後機制 2 倍。
private void grow(int minCapacity) {
// overflow-conscious code
//獲取陣列長度賦給 oldCapacity
int oldCapacity = elementData.length;
//新容量=原陣列長度的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量小於傳入的引數要求最小容量
if (newCapacity - minCapacity < 0)
//這給擴的不夠啊,得按傳入的引數擴
newCapacity = minCapacity;
//如果新容量大於陣列能容納的最大元素個數
if (newCapacity - MAX_ARRAY_SIZE > 0)
//那麼再判斷傳入的引數是否大於MAX_ARRAY_SIZE,
//如果傳入的引數大於MAX_ARRAY_SIZE,那麼新容量等於Integer.MAX_VALUE,
//否則等於MAX_ARRAY_SIZE
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
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;
}
說人話:ArrayList 的擴容過程是這樣的,傳入 要求的最小引數 minCapacity ,先獲取當前陣列的長度,以1.5倍擴容,得到新容量 newCapacity,接下來,如果新容量比 要求的最小的還小,那說明擴容得不夠,以要求的最小的為準,即 將要求的最小的容量賦給 新容量。接下來要檢查新容量有沒有超過 允許的陣列的最大容量——int 整型的最大值-8(為什麼要減 8 呢?等會回答。) 如果超過了,可能是因為擴容過頭了,(畢竟是 1.5倍擴容,可能人家傳入的剛好是長度的1.1 呢,本身的oldCapacity不夠,擴了個 1.5 又太多了),那就看看要求的最小的容量是否大於 允許的最大容量,也就是… … -8,如果大於,就把整數最大值 (2^ 31-1)賦給 新容量;如果不大於,說明確實擴容過頭了,把 … .-8賦給新容量。
之前老看網上說 ArrayList 的最大容量是 2^31-8 ,可是看上面的原始碼,擴容時候,如果要求的最小容量 已經比 …-8 大了,那就應該把 newCapacity 賦為整數的最大值(因為用來衡量陣列的長度是用整數),grow 方法的最後一步是按 呼叫 Arrays 的 copyOf,把原來的陣列元素都拷貝到擴容後的新陣列裡去,所以我覺得 ArrayList 的最大容量是 2 ^ 31,來看 MAX_ARRAY_SIZE 欄位上的註釋:
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
有道翻譯:要分配的陣列的最大大小。 有些虛擬機器在一個陣列中保留一些頭 header。試圖分配更大的陣列可能會導致 OutOfMemoryError:請求的陣列大小超過VM限制。所以在不超過 …-8 的前提下,應當是以 … -8 為準的,而超過了,那就只能把 MAX-VALUE 作為陣列的容量啦。
假如需要將 1000 個元素放置在 ArrayList 中,採用預設構造方法,則需要被動擴容 13 次才可以完成儲存。反之,如果在初始化時便指定了容量 new ArrayLIst(100), 那麼在初始化 ArrayList 物件的時候,就直接分配 1000 個儲存空間,從而避免被動擴容 和 陣列複製的額外開銷。
4、add方法
- 在末尾新增
public boolean add(E e) {
//先確保陣列容量是否夠用
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//如果是無參構造建立的空陣列
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//會按 預設容量10 和 要求最小的容量的值 中 最大值,作為容量最小值,
//所以預設的無參構造是在這一步才
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//還是以無參構造為例,這時,minCapacity為10 ,大於 0,需要擴容
if (minCapacity - elementData.length > 0)
//這時才擴容成大小為 10 的陣列
grow(minCapacity);
}
? ArrayList 的 add 方法執行緒不安全舉例
(1)
elementData[size++] = e; 分兩步執行:elementData [size]=e 和 size++。
假設 size =9,當前的長度是預設的容量 10,先檢查陣列容量是否夠用——無需擴容,接下來,CPU 先給執行緒 A ,執行緒A 將 e1 值賦給 elementData[9] ,執行緒A交出 CPU ,執行緒 B 獲取 CPU ,這時 size 仍為 9 ,執行緒B 會給 elementData[9] 賦值 e2,相當於覆蓋了執行緒 A 的值,然後 CPU 給執行緒 A ,size++ 變為10,CPU再給 執行緒 B ,size++ 變為 11,但是 size [10] 的值卻是 null 的。
(2)
假設 size=9,執行緒A ,先檢查陣列容量是否夠用——無需擴容,CPU 給執行緒B ,——也是 無需擴容。接下來 CPU 給執行緒 A,新增元素 ,size變成10 ,A 執行緒結束,CPU 給 B 執行緒,因為已經檢查過容量,所以 B 執行緒就會直接給 elementDate [10] 進行賦值,這時發生 陣列下標越界異常 ArrayIndexOutOfBoundsException 。
- 在指定下標處新增
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++;
}
remove
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);
//將末尾賦為 null,並將 size-1
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
5、ArrayList 的 iterator 刪除元素
Collection 就實現了一個介面 Iterable ,這個介面有以下方法:
//iterator 方法很常用的,它會返回 Iterator(是個介面) 的實現類型別
Iterator<T> iterator();
default void forEach(Consumer<? super T> action)
default Spliterator<T> spliterator()
所以所有的Collection 介面的實現類肯定都覆寫了 iterator 方法,通過迭代輸出的形式刪除內容:
package Java;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TestDemo {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("Hello");
list.add("Hello");
list.add("B");
list.add("Bit");
list.add("Bit");
Iterator<String> iterator=list.iterator();
while (iterator.hasNext())
{
String str=iterator.next();
if(str.equals("B"))
{ iterator.remove();
continue;}
System.out.println(str);}
}
}
注意到,想刪除一個元素,必須先跳過該元素,next 和 remove 方法是互相依賴的。
來看看 next 和 remove 的原始碼:
//ArrayList 有個內部類 Itr 實現了 Iterator 介面
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
//上一個元素的下標
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
public E next() {
//快速失敗機制
checkForComodification();
//指標,需要返回的元素的下標
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//this:在內部類的某個方法中 指定某個巢狀層次的 外圍類的 "this"
//呼叫 ArrayList 的 remove 方法,即把下標以後的元素依次向前移一位,末尾賦 null
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
}
catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
如果先呼叫 remove ,指向前一個元素的指標 lastRet 是指向 -1 的,陣列下標越界,所以必須結合 next() ,先讓 cursor、lastRet 都往後挪一個,然後呼叫 remove 才能刪除 lastRet 也就是這時候的[0],刪除元素後,再把 cursor、lastRet 挪回原位——0,-1。
ArrayList 和 LinkedList 區別
(1).是否保證執行緒安全:
ArrayList 和 LinkedList 都是不同步的,也就是不保證執行緒安全;
(2)底層資料結構:
ArrayList 的底層是 Object陣列,可以實現動態擴容,每次擴容成原來的 1.5 倍,支援隨機訪問,但是刪除或者插入元素效率很低,例如在陣列中間插入(刪除)一個元素,必須把這個元素以後的元素全部向後(前)搬移一個位置。
LinkedList 底層使用的是雙向連結串列資料結構(JDK1.6之前為迴圈連結串列,JDK1.7取消了迴圈。不存在初始容量和擴容的概念。訪問一個元素要遍歷節點,時間複雜度為 O(n)。刪除或者插入一個給定指標指向的結點時間複雜度為 O(1)。但是 LinkedList 的remove(Object o) 方法時間複雜度是 O(n^2) 的,因為要先遍歷找到刪除的節點。
(3)記憶體空間佔用:
ArrayList的空 間浪費主要體現在在 list 列表的結尾會預留一定的容量空間【8】,而LinkedList的空間花費則體現在它的每一個元素都需要消耗比 ArrayList更多的空間(因為要存放直接後繼和直接前驅以及資料)
二、Arrays
Arrays 是針對陣列物件進行操作的工具類,包括陣列的排序、查詢、對比、拷貝等操作。(尤其是排序,在多個 JDK 版本中不斷地進化,比如原來的歸併排序改成 Timsort,明顯地改善了集合的排序效能。)
陣列 與 集合 都是用來儲存物件的容器,前者性質單一,方便易用;後者型別安全,功能強大,且兩者之間必然有互相轉換的方式。 在陣列轉集合的過程中,注意是否使用了檢視方式直接返回陣列中的資料。比如 Arrays.asList() 為例,它把陣列轉換成集合時,不能使用其 修改集合相關的方法,它的 add / remove / clear 方法會丟擲 UnsupportedOperationException 異常。
? 例:
import java.util.Arrays;
import java.util.List;
public class ArrayAsList {
public static void main(String[] args) {
String[] stringArray=new String[3];
stringArray[0]="one";
stringArray[1]="two";
stringArray[2]="three";
List<String> stringList= Arrays.asList(stringArray);
//修改轉換後的集合,將第一個元素“one”改成“oneLIst”
stringList.set(0,"oneList");
System.out.println(stringArray[0]);
stringList.add("four");
stringList.remove(2);
stringList.clear();
}
}
執行結果:
可以通過 set() 方法修改元素的值,原有陣列相應位置的值同時也會被修改,但是不能進行修改元素個數的任何操作,否則 編譯正確,但是均會丟擲 UnsupportedOperationException 異常。 Arrays.asList 體現的是介面卡模式,後臺的資料仍是原有陣列,set() 方法即間接對陣列進行值的修改操作。Arrays.asList 返回物件是一個 Arrays 的內部類,它並沒有實現集合個數的相關修改方法,這也是丟擲異常的原因之一。原始碼如下:
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
返回的明明是 ArrayList 物件,咋就不能隨心所欲對集合進行修改呢? 注意 此 ArrayList 非彼 ArrayList,雖然 Arrays 和 ArrayList 同屬一個包,但是在 Arrays 類中還定義了一個 ArrayList 的內部類,根據作用域就近原則,此處的 ArrayList 是李鬼?,即 這是個內部類。此 李鬼十分簡單,只提供了個別方法的實現:
會丟擲 UnsupportedOperationException 異常 ,是因為它的父類 AbstractList:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
好傢伙,這貨有氣節,它傳遞的資訊是 “要麼直接用我,要麼小心異常!”。陣列轉集合引發故障還是很常見的,比如,某業務呼叫某介面時,對方以這樣的方式返回一個 List 型別的集合物件,本方獲取集合資料時,99.9% 是隻讀操作,但小概率需要增加一個元素,就會引發故障。所以, 在使用陣列轉集合時,需要使用 李逵 java.util.ArrayList 直接建立一個新集合,引數就是 Arrays.asList 返回的不可變集合 ,原始碼如下:
♥
List<Object> objectList=new java.util.ArrayList<Object>(Arrays.asList(陣列));
相比於 陣列 轉 集合,集合 轉 陣列 更加可控,畢竟是從相對自由的集合容器 轉為 更加苛刻的 陣列。比如,適配別人的陣列介面,或者 進行區域性方法計算等,都可能遇到 集合 轉 陣列的情況。
? 例:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class LIstToArray {
public static void main(String[] args) {
List<String> list=new ArrayList<String>(3);
list.add("one");
list.add("two");
list.add("three");
//(1)
Object[] array1=list.toArray();
//(2)
String[] array2=new String[2];
list.toArray(array2);
System.out.println(Arrays.asList(array2));
//(3)
String[] array3=new String[3];
list.toArray(array3);
System.out.println(Arrays.asList(array3));
}
}
執行結果:
執行成功了,從 (1) 可以看出,list.toArray() 返回的是 Object[ ] ,改程式碼後發現,不能用 String[ ] 去接,儘管返回陣列的每個元素都是 String 型別的,但就是不能用 String[ ] 去接,書上說這樣說明泛型丟失 ,不要用 toArray() 無參方法把集合轉換成陣列。來看看 toArray 方法的原始碼,就懂了:
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
關鍵在這,返回的是 Object[ ] 型別的陣列 elementData:
transient Object[] elementData; // non-private to simplify nested class access
(同時也注意到,這個儲存 ArrayList 真實資料的陣列由 transient 修飾,表示此欄位在類的序列化時常被忽略。因為集合序列化時 系統會呼叫 writeObject 寫入流中,在網路客戶端反序列化的 readObject 時,會重新賦值到 新物件的 elementData 中。點解多此一舉? 因為 elementData 容量經常會大於 實際儲存元素的數量,所以只需傳送真正有價值的陣列元素即可。)
@SuppressWarnings("unchecked")
public static <T> T[] copyOf(T[] original, int newLength)
{
return (T[]) copyOf(original, newLength, original.getClass());
}
但是吧,複製值的時候,傳進來的是泛型 U[ ] 陣列,所以 拷貝的原料 original 裡的元素是啥型別,最後結果裡的陣列的元素也就是啥型別(個人理解,如有異議請及時指出):
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
//新建一個陣列 copy
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;
}
從 (2)可以看出,傳入的陣列的容量不夠,輸出值是 null,來看原始碼:
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
最後來用程式碼模擬一下 入引數組容量不夠時、入引數組容量剛好時,以及 入引數組容量超過集合大小時,大概的執行時間,程式碼如下:
import java.util.ArrayList;
import java.util.List;
public class ToArraySpeedTest {
private static final int COUNT=100 * 100 * 100;
public static void main(String[] args) {
List<Double> list=new ArrayList<>(COUNT);
//構造一個 100 萬個元素的測試集合
for(int i=0;i<COUNT;i++)
{list.add(i * 1.0);}
long start=System.nanoTime();
Double[] notEnoughArray=new Double[COUNT-1];
list.toArray(notEnoughArray);
long middle1=System.nanoTime();
Double[] equalArray=new Double[COUNT];
list.toArray(equalArray);
long middle2=System.nanoTime();
Double[] doubleArray=new Double[COUNT * 2];
list.toArray(doubleArray);
long end=System.nanoTime();
long notEnoughArrayTime=middle1-start;
long equalEnoughArrayTime=middle2-middle1;
long doubleArrayTime=end-middle2;
System.out.println("陣列容量小於集合大小:notEnoughArrayTime:"+notEnoughArrayTime/
(1000.0 * 1000.0) + "ms");
System.out.println("陣列容量等於集合大小:equalEnoughArrayTime:"+equalEnoughArrayTime/
(1000.0 * 1000.0) + "ms");
System.out.println("陣列容量大於集合大小:doubleArrayTime:"+doubleArrayTime/
(1000.0 * 1000.0) + "ms");
}
}
執行結果:
陣列容量小於集合大小:notEnoughArrayTime:85.5096ms
陣列容量等於集合大小:equalEnoughArrayTime:7.1272ms
陣列容量大於集合大小:doubleArrayTime:8.7597ms
具體的執行時間,由於 CPU 資源佔用的隨機性,會有一定差異,多次執行結果顯示,當陣列容量等於集合大小時,執行總是最快的,空間消耗也是最少的。由此證明,如果陣列初始大小設定不當,不僅會降低效能,還會浪費空間。使用集合 toArray(T[ ] array) 方法,轉換成陣列時,注意需要傳入型別完全一樣的陣列,並且它的容量大小為 list.size( ) 。
相關文章
- 演算法與資料結構之集合演算法資料結構
- 04 Javascript資料結構與演算法 之 集合JavaScript資料結構演算法
- JavaScript資料結構與演算法——集合JavaScript資料結構演算法
- ArrayList 資料結構分析資料結構
- JavaScript資料結構——集合的實現與應用JavaScript資料結構
- Redis資料結構—整數集合與壓縮列表Redis資料結構
- 資料結構與演算法(1)資料結構演算法
- Redis資料結構之整數集合Redis資料結構
- 資料結構:用例項分析ArrayList與LinkedList的讀寫效能資料結構
- 資料結構與演算法之線性結構資料結構演算法
- 資料結構-集合資料結構
- 資料結構與演算法之美資料結構演算法
- 資料結構與演算法之排序資料結構演算法排序
- Java 集合之ArrayListJava
- Java集合之ArrayListJava
- 集合資料結構總結資料結構
- 演算法與資料結構 1 - 模擬演算法資料結構
- .NET Core 資料結構與演算法 1-1資料結構演算法
- 資料結構與排序資料結構排序
- (python)資料結構—集合Python資料結構
- python之資料結構與演算法分析Python資料結構演算法
- 資料結構與演算法之快速排序資料結構演算法排序
- 《redis設計與實現》1-資料結構與物件篇Redis資料結構物件
- 《JavaScript資料結構與演算法》筆記——第6章 集合JavaScript資料結構演算法筆記
- Java 集合系列1、細思極恐之ArrayListJava
- 資料結構與演算法-資料結構(棧)資料結構演算法
- 資料結構與演算法(1)- 基礎概念資料結構演算法
- 演算法與資料結構之圖的表示與遍歷演算法資料結構
- 【資料結構】ArrayList原理及實現資料結構
- 《資料結構與演算法之美》資料結構與演算法學習書單 (讀後感)資料結構演算法
- Redis資料結構與物件Redis資料結構物件
- chan資料結構與理解資料結構
- 資料結構與演算法知識點總結(1)陣列與連結串列資料結構演算法陣列
- 演算法與資料結構之並查集演算法資料結構並查集
- 演算法與資料結構之原地堆排序演算法資料結構排序
- 05 Javascript資料結構與演算法 之 樹JavaScript資料結構演算法
- 06 Javascript資料結構與演算法 之 圖JavaScript資料結構演算法
- 資料結構與演算法之稀疏陣列資料結構演算法陣列