前言
前一篇我們對數(shù)據(jù)結構有了個整體的概念上的了解,沒看過的小伙伴們可以看我的上篇文章:一文十三張圖帶你徹底了解所有數(shù)據(jù)結構。那么從今天開始,我們來對每一個數(shù)據(jù)結構進行一個詳細的講解,并帶著大家一起手寫代碼實現(xiàn)或者通過閱讀源碼來加強對數(shù)據(jù)結構的學習。我們從最簡單的也是最常用的數(shù)組開始。
線性表
在介紹數(shù)組之前,我們先了解一下什么是線性表。
線性表是指n個類型相同的數(shù)據(jù)元素的有限序列。在線性表的定義中我們可以提取出三個關鍵因素:
- 有限:這個n是指線性表的有限長度,線性表中的每個數(shù)據(jù)元素都有一個唯一確定的序號,我們通常也叫做下標,比如a[0]在線性表中的序號就是0,a[n]的序號就是n,這兒要注意的是,對于n個數(shù)據(jù)元素的線性表,第N個數(shù)據(jù)元素的序號是n-1,因為我們的序號是從0開始的。
- 類型相同:即數(shù)據(jù)元素的屬性相同,可以是數(shù)字、字符串,或者結構復雜的數(shù)據(jù)元素,比如商品、汽車、學生等。數(shù)據(jù)元素類型相同意味著每個數(shù)據(jù)元素在內存中存儲時都占用相同的內存空間,便于我們的查找定位。
- 序列:表示順序性,即線性表的相鄰元素之間存在著序偶關系。比如a[1]的直接前驅是a[0],a[1]的直接后續(xù)是a[2]。一言概之,線性表的的表頭沒有直接前驅,表尾沒有直接后續(xù)。除此之外,線性表中的每一個元素都有且僅有一個直接前驅和一個直接后續(xù)。
線性表的存儲結構分為兩種:
- 順序表:順序存儲結構
- 鏈表:鏈式存儲結構
數(shù)組
數(shù)組一種線性表數(shù)據(jù)結構,用一組連續(xù)的內存空間來存儲一組相同類型的數(shù)據(jù)。
從數(shù)組的定義中我們也可以提取三個關鍵因素:
- 線性表:見上面定義
- 連續(xù)內存空間:數(shù)據(jù)元素存儲在內存中的連續(xù)地址上。
- 類型相同:見線性表的介紹。
數(shù)組的特點
在內存中分配連續(xù)的空間,不需要存儲地址信息,位置就隱含著地址信息。
數(shù)組的優(yōu)點
- 高效的隨機訪問:數(shù)組按照索引查詢元素速度快。因為數(shù)組的數(shù)據(jù)元素是通過下標來訪問的,可以通過數(shù)組的首地址和尋址公式就能快速找到想要訪問的結點元素和存儲地址。
下面我們通過一張圖來看看數(shù)組是怎樣快速查找到結點的數(shù)據(jù)元素的。
在上篇文章中我介紹過數(shù)組的尋址公式:
數(shù)據(jù)元素存儲的內存地址=數(shù)組的起始地址+每個數(shù)據(jù)元素的大小下標*
通過尋址公式,我們可以很快的查找數(shù)組中每一個結點的存儲地址和數(shù)據(jù)元素,比如上圖中arr[3]的內存地址=1000+5*4=1020,(我們存的是int類型,所以元素的大小是4個字節(jié)),知道了存儲的存地址,也就查到了結點的數(shù)據(jù)元素68。
數(shù)組的缺點
- 刪除和插入數(shù)據(jù)元素效率較低:因為不管是刪除還是插入數(shù)據(jù)都需要大量移動數(shù)據(jù)元素,所以效率低下。
下面我畫圖來給大家演示數(shù)組中刪除和插入數(shù)據(jù)元素的步驟。
刪除元素:
可以看出,我們刪除數(shù)組下標為2的數(shù)據(jù),但是上圖中第二個圖并沒有真正刪掉,因為數(shù)組下標為2的位置還占著呢,最多算更新,把38變成null,那怎樣才算真正的刪除呢?就是把后續(xù)的數(shù)據(jù)都往前移一位,如上圖中步驟二黃色箭頭所示,最后變成第三章圖的結構,數(shù)組下標為5的結點內存地址沒有存任何數(shù)據(jù)。
這只是數(shù)組長度為6的一個數(shù)組,如果數(shù)組長度很大呢,每刪除一個元素,該元素后的元素都要相應往前移一位,相當于都要修改存儲地址,效率自然而然就低下了。
插入元素:
看上圖,如果我們要插入36這個元素,該怎么辦呢?我們都知道如果是添加的話自動往數(shù)組尾端添加,但是現(xiàn)在是在固定位置插入一個元素,我們只能將要插入元素位置的數(shù)據(jù)及其后續(xù)的所有元素都往后移一位,如上圖步驟二中黃色箭頭所示,最終結果見步驟三。
注意:元素右移要從最后一個元素右移,否則前面的值會將后面的值覆蓋。
同樣的道理,如果數(shù)組元素很多,每次插入都要移動大量的數(shù)據(jù)元素位置,效率也自然低下。
上面的插入和刪除,你可以想象在火車站排隊進站的時候,如果有個人跟你說趕不及了需要在你前面插個隊,那你包括你后面的人自然就要往后退一步。同理,你前面有個人因為看到個漂亮小姐姐跑走搭訕了,那你和你后面的人就會自動向前走一步。
接下來我們從源碼角度詳細的看一下數(shù)組的實現(xiàn)方式。
ArrayList源碼解析
基本上每個程序員面試的時候都被問過ArrayList底層是通過什么實現(xiàn)的,我想有很多小伙伴都是看面試題回答說通過數(shù)組實現(xiàn),但是我想知道有多少鄉(xiāng)親們是真的去看了ArrayList的源碼才這樣回答的,如果讓你自己去實現(xiàn)一個數(shù)組,或者用數(shù)組實現(xiàn)一個ArrayList,你會寫嗎?
沒看過沒關系,沒寫過也沒關系,今天我?guī)Т蠹乙黄鹑プxArrayList的源碼,我希望看完本篇文章的鄉(xiāng)親們,以后不但要知其然,還要知其所以然,這也是我寫這篇文章的初衷。好了,我們進入正題。
數(shù)組有哪些基本操作
說到數(shù)組,我們首先能想到常用的幾個方法:
- add:增加一個元素(或者是在指定下標處添加一個數(shù)據(jù)元素)
- get:返回指定索引(下標)位置的元素
- remove:刪除元素(或者是刪除指定下標處的元素)
- size:數(shù)組所有元素的數(shù)量
我們知道,數(shù)組存儲數(shù)據(jù)需要分配連續(xù)的內存空間,所以在定義數(shù)組的時候,需要預先指定數(shù)組的大小,如果超過數(shù)組的大小,則不能再往數(shù)組中添加元素。如果非要添加,則需要重新分配一塊更大的連續(xù)內存空間,將老的數(shù)組的元素復制過來,再添加新的元素。
但是,使用ArrayList則可以不用關系底層的擴容邏輯,因為它已經(jīng)幫我們實現(xiàn)好了,也就是ArrayList會幫我們動態(tài)擴容,這也是使用ArrayList最大的優(yōu)勢,ArrayList將很多數(shù)組操作的細節(jié)封裝起來。
下面我們來詳細解讀ArrayList的源碼。
ArrayList的完整結構圖
ArrayList定義
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
// 默認容量
private static final int DEFAULT_CAPACITY = 10;
// 初始化的一個空數(shù)組對象
private static final Object[] EMPTY_ELEMENTDATA = {};
// 同樣是一個空數(shù)組對象,如果使用默認構造函數(shù)創(chuàng)建,則默認對象內容默認是該值,為了與EMPTY_ELEMENTDATA區(qū)分開來
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 當前要操作的數(shù)據(jù)存放的對象
transient Object[] elementData; // non-private to simplify nested class access
// 當前數(shù)組的長度
private int size;
// 當前數(shù)組允許的最大長度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// ....
}
ArrayList構造函數(shù)
ArrayList默認有三個構造函數(shù),分別為:
- 無參構造函數(shù)
- 指定大小的構造函數(shù)
- 帶Collection對象的構造函數(shù)
我們來看一下源碼:
// 指定大小的構造函數(shù),如果為0,使用上面定義的EMPTY_ELEMENTDATA
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);
}
}
// 無參構造函數(shù),默認為空,使用上面定義的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 帶Collection對象的構造函數(shù)
public ArrayList(Collection<? extends E> c) {
// 將collection對象轉換成數(shù)組,然后將數(shù)組的地址的賦給elementData,淺拷貝
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;
}
}
add()方法
ArrayList提供了兩個add方法,一個是一個參數(shù),另一個兩個參數(shù),看源碼注釋,我將解釋寫在注釋上。
// 一個參數(shù)的代表將新元素加到數(shù)組的最后一個位置,如果數(shù)組長度大小夠的話
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
// 將新數(shù)據(jù)放到數(shù)組最后一個
elementData[size++] = e;
return true;
}
// 兩個參數(shù)的add方法表示將新元素添加到指定的數(shù)組下標位置處。
public void add(int index, E element) {
// 校驗要添加的位置下標是否小于0或者大于數(shù)組的size,是的話無法添加就拋異常
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
// 將需要插入的位置(index)后面的元素統(tǒng)統(tǒng)往后移動一位。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 將新的數(shù)據(jù)內容存放到數(shù)組的指定位置(index)上
elementData[index] = element;
size++;
}
上面兩個add方法的代碼中都有一句
ensureCapacityInternal(size + 1);
這是什么意思呢?這也是我認為的ArrayList源碼中最重要的一段代碼,下面我們來詳細看一下這句代碼里干了哪些事。
// 1、見下方解釋
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 2、見下方解釋
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 3、見下方解釋
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 4、見下方解釋
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);
}
按照順序對應解釋一下上面代碼的意思:
- 確保數(shù)組已使用長度(size)加1之后足夠存儲下一個數(shù)據(jù)。
- 確保添加的元素有地方存儲。在使用默認構造函數(shù)初始的時候(比如List list = new ArrayList()),這個時候elementData數(shù)組其實是空的,當?shù)谝淮瓮鶖?shù)組里添加元素時,先判斷數(shù)組是不是空的,如果是空的,會將當前elementData數(shù)組的長度變?yōu)?0:
- 將修改次數(shù)(modCount)自增1,判斷是否需要擴充數(shù)組長度,判斷條件就是用當前所需的數(shù)組最小長度與當前數(shù)組的長度對比,如果大于0,則需要增長數(shù)組長度(比如數(shù)組長度10,這時候需要添加第11個元素,11-10>0,則需要擴大數(shù)組容量。),也就是動態(tài)擴容。
- 動態(tài)擴容:如果當前的數(shù)組已使用空間(size)加1之后 大于數(shù)組長度,則增大數(shù)組容量,擴大為原來的1.5倍(oldCapacity + (oldCapacity >> 1)表示右移一位,位運算相當于oldCapacity/2)。
上面就是我個人認為ArrayList操作中最重要的一個特點和優(yōu)勢:動態(tài)擴容 。怎么樣?小伙伴們現(xiàn)在都明白了嗎?如果還不明白,送給你一段代碼,打個斷點,將源碼debug一遍,你就知道我說的意思了。測試代碼很簡單,如下:
package com.mzc.datastrcuture;
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
list.add(8);
list.add(9);
list.add(10);
list.add(11); // 在這一行打斷點,走到add方法里面去,看看發(fā)生了什么,是不是如我上面1234的步驟所說。
System.out.println(list.size());
}
}
好了,add方法就講到這兒了,說完了add方法和動態(tài)擴容,ArrayList中的其他方法就都很簡單了,只要理解了我上面說的數(shù)組操作,基本上看一遍就都能懂了,就不多說了。
總結
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
一、從ArrayList的定義上我們能看出來:
- ArrayList 繼承了AbstractList,實現(xiàn)了List。它是一個數(shù)組隊列,提供了相關的添加、刪除、修改、遍歷等功能。
- ArrayList 實現(xiàn)了RandmoAccess接口,即提供了隨機訪問功能。
- ArrayList實現(xiàn)了Cloneable接口,即覆蓋了函數(shù)clone(),能被克隆。
- ArrayList實現(xiàn)java.io.Serializable接口,這意味著ArrayList支持序列化,能通過序列化去傳輸。
二、方法操作總結:
- ArrayList自己實現(xiàn)了序列化和反序列化的方法,因為它自己實現(xiàn)了writeObject和readObject方法。
private void writeObject(java.io.ObjectOutputStream s)、
private void readObject(java.io.ObjectInputStream s)
- ArrayList基于數(shù)組方式實現(xiàn),無容量的限制(會擴容)。
- 添加元素時可能要擴容(所以最好預判一下),刪除元素時不會減少容量(若希望減少容量,trimToSize()),刪除元素時,將刪除掉的位置元素置為null,下次gc就會回收這些元素所占的內存空間。
- 線程不安全
- add(int index, E element):添加元素到數(shù)組中指定位置的時候,需要將該位置及其后邊所有的元素都整塊向后復制一位,注意是從最后一位開始向后移動,即先將n-1 移動到n,再將n-2移動到n-1,以此類推到index。
- get(int index):獲取指定位置上的元素時,可以通過索引直接獲?。∣(1))。
- remove(Object o)需要遍歷數(shù)組。
- remove(int index)不需要遍歷數(shù)組,只需判斷index是否符合條件即可,效率比remove(Object o)高。
- contains(E)需要遍歷數(shù)組。
- 使用iterator遍歷可能會引發(fā)多線程異常。
三、面試常問總結:
- ArrayList初始化大小是10。
- ArrayList通過grow()方法擴容,每次擴容1.5倍。
- ArrayList讀取查找數(shù)據(jù)效率高,修改刪除效率低。
- ArrayList可以存放重復元素,也可以有null值。
- 和Vector不同,ArrayList中的操作不是線程安全的。所以,建議在單線程中才使用ArrayList,而在多線程中可以選擇Vector或者CopyOnWriteArrayList。
喜歡本文的同學可以關注我的公眾號,碼之初。帶你解鎖更多數(shù)據(jù)結構和源碼解讀。