增加数组下标_数组以及ArrayList源码解析
點擊上方"碼之初"關(guān)注,···選擇"設(shè)為星標"
與精品技術(shù)文章不期而遇
前言
前一篇我們對數(shù)據(jù)結(jié)構(gòu)有了個整體的概念上的了解,沒看過的小伙伴們可以看我的上篇文章:一文十三張圖帶你徹底了解所有數(shù)據(jù)結(jié)構(gòu)。那么從今天開始,我們來對每一個數(shù)據(jù)結(jié)構(gòu)進行一個詳細的講解,并帶著大家一起手寫代碼實現(xiàn)或者通過閱讀源碼來加強對數(shù)據(jù)結(jié)構(gòu)的學習。我們從最簡單的也是最常用的數(shù)組開始。線性表
在介紹數(shù)組之前,我們先了解一下什么是線性表。線性表是指n個類型相同的數(shù)據(jù)元素的有限序列。在線性表的定義中我們可以提取出三個關(guān)鍵因素:有限:這個n是指線性表的有限長度,線性表中的每個數(shù)據(jù)元素都有一個唯一確定的序號,我們通常也叫做下標,比如a[0]在線性表中的序號就是0,a[n]的序號就是n,這兒要注意的是,對于n個數(shù)據(jù)元素的線性表,第N個數(shù)據(jù)元素的序號是n-1,因為我們的序號是從0開始的。
類型相同:即數(shù)據(jù)元素的屬性相同,可以是數(shù)字、字符串,或者結(jié)構(gòu)復雜的數(shù)據(jù)元素,比如商品、汽車、學生等。數(shù)據(jù)元素類型相同意味著每個數(shù)據(jù)元素在內(nèi)存中存儲時都占用相同的內(nèi)存空間,便于我們的查找定位。
序列:表示順序性,即線性表的相鄰元素之間存在著序偶關(guān)系。比如a[1]的直接前驅(qū)是a[0],a[1]的直接后續(xù)是a[2]。一言概之,線性表的的表頭沒有直接前驅(qū),表尾沒有直接后續(xù)。除此之外,線性表中的每一個元素都有且僅有一個直接前驅(qū)和一個直接后續(xù)。
線性表的存儲結(jié)構(gòu)分為兩種:
順序表:順序存儲結(jié)構(gòu)
鏈表:鏈式存儲結(jié)構(gòu)
數(shù)組
數(shù)組一種線性表數(shù)據(jù)結(jié)構(gòu),用一組連續(xù)的內(nèi)存空間來存儲一組相同類型的數(shù)據(jù)。
從數(shù)組的定義中我們也可以提取三個關(guān)鍵因素:
線性表:見上面定義
連續(xù)內(nèi)存空間:數(shù)據(jù)元素存儲在內(nèi)存中的連續(xù)地址上。
類型相同:見線性表的介紹。
數(shù)組的特點
在內(nèi)存中分配連續(xù)的空間,不需要存儲地址信息,位置就隱含著地址信息。
數(shù)組的優(yōu)點
高效的隨機訪問:數(shù)組按照索引查詢元素速度快。因為數(shù)組的數(shù)據(jù)元素是通過下標來訪問的,可以通過數(shù)組的首地址和尋址公式就能快速找到想要訪問的結(jié)點元素和存儲地址。
下面我們通過一張圖來看看數(shù)組是怎樣快速查找到結(jié)點的數(shù)據(jù)元素的。
在上篇文章中我介紹過數(shù)組的尋址公式:數(shù)據(jù)元素存儲的內(nèi)存地址=數(shù)組的起始地址+每個數(shù)據(jù)元素的大小下標*通過尋址公式,我們可以很快的查找數(shù)組中每一個結(jié)點的存儲地址和數(shù)據(jù)元素,比如上圖中arr[3]的內(nèi)存地址=1000+5*4=1020,(我們存的是int類型,所以元素的大小是4個字節(jié)),知道了存儲的存地址,也就查到了結(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ù)都往前移一位,如上圖中步驟二黃色箭頭所示,最后變成第三章圖的結(jié)構(gòu),數(shù)組下標為5的結(jié)點內(nèi)存地址沒有存任何數(shù)據(jù)。
這只是數(shù)組長度為6的一個數(shù)組,如果數(shù)組長度很大呢,每刪除一個元素,該元素后的元素都要相應(yīng)往前移一位,相當于都要修改存儲地址,效率自然而然就低下了。
插入元素:
看上圖,如果我們要插入36這個元素,該怎么辦呢?我們都知道如果是添加的話自動往數(shù)組尾端添加,但是現(xiàn)在是在固定位置插入一個元素,我們只能將要插入元素位置的數(shù)據(jù)及其后續(xù)的所有元素都往后移一位,如上圖步驟二中黃色箭頭所示,最終結(jié)果見步驟三。
注意:元素右移要從最后一個元素右移,否則前面的值會將后面的值覆蓋。
同樣的道理,如果數(shù)組元素很多,每次插入都要移動大量的數(shù)據(jù)元素位置,效率也自然低下。上面的插入和刪除,你可以想象在火車站排隊進站的時候,如果有個人跟你說趕不及了需要在你前面插個隊,那你包括你后面的人自然就要往后退一步。同理,你前面有個人因為看到個漂亮小姐姐跑走搭訕了,那你和你后面的人就會自動向前走一步。接下來我們從源碼角度詳細的看一下數(shù)組的實現(xiàn)方式。ArrayList源碼解析基本上每個程序員面試的時候都被問過ArrayList底層是通過什么實現(xiàn)的,我想有很多小伙伴都是看面試題回答說通過數(shù)組實現(xiàn),但是我想知道有多少鄉(xiāng)親們是真的去看了ArrayList的源碼才這樣回答的,如果讓你自己去實現(xiàn)一個數(shù)組,或者用數(shù)組實現(xiàn)一個ArrayList,你會寫嗎?沒看過沒關(guān)系,沒寫過也沒關(guān)系,今天我?guī)Т蠹乙黄鹑プxArrayList的源碼,我希望看完本篇文章的鄉(xiāng)親們,以后不但要知其然,還要知其所以然,這也是我寫這篇文章的初衷。好了,我們進入正題。
數(shù)組有哪些基本操作
說到數(shù)組,我們首先能想到常用的幾個方法:
add:增加一個元素(或者是在指定下標處添加一個數(shù)據(jù)元素)
get:返回指定索引(下標)位置的元素
remove:刪除元素(或者是刪除指定下標處的元素)
size:數(shù)組所有元素的數(shù)量
下面我們來詳細解讀ArrayList的源碼。
ArrayList的完整結(jié)構(gòu)圖
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ù)組對象,如果使用默認構(gòu)造函數(shù)創(chuàng)建,則默認對象內(nèi)容默認是該值,為了與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構(gòu)造函數(shù)
ArrayList默認有三個構(gòu)造函數(shù),分別為:
無參構(gòu)造函數(shù)
指定大小的構(gòu)造函數(shù)
帶Collection對象的構(gòu)造函數(shù)
我們來看一下源碼:
// 指定大小的構(gòu)造函數(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);
}
}// 無參構(gòu)造函數(shù),默認為空,使用上面定義的DEFAULTCAPACITY_EMPTY_ELEMENTDATApublic ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}// 帶Collection對象的構(gòu)造函數(shù)public ArrayList(Collection<? extends E> c) {// 將collection對象轉(zhuǎn)換成數(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ù)內(nèi)容存放到數(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 codeif (minCapacity - elementData.length > 0)grow(minCapacity);
}// 4、見下方解釋private void grow(int minCapacity) {// overflow-conscious codeint 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);
}
按照順序?qū)?yīng)解釋一下上面代碼的意思:
確保數(shù)組已使用長度(size)加1之后足夠存儲下一個數(shù)據(jù)。
確保添加的元素有地方存儲。在使用默認構(gòu)造函數(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)。
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ù)組操作,基本上看一遍就都能懂了,就不多說了。
總結(jié)
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable一、從ArrayList的定義上我們能看出來:
ArrayList 繼承了AbstractList,實現(xiàn)了List。它是一個數(shù)組隊列,提供了相關(guān)的添加、刪除、修改、遍歷等功能。
ArrayList 實現(xiàn)了RandmoAccess接口,即提供了隨機訪問功能。
ArrayList實現(xiàn)了Cloneable接口,即覆蓋了函數(shù)clone(),能被克隆。
ArrayList實現(xiàn)java.io.Serializable接口,這意味著ArrayList支持序列化,能通過序列化去傳輸。
二、方法操作總結(jié):
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),無容量的限制(會擴容)。
添加元素時可能要擴容(所以最好預(yù)判一下),刪除元素時不會減少容量(若希望減少容量,trimToSize()),刪除元素時,將刪除掉的位置元素置為null,下次gc就會回收這些元素所占的內(nèi)存空間。
線程不安全
add(int index, E element):添加元素到數(shù)組中指定位置的時候,需要將該位置及其后邊所有的元素都整塊向后復制一位,注意是從最后一位開始向后移動,即先將n-1 移動到n,再將n-2移動到n-1,以此類推到index。
get(int index):獲取指定位置上的元素時,可以通過索引直接獲取(O(1))。
remove(Object o)需要遍歷數(shù)組。
remove(int index)不需要遍歷數(shù)組,只需判斷index是否符合條件即可,效率比remove(Object o)高。
contains(E)需要遍歷數(shù)組。
使用iterator遍歷可能會引發(fā)多線程異常。
三、面試常問總結(jié):
ArrayList初始化大小是10。
ArrayList通過grow()方法擴容,每次擴容1.5倍。
ArrayList讀取查找數(shù)據(jù)效率高,修改刪除效率低。
ArrayList可以存放重復元素,也可以有null值。
和Vector不同,ArrayList中的操作不是線程安全的。所以,建議在單線程中才使用ArrayList,而在多線程中可以選擇Vector或者CopyOnWriteArrayList。
好文,點個在看吧
總結(jié)
以上是生活随笔為你收集整理的增加数组下标_数组以及ArrayList源码解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 全险包含车损险吗
- 下一篇: 中反应器体积_实验室规模半连续和连续生物