做者:小傅哥
博客:https://bugstack.cnhtml
沉澱、分享、成長,讓本身和他人都能有所收穫!😄
數據結構是寫好代碼的基礎!
java
說到數據結構基本包括;數組、鏈表、隊列、紅黑樹等,但當你看到這些數據結構以及想到本身平時的開發,彷佛並無用到過。那麼爲何還要學習數據結構?程序員
其實這些知識點你並非沒有用到的,而是Java中的API已經將各個數據結構封裝成對應的工具類,例如ArrayList、LinkedList、HashMap等,就像在前面的章節中,小傅哥寫了5篇文章將近2萬字來分析HashMap,從而學習它的核心設計邏輯。面試
可能有人以爲這類知識就像八股文,學習只是爲了應付面試。若是你真的把這些用於支撐其整個語言的根基當八股文學習,那麼硬背下來不會有多少收穫。理科學習更在意邏輯,重在是理解基本原理,有了原理基礎就複用這樣的技術運用到實際的業務開發。設計模式
那麼,你何時會用到這樣的技術呢?就是,當你考慮體量、夯實服務、琢磨性能時,就會逐漸的深刻到數據結構以及核心的基本原理當中,那裏的每一分深刻,都會讓整個服務性能成指數的提高。數組
謝飛機,據說你最近在家很努力學習HashMap?那考你個ArrayList知識點🦀數據結構
你看下面這段代碼輸出結果是什麼?app
public static void main(String[] args) { List<String> list = new ArrayList<String>(10); list.add(2, "1"); System.out.println(list.get(0)); }
嗯?不知道!👀眼睛看題,看我臉幹啥?好好好,告訴你吧,這樣會報錯!至於爲何,回家看看書吧。dom
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 2, Size: 0 at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:665) at java.util.ArrayList.add(ArrayList.java:477) at org.itstack.interview.test.ApiTest.main(ApiTest.java:13) Process finished with exit code 1
🤭謝飛機是懵了,我們一點點分析ArrayList函數
Array + List = 數組 + 列表 = ArrayList = 數組列表
ArrayList的數據結構是基於數組實現的,只不過這個數組不像咱們普通定義的數組,它能夠在ArrayList的管理下插入數據時按需動態擴容、數據拷貝等操做。
接下來,咱們就逐步分析ArrayList的源碼,也同時解答謝飛機
的疑問。
List<String> list = new ArrayList<String>(10); public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */ 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); } }
EMPTY_ELEMENTDATA
是一個定義好的空對象;private static final Object[] EMPTY_ELEMENTDATA = {}
ArrayList<String> list = new ArrayList<String>(); list.add("aaa"); list.add("bbb"); list.add("ccc");
ArrayList<String> list = new ArrayList<String>() \\{ add("aaa"); add("bbb"); add("ccc"); \\};
ArrayList<String> list = new ArrayList<String>(Arrays.asList("aaa", "bbb", "ccc"));
以上是經過Arrays.asList
傳遞給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; } }
Collection
類的均可以做爲入參。Arrays.copyOf
到Object[]
集合中在賦值給屬性elementData
。注意:c.toArray might (incorrectly) not return Object[] (see 6260652
)
see 6260652 是JDK bug庫的編號,有點像商品sku,bug地址:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652
那這是個什麼bug呢,咱們來測試下面這段代碼;
@Test public void t(){ List<Integer> list1 = Arrays.asList(1, 2, 3); System.out.println("經過數組轉換:" + (list1.toArray().getClass() == Object[].class)); ArrayList<Integer> list2 = new ArrayList<Integer>(Arrays.asList(1, 2, 3)); System.out.println("經過集合轉換:" + (list2.toArray().getClass() == Object[].class)); }
測試結果:
經過數組轉換:false 經過集合轉換:true Process finished with exit code 0
public Object[] toArray()
返回的類型不必定就是 Object[]
,其類型取決於其返回的實際類型,畢竟 Object 是父類,它能夠是其餘任意類型。形成這個結果的緣由,以下;
Arrays.copyOf(this.a, size,(Class<? extends T[]>) a.getClass());
Arrays.copyOf(elementData, size, Object[].class);
你知道嗎?
那這到底爲何呢,由於Arrays.asList構建出來的List與new ArrayList獲得的List,壓根就不是一個List!類關係圖以下;
從以上的類圖關係能夠看到;
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable
此外,Arrays是一個工具包,裏面還有一些很是好用的方法,例如;二分查找Arrays.binarySearch
、排序Arrays.sort
等
Collections.nCopies
是集合方法中用於生成多少份某個指定元素的方法,接下來就用它來初始化ArrayList,以下;
ArrayList<Integer> list = new ArrayList<Integer>(Collections.nCopies(10, 0));
ArrayList對元素的插入,其實就是對數組的操做,只不過須要特定時候擴容。
List<String> list = new ArrayList<String>(); list.add("aaa"); list.add("bbb"); list.add("ccc");
當咱們依次插入添加元素時,ArrayList.add方法只是把元素記錄到數組的各個位置上了,源碼以下;
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
size++
自增,把對應元素添加進去。在前面初始化
部分講到,ArrayList默認初始化時會申請10個長度的空間,若是超過這個長度則須要進行擴容,那麼它是怎麼擴容的呢?
從根本上分析來講,數組是定長的,若是超過原來定長長度,擴容則須要申請新的數組長度,並把原數組元素拷貝到新數組中,以下圖;
圖中介紹了當List結合可用空間長度不足時則須要擴容,這主要包括以下步驟;
ensureCapacityInternal(size + 1);
grow(int minCapacity)
擴容的長度計算;int newCapacity = oldCapacity + (oldCapacity >> 1);
,舊容量 + 舊容量右移1位,這至關於擴容了原來容量的(int)3/2
。
Arrays.copyOf(elementData, newCapacity);
,但他的底層用到的是;System.arraycopy
System.arraycopy;
@Test public void test_arraycopy() { int[] oldArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int[] newArr = new int[oldArr.length + (oldArr.length >> 1)]; System.arraycopy(oldArr, 0, newArr, 0, oldArr.length); newArr[11] = 11; newArr[12] = 12; newArr[13] = 13; newArr[14] = 14; System.out.println("數組元素:" + JSON.toJSONString(newArr)); System.out.println("數組長度:" + newArr.length); /** * 測試結果 * * 數組元素:[1,2,3,4,5,6,7,8,9,10,0,11,12,13,14] * 數組長度:15 */ }
System.arraycopy
的操做。oldArr
拷貝到newArr
,同時新數組的長度,採用和ArrayList同樣的計算邏輯;oldArr.length + (oldArr.length >> 1)
list.add(2, "1");
到這,終於能夠說說謝飛機
的面試題,這段代碼輸出結果是什麼,以下;
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 2, Size: 0 at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:665) at java.util.ArrayList.add(ArrayList.java:477) at org.itstack.interview.test.ApiTest.main(ApiTest.java:14)
其實,一段報錯提示,爲何呢?咱們翻開下源碼學習下。
public void add(int index, E element) { rangeCheckForAdd(index); ... } private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
rangeCheckForAdd
,size的長度。size++
。IndexOutOfBoundsException
異常。
指定位置插入的核心步驟包括;
ensureCapacityInternal(size + 1);
。部分源碼:
public void add(int index, E element) { ... // 判斷是否須要擴容以及擴容操做 ensureCapacityInternal(size + 1); // 數據拷貝遷移,把待插入位置空出來 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 數據插入操做 elementData[index] = element; size++; }
System.arraycopy
,上面咱們已經演示過相應的操做方式。實踐:
List<String> list = new ArrayList<String>(Collections.nCopies(9, "a")); System.out.println("初始化:" + list); list.add(2, "b"); System.out.println("插入後:" + list);
測試結果:
初始化:[a, a, a, a, a, a, a, a, a] 插入後:[a, a, 1, a, a, a, a, a, a, a] Process finished with exit code 0
1
,後面的數據向後遷移完成。有了指定位置插入元素的經驗,理解刪除的過長就比較容易了,以下圖;
這裏咱們結合着代碼:
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; }
刪除的過程主要包括;
rangeCheck(index);
numMoved
,並經過System.arraycopy
本身把元素複製給本身。這裏咱們作個例子:
@Test public void test_copy_remove() { int[] oldArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int index = 2; int numMoved = 10 - index - 1; System.arraycopy(oldArr, index + 1, oldArr, index, numMoved); System.out.println("數組元素:" + JSON.toJSONString(oldArr)); }
測試結果:
數組元素:[1,2,4,5,6,7,8,9,10,10] Process finished with exit code 0
元素4
佔據了原來元素3
的位置,同時結尾的10還等待刪除。這就是爲何ArrayList中有這麼一句代碼;elementData[--size] = null若是給你一組元素;a、b、c、d、e、f、g
,須要你放到ArrayList中,可是要求獲取一個元素的時間複雜度都是O(1),你怎麼處理?
想解決這個問題,就須要知道元素添加到集合中後知道它的位置,而這個位置呢,其實能夠經過哈希值與集合長度與運算,得出存放數據的下標,以下圖;
List<String> list = new ArrayList<String>(Collections.<String>nCopies(8, "0")); list.set("a".hashCode() & 8 - 1, "a"); list.set("b".hashCode() & 8 - 1, "b"); list.set("c".hashCode() & 8 - 1, "c"); list.set("d".hashCode() & 8 - 1, "d"); list.set("e".hashCode() & 8 - 1, "e"); list.set("f".hashCode() & 8 - 1, "f"); list.set("g".hashCode() & 8 - 1, "g");
ArrayList
,並存放相應的元素。存放時候計算出每一個元素的下標值。System.out.println("元素集合:" + list); System.out.println("獲取元素f [\"f\".hashCode() & 8 - 1)] Idx:" + ("f".hashCode() & (8 - 1)) + " 元素:" + list.get("f".hashCode() & 8 - 1)); System.out.println("獲取元素e [\"e\".hashCode() & 8 - 1)] Idx:" + ("e".hashCode() & (8 - 1)) + " 元素:" + list.get("e".hashCode() & 8 - 1)); System.out.println("獲取元素d [\"d\".hashCode() & 8 - 1)] Idx:" + ("d".hashCode() & (8 - 1)) + " 元素:" + list.get("d".hashCode() & 8 - 1));
元素集合:[0, a, b, c, d, e, f, g] 獲取元素f ["f".hashCode() & 8 - 1)] Idx:6 元素:f 獲取元素e ["e".hashCode() & 8 - 1)] Idx:5 元素:e 獲取元素d ["d".hashCode() & 8 - 1)] Idx:4 元素:d Process finished with exit code 0
HashMap
中的桶結構。