目錄
- 面試三連
- 初識ArrayList
- ArrayList源碼解析
- ArrayList方法
- add()
- trimToSize()
- ensureCapacity(int minCapacity)
- size()
- isEmpty()
- contains(Object o)
- indexOf(Object o)
- lastIndexOf(Object o)
- clone()
- toArray()
- get(int index)
- set(int index, E element)
- remove(int index)
- remove(Object o)
- clear()
- addAll(Collection<? extends E> c)
- addAll(int index, Collection<? extends E> c)
- 開篇解答
- 總結
面試三連
面試官:使用過集合嗎?能說說都使用過哪些嗎?
小明:當然使用過,使用比較多的就是ArrayList與HashMap,還有LinkedList、HashTable、ConcurrentHashMap等等。
面試官:用的不少啊,那來說說你對ArrayList的理解吧。
小明:ArrayList是一個基於數組實現的集合,主要特點在於隨機訪問速度較快,但是插入刪除速度較慢。
面試官:那你知道爲什麼隨機訪問速度較快,插入刪除速度較慢嗎?
小明:不知道。
面試官: 現在內存還有10M內存,現在想申請一塊5M大小的ArrayList空間,程序會拋出OOM嗎?
小明:不會。
面試官:出去的時候記得把門帶上,謝謝!
小明在面試在面試的時候被問到了ArrayList,但是他只回答到了一部分,比如剛剛的那個問題:爲什麼隨機訪問速度較快,插入刪除速度較慢?小明就矇蔽了,因爲小明背面試題的時候只是記住結論,而並沒有探索爲什麼,所以再面試的時候就gg了,這也給了我們一個警告,我們在看資料的時候一定不能只看結論,否則就只能和小明一樣回家等通知了。
初識ArrayList
ArrayList就是動態數組,用MSDN中的說法,就是Array的複雜版本,它提供了動態的增加和減少元素,實現了ICollection和IList接口,靈活的設置數組的大小等好處。
也就是說,ArrayList其實就是一個數組,一般的數組長度是不允許發生改變的,但是ArrayList實現了數組的長度改變,所以叫動態數組,那你好奇他是怎麼實現的動態數組嗎?請隨我一起剝開ArrayList的神祕面紗。
我相信很多人在開發中或多或少都會使用到ArrayList,比如接收數據庫返回的列表,前端的批量保存等等,所以ArrayList在我們開發中還是比較重要的存在,所以今天我就來講一講它的源碼解析。
ArrayList源碼解析
ArrayList成員變量
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
DEFAULT_CAPACITY:數組初始默認大小,大小等於10
EMPTY_ELEMENTDATA:使用有參構造時,但是數組大小爲0或者數組爲空的時候使用。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA :使用無參構造的默認數組值,也就是elementData
elementData:動態數組
size:數組大小
實例化ArrayList
ArrayList的實例化一共有三種方式
1.無參構造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
注意這裏的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,是不是我們剛剛說的,無參構造的時候elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
使用方法
ArrayList<String> list = new ArrayList<String>();
System.out.println("集合:"+list);
集合:[]
Process finished with exit code 0
2.有參構造
有參構造分兩種情況,第一種是給定數組的初始化大小,第二種是拷貝其他集合
指定數組大小
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
initialCapacity:初始化數組的大小,如果initialCapacity大於o,那麼就會創建一個長度爲initialCapacity的數組,等於0,就會將EMPTY_ELEMENTDATA賦值給elementData,否則怕拋出異常。
使用方法
//有參構造
ArrayList<String> list2 = new ArrayList<String>(50);
System.out.println("集合:" + list2);
集合:[]
Process finished with exit code 0
這裏就有疑問了,上面兩種創建方式返回的結果都是一樣的,爲什麼ArrayList還要給出一個指定大小的構造呢?肯定是有原因的,這個我們在後面講。
數組拷貝
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
我們來看看,首先講需要拷貝的集合轉成數組,然後判斷需要拷貝的數組大小是否等於0,等於0直接給一個空數組:EMPTY_ELEMENTDATA,需如果需要拷貝到餓數組大於0並且和當前的數組不是同一個對象,那麼就執行拷貝,請注意這裏的拷貝屬於淺拷貝,爲什麼這麼說呢?請看下面代碼
List<User> list3 = new ArrayList<User>();
//初始化User對象
User user = new User();
user.setUserName("小明");
user.setSex(1);
user.setAge(18);
list3.add(user);
System.out.println("list3:"+list3);
//集合拷貝
ArrayList<User> list4 = new ArrayList<User>(list3);
System.out.println("拷貝完之後的list4:"+list4);
//集合拷貝完成之後修改User對象的值
user.setAge(20);
System.out.println("修改User對象年齡之後的lsit4:"+list4);
明白我爲什麼這麼寫嗎?因爲我剛剛說了這裏的集合拷貝指的是淺拷貝,所以我打印了還沒有背拷貝的list3、拷貝完之後的list4以及修改User對象年齡之後的lsit4,你能猜到他們對應的輸出結果嗎?自己可以在腦海中想象一下,然後請看下面輸出結果
list3:[User(userName=小明, age=18, sex=1)]
拷貝完之後的list4:[User(userName=小明, age=18, sex=1)]
修改User對象年齡之後的lsit4:[User(userName=小明, age=20, sex=1)]
Process finished with exit code 0
我們可以看到list3和拷貝完之後的list4是一模一樣的,但是修改User對象年齡之後的lsit4卻發生了改變,那就是年齡變成了20,我們並沒有對list4的對象做修改,他爲啥改變了呢?這就是java淺拷貝和深拷貝的知識了,如果對這方面不熟悉的可以參考:原型模式:如何快速的克隆出一個對象?。
ArrayList的構造函數基本上講的差不多了,但是這裏還是沒有引出動態數組的概念啊,他還是一個死的,那是什麼時候他會變成動態的呢?客官不要心急,我們接着往下看。
ArrayList方法
add()
ArrayList一共給我們提供了兩個add(),我們一起來看一下吧。
第一個:add(E e)
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) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//數組初始化的大小與需要插入位置的大小比較,返回大的那一個
public static int max(int a, int b) {
return (a >= b) ? a : b;
}
//判斷是否需要擴容,如果插入的位置已經大於數組的大小,那麼進行擴容操作
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//擴容,將數組擴大原來的1.5倍,並且將原來的數組拷貝到新數組,再將新數組複製給原數組
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);
}
add()的源碼大致就是這樣的,每次添加的時候都會判斷插入的位置是否大於了數組的大小,如果大於就進行擴容處理,將數組擴大原來的1.5倍( oldCapacity + (oldCapacity >> 1)),但是這裏有一點需要特別注意一下,如果擴容的大小已經超過了ArrayList指定的最大數值,那會發生什麼呢?
@Native public static final int MAX_VALUE = 0x7fffffff;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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指定的最大數值,他會先判斷插入的位置是否已經大於了ArrayList允許的最大數值,如果大於,直接返回:MAX_VALUE,否者返回MAX_ARRAY_SIZE,這裏一定要注意一個是擴容後的大小,一個是插入位置,一定不要搞錯,這裏就是ArrayList爲什麼被稱爲動態數組。
使用方法
//無參構造
ArrayList<String> list = new ArrayList<String>();
list.add("小明");
list.add("賣托兒索的小火柴");
System.out.println("list:" + list);
list:[小明, 賣托兒索的小火柴]
Process finished with exit code 0
我這裏初始化的時候創建了一個無參構造,所以數組的初始大小爲:10,添加兩個元素的時候並不會觸發擴容機制。
第二種:add(int index, E element)
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++;
}
故名思意,看參數就應該能大致的猜出這個方法是幹什麼的,沒錯,他就是插入指定位置元素,他的插入和第一個差不多,唯一的區別就是第一個是往後添加,這裏是按index添加到這指定下表位置,然後將其他的元素往後移,也就是System.arraycopy(elementData, index, elementData, index + 1, size - index)。
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("賣托兒索的小火柴");
list.add("海闊天空");
list.add(5,"逆天而行");
System.out.println("list:" + list);
你們可以猜到執行的結果嗎?執行結果就是報錯,爲什麼呢?源碼裏面有這麼一個方法
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
判斷插入的位置是否大於elementData的數組長度或者是否小於0,由於我這裏只添加了兩個元素,所以size應該是3,我們添加的下標卻是5,所以就會拋出異常
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 5, Size: 2
at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:661)
at java.util.ArrayList.add(ArrayList.java:473)
at com.ymy.list.MyArrayList.main(MyArrayList.java:18)
Process finished with exit code 1
我們修改一下代碼,將下表修改成1
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("賣托兒索的小火柴");
list.add("海闊天空");
list.add(1,"逆天而行");
System.out.println("list:" + list);
這個時候我們再來看運行結果
list:[小明, 逆天而行, 賣托兒索的小火柴, 海闊天空]
Process finished with exit code 0
我們發現逆天而行被添加到了下標爲1的位置,而賣托兒索的小火柴,和海闊天空相應的往後移了一位。
trimToSize()
之前沒有說清楚size與elementData的關係,size表示的是elementData數組中已經存放了多少元素,而elementData.length表示ArrayList的初始數組大小,請不要搞混.。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
這個方法就是判斷已經存在數組中的元素個數(size)和數組初始化的大小(elementData.length)做對比,如果小於初始化值就去掉多餘的,返回一個elementData大小等於size,實現的方式就是通過拷貝的形式。
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("賣托兒索的小火柴");
list.trimToSize();
System.out.println("list:" + list);
爲了能看到我說的,我們斷點調試走一波
我們發現走到斷點的那一行size=2,elementData.length= 10,下面就是判斷了,很明顯2<10,所以這裏會執行數據拷貝,拷貝完成之後我們在看結果
清除了多餘沒用的元素下標,但是這個方法大家在使用的時候還是慎重比較好,如果你清除完成之後又想添加數據,這個時候ArrayList就會執行擴容操作了,這是需要進行數據拷貝的,慎重哦。
ensureCapacity(int minCapacity)
源碼如下
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
大致意思:當你初始化了一個大小爲10的初始數組之後,並添加了5條數據,這個時候你發現10可能不夠,要是數組大小在大一點就好了,ensureCapacity就是解決這個問題的,他會擴大你指定的大小,但是擴大之後數組的大小是不是你指定的大小這個是不確定的,因爲ensureExplicitCapacity(minCapacity);的源碼在上面也看到了,他會現在原來的數組大小的基礎上擴大1.5倍,然後在和你傳入的數值做對比,如果大於你傳入的,那麼使用舊數組(elementData)大小的1.5倍作爲新數組的大小,如果小於你傳入的數值,這個時候就會以你傳入的大小作爲數組(elementData)的大小,這點一定要搞清楚哦,不然的話,你會發現你明明設置了值,但是最後數組的大小卻和你設置的不一樣,就會感覺是你的代碼寫的有問題。
我們先來看一個擴容小於原數組大小1.5倍的數值:12
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("賣托兒索的小火柴");
list.ensureCapacity(12);
System.out.println("list:" + list);
我們發現elementData的大小並不是我們傳入的12,而是15,要注意哦
我們再來看看擴容大於原始數組大小1.5倍的數值:20
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("賣托兒索的小火柴");
list.ensureCapacity(20);
System.out.println("list:" + list);
在這裏我在貼出一下導致這兩種原因的代碼在哪裏
size()
返回當前ArrayList已經添加了多少條元素,這個不用多說,相信大家都知道。
isEmpty()
public boolean isEmpty() {
return size == 0;
}
判斷ArrayList是否添加了數據,但是這點需要注意一下,這裏只能判斷是否存在元素,不能判斷ArrayList是否爲空,這點需要注意,如果使用這個方法判斷空的話就報錯哦。
contains(Object o)
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
判斷ArrayList所有元素中是否存在當前元素,如果是對象,判斷的就是引用地址了,這裏需要注意,如果我們的ArrayList的泛型是對象,那麼最好重寫一下equals和hashcode方法,舉個例子
沒有重寫equals()與 hashCode()
ArrayList<User> list = new ArrayList<User>(10);
//用戶插入集合的數據
User user1 = new User();
user1.setUserName("小明");
user1.setSex(1);
user1.setAge(18);
//用於對比的數據
User user2 = new User();
user2.setUserName("小明");
user2.setSex(1);
user2.setAge(18);
list.add(user1);
System.out.println("是否包含user1:" + list.contains(user1));
System.out.println("是否包含user2:" + list.contains(user2));
是否包含user1:true
是否包含user2:false
Process finished with exit code 0
看到輸出結果了吧,判斷是否包含user1結果爲:true;判斷是否包含user2的結果爲:false,那是因爲往list中添加的是user1,所以比較user1的時候他們都是同一個引用地址,所以返回true,而user2是新new出來的,他們是兩個完全不相同的對象,內存地址肯定也不相同,所以這個時候肯定就返回false,因爲user1和user2裏面存放的數據都是一樣的,有時候我們只需要判斷內容是否相等,並不需要判斷內存地址是否相等的時候需要怎麼做呢?
重寫equals()與 hashCode()
改造一下我們的User對象
package com.ymy.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.Objects;
@Getter
@Setter
@ToString
public class User {
private String userName;
private Integer age;
private Integer sex;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(userName, user.userName) &&
Objects.equals(age, user.age) &&
Objects.equals(sex, user.sex);
}
@Override
public int hashCode() {
return Objects.hash(userName, age, sex);
}
}
測試代碼還是不變,我們在運行查看結果
是否包含user1:true
是否包含user2:true
Process finished with exit code 0
總結就是一句話,ArrayList引用對象的時候如果沒有重寫equals()與 hashCode()對比的就是內存地址,如果重寫了equals()與 hashCode(),對比的就是實實在在的數據。請拿小本本記好,這個要考。
indexOf(Object o)
查找元素所在的下標,如果查找的是對象,默認比較的是內存地址這點和contains(Object o)一樣。
源碼如下
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;
}
如果查找的內容爲空,那麼這個就會返回第一個元素爲空的下標,否者返回數組中第一次出現查找元素的下標。
沒有重寫equals()與 hashCode()
ArrayList<User> list = new ArrayList<User>(10);
//用戶插入集合的數據
User user1 = new User();
user1.setUserName("小明");
user1.setSex(1);
user1.setAge(18);
//用於對比的數據
User user2 = new User();
user2.setUserName("小明");
user2.setSex(1);
user2.setAge(18);
list.add(user1);
System.out.println("是否包含user1:" + list.indexOf(user1));
System.out.println("是否包含user2:" + list.indexOf(user2));
是否包含user1:0
是否包含user2:-1
Process finished with exit code 0
很明顯查找user1的時候是同一個內存地址,所以返回了對應的下標,而user2與user1不是同一個內存地址,所以返回了-1。
重寫equals()與 hashCode()
重寫的方法和contains()一樣,我們直接看結果即可
是否包含user1:0
是否包含user2:0
Process finished with exit code 0
所以一定要區分你需要查找的是值相同還是地址相同,不然就會導致bug哦。
lastIndexOf(Object o)
與indexOf()效果一樣,都是查找元素所在的下標,但是又有一點區別,那就是lastIndexOf()返回的是最後一次出現的下標位置。
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;
}
這個使用和indexOf一樣,這裏就不做demo展示了。
clone()
克隆一個ArrayList
源碼如下
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
使用方式
ArrayList<User> list = new ArrayList<User>(10);
//用戶插入集合的數據
User user1 = new User();
user1.setUserName("小明");
user1.setSex(1);
user1.setAge(18);
list.add(user1);
System.out.println("list1:"+list);
ArrayList<User> list2 = (ArrayList<User>) list.clone();
System.out.println("list2:"+list2);
運行結果
list1:[User(userName=小明, age=18, sex=1)]
list2:[User(userName=小明, age=18, sex=1)]
Process finished with exit code 0
將list拷貝到list2,但是這裏需要注意一點,這裏的拷貝屬於淺拷貝,list2和list1共享一個User對象,這是需要特別注意的。
toArray()
將ArrayList轉換成數組
源碼
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
使用方式
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("逆天而行");
System.out.println("list1:" + list);
Object[] array = list.toArray();
System.out.println("array:" + array);
array[2] = "海闊天空";
運行結果
list1:[小明, 逆天而行]
array:[Ljava.lang.Object;@3a71f4dd
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2
at com.ymy.list.MyArrayList.main(MyArrayList.java:17)
Process finished with exit code 1
ArrayList轉數組沒有問題,但是在數組賦值的時候卻報錯了,這一點需要注意,這裏的數組長度就是ArrayList的數組實際長度,ArrayList的長度是2,下標最大爲1,但是我們賦值的時候給的下標是2,所以就會拋出數組越界的錯誤。
get(int index)
根據下表獲取元素信息
源碼
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
第一步校驗下標是否越界,然後返回對應下標元素信息。
使用方法
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("逆天而行");
System.out.println("list1:" + list);
String name = list.get(1);
System.out.println("name:"+name);
運行結果
Connected to the target VM, address: '127.0.0.1:62855', transport: 'socket'
list1:[小明, 逆天而行]
name:逆天而行
Disconnected from the target VM, address: '127.0.0.1:62855', transport: 'socket'
Process finished with exit code 0
這裏面的下標一定不能大於elementData的size,否者就會拋出數組越界。
set(int index, E element)
在指定下標添加元素
源碼
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
添加的下標不能越界,他會將你的元素添加到數組的指定下標,並且返回被替換的元素,這裏是替換哦,被替換的元素不會往後移,這點需要特別注意。
使用方法
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("逆天而行");
System.out.println("list:" + list);
String name = list.set(1,"海闊天空");
System.out.println("name:"+name);
System.out.println("修改後的list:"+list);
結果
list:[小明, 逆天而行]
name:逆天而行
修改後的list:[小明, 海闊天空]
Process finished with exit code 0
remove(int index)
刪除指定下標的元素,並返回被刪除的元素值
源碼
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;
}
他在刪除了指定下標之後,那這個下標的位置就會處於空缺,這個時候ArrayList做了一件事,那就是將數組進行重新排序,實現的方式就是數據拷貝,使用一個新的數組接受兩段數據,一段是刪除下標之前的數據,一段是刪除下表之後的數據,整合到一個新的數組,然後賦值到原數組中。這裏只需要瞭解一下即可,最後返回了被刪除的元素值。
使用方法
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("逆天而行");
System.out.println("list:" + list);
String name = list.remove(1);
System.out.println("name:"+name);
System.out.println("刪除後的list:"+list);
運行結果
list:[小明, 逆天而行]
name:逆天而行
刪除後的list:[小明]
Process finished with exit code 0
remove(Object o)
通過元素值刪除數組中存在的元素,這種刪除比較耗時間,爲什麼這麼說呢?請看源碼
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;
}
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<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("逆天而行");
System.out.println("list:" + list);
boolean remove = list.remove("逆天而行");
System.out.println("是否刪除成功:"+remove);
System.out.println("刪除後的list:"+list);
運行結果
list:[小明, 逆天而行]
是否刪除成功:true
刪除後的list:[小明]
Process finished with exit code 0
clear()
這個方法比較簡單,就是將數組中所有的元素都設置爲null,然後將size設置爲0。
源碼
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
使用方法
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("逆天而行");
System.out.println("list:" + list);
list.clear();
System.out.println("刪除後的list:"+list);
運行結果
list:[小明, 逆天而行]
刪除後的list:[]
Process finished with exit code 0
addAll(Collection<? extends E> c)
將其他的集合添加到當前集合,這裏添加方式是通過拷貝實現的。
源碼如下
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
ensureCapacityInternal(size + numNew);這行代碼是不是經常看到,不用我多說想必大家也知道了,沒錯,就是判斷當前的集合是否可以裝下這些數據,是否需要擴容,接下來就是數據添加了,添加的方式就是通過數據拷貝,這裏的拷貝同樣屬於淺拷貝。
使用方法
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("逆天而行");
System.out.println("list:" + list);
ArrayList<String> list2 = new ArrayList<String>();
list2.add("隨風起舞");
lis2t.add("窮兇極惡");
list.addAll(list2);
System.out.println("添加之後的lsit:"+list);
運行結果
list:[小明, 逆天而行]
添加之後的lsit:[小明, 逆天而行, 隨風起舞, 窮兇極惡]
Process finished with exit code 0
addAll(int index, Collection<? extends E> c)
這個方法其實和上面那個也是大同小異,就是添加集合,但是這裏的添加方式有點區別,這裏可以指定下標。
源碼如下
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
它會往你指定的下標添加集合元素,原本屬於當前下標的元素向後移動,移動方式也是通過數據拷貝事項的。
使用方法
ArrayList<String> list = new ArrayList<String>(10);
list.add("小明");
list.add("逆天而行");
list.add("鐵血無雙");
System.out.println("list:" + list);
ArrayList<String> list2 = new ArrayList<String>();
list2.add("隨風起舞");
list2.add("窮兇極惡");
list.addAll(1,list2);
System.out.println("添加之後的lsit:"+list);
運行結果
list:[小明, 逆天而行, 鐵血無雙]
添加之後的lsit:[小明, 隨風起舞, 窮兇極惡, 逆天而行, 鐵血無雙]
Process finished with exit code 0
我們插入的下標位置爲1,這個時候ArrayList就將list2這兩個元素從下標1開始往後田間,衝突的元素就往後移,直到沒有衝突爲止。
這裏就暫時先說這麼多吧,這些都是一些比較常用的方法,看了肯定會對你有所幫助。
開篇解答
再文章開頭的時候我們說到小明面試的時候被問到在一塊內存只有10M的空間中申請一塊5M的數組空間,會導致OOM嗎?
這個答案是:不確定,爲什麼這麼說呢?原因很簡單,那是因爲數組在內存中存放的地址都是連續的,比如:00xx01、00xx02、00xx03 … 00xxnn,雖然說內存還有10M,但是不能保證連續的內存空間還剩5M,如果連續空間不足5M,那麼在創建ArrayList的時候就會拋出OOM,這個時候你就會疑問了,既然數組要求內存地址是連續的,那是什麼導致內存地址不連續呢?這個就涉及到鏈表了,鏈表存儲的數據在內存中的地址是隨機的,關於鏈表這個就不展開了,否者又得講半天,所以只需要記住:數組盛情內存空間的時候要求內存地址是連續的,如果連續的內存地址空間不足,那麼在創建數組的時候就會拋出OOM。
總結
雖然我們在日常開發中經常使用ArrayList,但是我們對他的原理熟悉嗎?如果不熟悉就因爲一個細節就會讓你的程序變慢或者內存溢出。
自動擴容:如果我們創建ArrayList的時候知道了大概的長度的時候儘量指明數組長度,否者在數據添加的時候就會頻繁出發擴容,然而擴容就會導致數據拷貝,雖然數據拷貝屬於淺拷貝,但是頻繁的數據拷貝同樣會消耗我們的性能,所以在實例化的時候最好給出數組初始長度,避免頻繁擴容。
手動擴容(ensureCapacity):手動擴容的時候需要注意一點,手動擴容的最終數組大小有可能不是你指定的大小,他有一個校驗規則,第一,將元素組長度擴大1.5倍,然後在和你傳入的擴容數值做對比,誰大用誰。
刪除、修改(元素刪除):這兩個相對來說比較耗時,爲什麼這麼說呢?原因就是刪除的時候會循環整個數組,最好情況第一次就找到了你要操作的數據,但是最壞情況是你循環了一遍數組才找到你要操作的元素,所以刪除、修改的時間複雜度爲:O(n),並且操作完成之後還伴隨這一次數據拷貝,所以刪除的時候能用下標就用下標,是在找不到下標在使用元素刪除。
查詢:隨機訪問的速度較快,那是因爲根據下標能很快的找到對應的元素,時間複雜度爲:O(1)。
線程安全性問題:ArrayList不是線程安全的,這點想必大家都知道,這裏就不再囉嗦了。
總的來說就是儘量指定數組長度,避免頻繁擴容,少使用元素刪除,所以在選型的時候一定要注意使用,雖然ArrayList簡單,但是使用不當,也會給項目造成很大損失。
ArrayList的源碼解析並沒有完全寫完,還有一些,我覺得開發中可能使用的不多,所以這裏就不打算繼續講了,大家看着也累,後續有時間的話再給補上,還請見諒。