前言
什么是線性表?
跟著我往下看(這里只講順序存儲方式的線性表):
不明白?繼續往下看
下面開始進入正題:
ArrayList就是使用順序結構線性表,分析學習的最好例子,繼承了AbstractList,實現了List。ArrayList在工作中經常用到,所以要弄懂這個類是極其重要的。
構造圖如下:
藍色線條:繼承
綠色線條:接口實現
正文
ArrayList簡介
ArrayList定義
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList 是一個數組隊列,相當于 動態數組。與Java中的數組相比,它的容量能動態增長。它繼承于AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些接口。
ArrayList 繼承了AbstractList,實現了List。它是一個數組隊列,提供了相關的添加、刪除、修改、遍歷等功能。
ArrayList 實現了RandmoAccess接口,即提供了隨機訪問功能。RandmoAccess是java中用來被List實現,為List提供快速訪問功能的。在ArrayList中,我們即可以通過元素的序號快速獲取元素對象;這就是快速隨機訪問。稍后,我們會比較List的“快速隨機訪問”和“通過Iterator迭代器訪問”的效率。
ArrayList 實現了Cloneable接口,即覆蓋了函數clone(),能被克隆。
ArrayList 實現java.io.Serializable接口,這意味著ArrayList支持序列化,能通過序列化去傳輸。
和Vector不同,ArrayList中的操作不是線程安全的!所以,建議在單線程中才使用ArrayList,而在多線程中可以選擇Vector或者CopyOnWriteArrayList。
ArrayList屬性
顧名思義哈,ArrayList就是用數組實現的List容器,既然是用數組實現,當然底層用數組來保存數據啦
/**
*保存ArrayList中數據的數組
*/
private transient Object[] elementData;
/**
*ArrayList中實際數據的數量
*/
private int size;
/**
* 用于空實例的共享空數組實例,就是默認空構造中使用,默認使elementData = EMPTY_ELEMENTDATA
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 默認初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
ArrayList包含了兩個重要的對象:elementData 和 size。
(1) elementData 是"Object[]類型的數組",它保存了添加到ArrayList中的元素。實際上,elementData是個動態數組,我們能通過構造函數 ArrayList(int initialCapacity)來執行它的初始容量為initialCapacity;如果通過不含參數的構造函數ArrayList()來創建ArrayList,則elementData的容量默認是10。elementData數組的大小會根據ArrayList容量的增長而動態的增長,具體的增長方式,請參考源碼分析中的ensureCapacity()函數。
(2) size 則是動態數組的實際大小。
ArrayList構造函數
// ArrayList帶容量大小的構造函數。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
// 新建一個數組
this.elementData = new Object[initialCapacity];
}
/**
*舊版,ArrayList無參構造函數。默認容量是10。
** /
public ArrayList() {
this(10);
}
/**
*新版sdk中,會讓初始容量為空,
*在擴容時判斷elementData = EMPTY_ELEMENTDATA,為空時再動
*態增加容量
*/
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
//即下面這個方法,后面會提到這個方法
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
......
}
// 構造一個包含指定元素的list,這些元素的是按照Collection的迭代器返回的順序排列的
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
- 第一個構造方法使用提供的initialCapacity來初始化elementData數組的大小。
- 第二個構造方法調用第一個構造方法并傳入參數10,即默認elementData數組的大小為10。
- 第三個構造方法則將提供的集合轉成數組返回給elementData(返回若不是Object[]將調用Arrays.copyOf方法將其轉為Object[])。
API方法摘要
ArrayList源碼解析(基于JDK1.6.0_45)
增加
/**
* 添加一個元素
*/
public boolean add(E e) {
// 進行擴容檢查
ensureCapacity( size + 1); // Increments modCount
// 將e增加至list的數據尾部,容量+1
elementData[size ++] = e;
return true;
}
/**
* 在指定位置添加一個元素
*/
public void add(int index, E element) {
// 判斷索引是否越界,這里會拋出多么熟悉的異常。。。
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: " +size);
// 進行擴容檢查
ensureCapacity( size+1); // Increments modCount
// 對數組進行復制處理,目的就是空出index的位置插入element,并將index后的元素位移一個位置
System. arraycopy(elementData, index, elementData, index + 1,
size - index);
// 將指定的index位置賦值為element
elementData[index] = element;
// list容量+1
size++;
}
/**
* 增加一個集合元素
*/
public boolean addAll(Collection<? extends E> c) {
//將c轉換為數組
Object[] a = c.toArray();
int numNew = a.length ;
//擴容檢查
ensureCapacity( size + numNew); // Increments modCount
//將c添加至list的數據尾部
System. arraycopy(a, 0, elementData, size, numNew);
//更新當前容器大小
size += numNew;
return numNew != 0;
}
/**
* 在指定位置,增加一個集合元素
*/
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length ;
ensureCapacity( size + numNew); // Increments modCount
// 計算需要移動的長度(index之后的元素個數)
int numMoved = size - index;
// 數組復制,空出第index到index+numNum的位置,即將數組index后的元素向右移動numNum個位置
if (numMoved > 0)
System. arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 將要插入的集合元素復制到數組空出的位置中
System. arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
/**
* 數組容量檢查,不夠時則進行擴容
*/
public void ensureCapacity( int minCapacity) {
modCount++;
// 當前數組的長度
int oldCapacity = elementData .length;
// 最小需要的容量大于當前數組的長度則進行擴容
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
// 新擴容的數組長度為舊容量的1.5倍+1
int newCapacity = (oldCapacity * 3)/2 + 1;
// 如果新擴容的數組長度還是比最小需要的容量小,則以最小需要的容量為長度進行擴容
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
// 進行數據拷貝,Arrays.copyOf底層實現是System.arrayCopy()
elementData = Arrays.copyOf( elementData, newCapacity);
}
}
刪除
/**
* 根據索引位置刪除元素
*/
public E remove( int index) {
// 數組越界檢查
RangeCheck(index);
modCount++;
// 取出要刪除位置的元素,供返回使用
E oldValue = (E) elementData[index];
// 計算數組要復制的數量
int numMoved = size - index - 1;
// 數組復制,就是將index之后的元素往前移動一個位置
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
// 將數組最后一個元素置空(因為刪除了一個元素,然后index后面的元素都向前移動了,所以最后一個就沒用了),好讓gc盡快回收
// 不要忘了size減一
elementData[--size ] = null; // Let gc do its work
return oldValue;
}
/**
* 根據元素內容刪除,只刪除匹配的第一個
*/
public boolean remove(Object o) {
// 對要刪除的元素進行null判斷
// 對數據元素進行遍歷查找,知道找到第一個要刪除的元素,刪除后進行返回,如果要刪除的元素正好是最后一個那就慘了,時間復雜度可達O(n) 。。。
if (o == null) {
for (int index = 0; index < size; index++)
// null值要用==比較
if (elementData [index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 非null當然是用equals比較了
if (o.equals(elementData [index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
// 原理和之前的add一樣,還是進行數組復制,將index后的元素向前移動一個位置,不細解釋了,
int numMoved = size - index - 1;
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size ] = null; // Let gc do its work
}
/**
* 數組越界檢查
*/
private void RangeCheck(int index) {
if (index >= size )
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: " +size);
}
增加和刪除方法到這里就解釋完了,代碼是很簡單,主要需要特別關心的就兩個地方:1.數組擴容,2.數組復制,這兩個操作都是極費效率的,最慘的情況下(添加到list第一個位置,刪除list最后一個元素或刪除list第一個索引位置的元素)時間復雜度可達O(n)。
還記得上面那個坑嗎(為什么提供一個可以指定容量大小的構造方法 )?看到這里是不是有點明白了呢,簡單解釋下:如果數組初試容量過小,假設默認的10個大小,而我們使用ArrayList的主要操作時增加元素,不斷的增加,一直增加,不停的增加,會出現上面后果?那就是數組容量不斷的受挑釁,數組需要不斷的進行擴容,擴容的過程就是數組拷貝System.arraycopy的過程,每一次擴容就會開辟一塊新的內存空間和數據的復制移動,這樣勢必對性能造成影響。那么在這種以寫為主(寫會擴容,刪不會縮容)場景下,提前預知性的設置一個大容量,便可減少擴容的次數,提高了性能。
上面兩張圖分別是數組擴容和數組復制的過程,需要注意的是,數組擴容伴隨著開辟新建的內存空間以創建新數組然后進行數據復制,而數組復制不需要開辟新內存空間,只需將數據進行復制。
上面講增加元素可能會進行擴容,而刪除元素卻不會進行縮容,如果在已刪除為主的場景下使用list,一直不停的刪除而很少進行增加,那么會出現什么情況?再或者數組進行一次大擴容后,我們后續只使用了幾個空間,會出現上面情況?當然是空間浪費啦啦啦,怎么辦呢?
/**
* 將底層數組的容量調整為當前實際元素的大小,來釋放空間。
*/
public void trimToSize() {
modCount++;
// 當前數組的容量
int oldCapacity = elementData .length;
// 如果當前實際元素大小 小于 當前數組的容量,則進行縮容
if (size < oldCapacity) {
elementData = Arrays.copyOf( elementData, size );
}
更新
/**
* 將指定位置的元素更新為新元素
*/
public E set( int index, E element) {
// 數組越界檢查
RangeCheck(index);
// 取出要更新位置的元素,供返回使用
E oldValue = (E) elementData[index];
// 將該位置賦值為行的元素
elementData[index] = element;
// 返回舊元素
return oldValue;
}
查找
/**
* 查找指定位置上的元素
*/
public E get( int index) {
RangeCheck(index);
return (E) elementData [index];
}
是否包含
/**
* Returns <tt>true</tt> if this list contains the specified element.
* More formally, returns <tt>true</tt> if and only if this list contains
* at least one element <tt>e</tt> such that
* <tt>(o==null ? e==null : o.equals(e))</tt>.
*
* @param o element whose presence in this list is to be tested
* @return <tt> true</tt> if this list contains the specified element
*/
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
/**
* Returns the index of the first occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the lowest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
* or -1 if there is no such index.
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData [i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData [i]))
return i;
}
return -1;
}
/**
* Returns the index of the last occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the highest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
* or -1 if there is no such index.
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData [i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData [i]))
return i;
}
return -1;
}
contains主要是檢查indexOf,也就是元素在list中出現的索引位置也就是數組下標,再看indexOf和lastIndexOf代碼是不是很熟悉,沒錯,和public boolean remove(Object o) 的代碼一樣,都是元素null判斷,都是循環比較,不多說了。。。但是要知道,最差的情況(要找的元素是最后一個)也是很慘的。。。
容量判斷
/**
* Returns the number of elements in this list.
*
* @return the number of elements in this list
*/
public int size() {
return size ;
}
/**
* Returns <tt>true</tt> if this list contains no elements.
*
* @return <tt> true</tt> if this list contains no elements
*/
public boolean isEmpty() {
return size == 0;
}
由于使用了size進行計數,發現list大小獲取和判斷真的好容易。
總結:
(01) ArrayList 實際上是通過一個數組去保存數據的。當我們構造ArrayList時;若使用默認構造函數,則ArrayList的默認容量大小是10。
(02) 當ArrayList容量不足以容納全部元素時,ArrayList會重新設置容量:新的容量=“原始容量 + 原始容量*2”。
(03) ArrayList的克隆函數,即是將全部元素克隆到一個數組中。
(04) ArrayList實現java.io.Serializable的方式。當寫入到輸出流時,先寫入“容量”,再依次寫入“每一個元素”;當讀出輸入流時,先讀取“容量”,再依次讀取“每一個元素”。
ArrayList遍歷方式
ArrayList支持3種遍歷方式
(01) 第一種,通過迭代器遍歷。即通過Iterator去遍歷。
Integer value = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
value = (Integer)iter.next();
}
(02) 第二種,隨機訪問,通過索引值去遍歷。
由于ArrayList實現了RandomAccess接口,它支持通過索引值去隨機訪問元素。
Integer value = null;
int size = list.size();
for (int i=0; i<size; i++) {
value = (Integer)list.get(i);
}
(03) 第三種,for循環遍歷。如下:
Integer value = null;
for (Integer integ:list) {
value = integ;
}
下面通過一個實例,比較這3種方式的效率,實例代碼(ArrayListRandomAccessTest.java)如下:
import java.util.*;
import java.util.concurrent.*;
/*
* @desc ArrayList遍歷方式和效率的測試程序。
*
* @author skywang
*/
public class ArrayListRandomAccessTest {
public static void main(String[] args) {
List list = new ArrayList();
for (int i=0; i<100000; i++)
list.add(i);
//isRandomAccessSupported(list);
iteratorThroughRandomAccess(list) ;
iteratorThroughIterator(list) ;
iteratorThroughFor2(list) ;
}
private static void isRandomAccessSupported(List list) {
if (list instanceof RandomAccess) {
System.out.println("RandomAccess implemented!");
} else {
System.out.println("RandomAccess not implemented!");
}
}
public static void iteratorThroughRandomAccess(List list) {
long startTime;
long endTime;
startTime = System.currentTimeMillis();
for (int i=0; i<list.size(); i++) {
list.get(i);
}
endTime = System.currentTimeMillis();
long interval = endTime - startTime;
System.out.println("iteratorThroughRandomAccess:" + interval+" ms");
}
public static void iteratorThroughIterator(List list) {
long startTime;
long endTime;
startTime = System.currentTimeMillis();
for(Iterator iter = list.iterator(); iter.hasNext(); ) {
iter.next();
}
endTime = System.currentTimeMillis();
long interval = endTime - startTime;
System.out.println("iteratorThroughIterator:" + interval+" ms");
}
public static void iteratorThroughFor2(List list) {
long startTime;
long endTime;
startTime = System.currentTimeMillis();
for(Object obj:list)
;
endTime = System.currentTimeMillis();
long interval = endTime - startTime;
System.out.println("iteratorThroughFor2:" + interval+" ms");
}
}
運行結果:
iteratorThroughRandomAccess:3 ms
iteratorThroughIterator:8 ms
iteratorThroughFor2:5 ms
由此可見,遍歷ArrayList時,使用隨機訪問(即,通過索引序號訪問)效率最高,而使用迭代器的效率最低!
ArrayList示例
本文通過一個實例(ArrayListTest.java),介紹 ArrayList 中常用API的用法。
import java.util.*;
/*
* @desc ArrayList常用API的測試程序
* @author skywang
* @email kuiwu-wang@163.com
*/
public class ArrayListTest {
public static void main(String[] args) {
// 創建ArrayList
ArrayList list = new ArrayList();
// 將“”
list.add("1");
list.add("2");
list.add("3");
list.add("4");
// 將下面的元素添加到第1個位置
list.add(0, "5");
// 獲取第1個元素
System.out.println("the first element is: "+ list.get(0));
// 刪除“3”
list.remove("3");
// 獲取ArrayList的大小
System.out.println("Arraylist size=: "+ list.size());
// 判斷list中是否包含"3"
System.out.println("ArrayList contains 3 is: "+ list.contains(3));
// 設置第2個元素為10
list.set(1, "10");
// 通過Iterator遍歷ArrayList
for(Iterator iter = list.iterator(); iter.hasNext(); ) {
System.out.println("next is: "+ iter.next());
}
// 將ArrayList轉換為數組
String[] arr = (String[])list.toArray(new String[0]);
for (String str:arr)
System.out.println("str: "+ str);
// 清空ArrayList
list.clear();
// 判斷ArrayList是否為空
System.out.println("ArrayList is empty: "+ list.isEmpty());
}
}
運行結果:
the first element is: 5
Arraylist size=: 4
ArrayList contains 3 is: false
next is: 5
next is: 10
next is: 2
next is: 4
str: 5
str: 10
str: 2
str: 4
ArrayList is empty: true
總結
ArrayList和LinkedList的區別
- ArrayList是實現了基于動態數組的數據結構,LinkedList基于鏈表的數據結構。
- 對于隨機訪問get和set,ArrayList覺得優于LinkedList,因為LinkedList要移動指針。
- 對于新增和刪除操作add和remove,LinkedList比較占優勢,因為ArrayList要移動數據。
ArrayList和Vector的區別
- Vector和ArrayList幾乎是完全相同的,唯一的區別在于Vector是同步類(synchronized),屬于強同步類。因此開銷就比ArrayList要大,訪問要慢。正常情況下,大多數的Java程序員使用ArrayList而不是Vector,因為同步完全可以由程序員自己來控制。
- Vector每次擴容請求其大小的2倍空間,而ArrayList是1.5倍。
- Vector還有一個子類Stack.