「吊打面試官」系列-ArrayList

文章來源:https://mp.weixin.qq.com/s/3HmDLST9ybJr75nMSbfEkg


前言

作為一個在互聯網公司面一次拿一次Offer的麵霸,打敗了無數競爭對手,每次都只能看到無數落寞的身影失望的離開,略感愧疚(請允許我使用一下誇張的修辭手法)。

於是在一個寂寞難耐的夜晚,我痛定思痛,決定開始寫互聯網技術棧面試相關的文章,希望能幫助各位讀者以後面試勢如破竹,對面試官進行360°的反擊,吊打問你的面試官,讓一同面試的同僚瞠目結舌,瘋狂收割大廠Offer!

所有文章的名字只是我的噱頭,我們應該有一顆謙遜的心,所以希望大家懷著空杯心態好好學,一起進步。


正文

一個婀娜多姿,穿著襯衣的小姐姐,拿著一個精緻的小筆記本,徑直走過來坐在我的面前。

看著眼前這個美麗的女人,心想這不會就是Java基礎系列的面試官吧,真香。

不過看樣子這麼年輕應該問不出什麼深度的吧,嘻嘻。(哦?是麼)

帥丙,上次面試到現在都過去2個星期你才過來,為啥鴿了這麼久?

美麗迷人的面試官您好,因為之前得了流感,差點就沒了,還有最近年底忙年會和年終review的事情,實在太忙了,不好意思。

還做了一波導演(其實是打雜)去拍攝蘑菇街的年會視頻了,實在忙到爆炸,週末也沒能懟出文章哈哈。


好吧那我理解了,下次這種情況記得提前跟我說,對了,多喝熱水。

面試官最後的多喝熱水,直接觸動我內心的防線,居然還有人這麼關心我,帥丙的眼角,又溼了……

ArrayList有用過嗎?它是一個什麼東西?可以用來幹嘛?

有用過,ArrayList就是數組列表,主要用來裝載數據,當我們裝載的是基本類型的數據int,long,boolean,short,byte…的時候我們只能存儲他們對應的包裝類,它的主要底層實現是數組Object[] elementData。

與它類似的是LinkedList,和LinkedList相比,它的查找和訪問元素的速度較快,但新增,刪除的速度較慢。

小結:ArrayList底層是用數組實現的存儲。

特點:查詢效率高,增刪效率低,線程不安全。使用頻率很高。


為啥線程 不安全還使用他呢?

因為我們正常使用的場景中,都是用來查詢,不會涉及太頻繁的增刪,如果涉及頻繁的增刪,可以使用LinkedList,如果你需要線程安全就使用Vector,這就是三者的區別了,實際開發過程中還是ArrayList使用最多的。

不存在一個集合工具是查詢效率又高,增刪效率也高的,還線程安全的,至於為啥大家看代碼就知道了,因為數據結構的特性就是優劣共存的,想找個平衡點很難,犧牲了性能,那就安全,犧牲了安全那就快速。

Tip:這裡還是強調下大家不要為了用而用,我記得我以前最開始工作就有這個毛病。就是不管三七二十一,就是為了用而用,別人用我也用,也不管他的性能啊,是否線程安全啥的,所幸最開始公司接觸的,都是線下的系統,而且沒啥併發。

現在回想起來還有點後怕,而且我第一次出去面試就被一個算法的大佬給虐得體無完膚,其實他問的我都會用,但是就是說不上來為啥用,為啥這麼做。

回想起一年前的第一次面試,我又想到了那時候的點滴,那時候我身邊還有女人,那時候我還有頭髮,那時候……我的眼角又溼了。

您說它的底層實現是數組,但是數組的大小是定長的,如果我們不斷的往裡面添加數據的話,不會有問題嗎?

ArrayList可以通過構造方法在初始化的時候指定底層數組的大小。

通過無參構造方法的方式ArrayList()初始化,則賦值底層數Object[] elementData為一個默認空數組Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}所以數組容量為0,只有真正對數據進行添加add時,才分配默認DEFAULT_CAPACITY = 10的初始容量。

大家可以分別看下他的無參構造器和有參構造器,無參就是默認大小,有參會判斷參數。

數組的長度是有限制的,而ArrayList是可以存放任意數量對象,長度不受限制,那麼他是怎麼實現的呢?

其實實現方式比較簡單,他就是通過數組擴容的方式去實現的。

就比如我們現在有一個長度為10的數組,現在我們要新增一個元素,發現已經滿了,那ArrayList會怎麼做呢?

第一步他會重新定義一個長度為10+10/2的數組也就是新增一個長度為15的數組。

然後把原數組的數據,原封不動的複製到新數組中,這個時候再把指向原數的地址換到新數組,ArrayList就這樣完成了一次改頭換面。

Tip:很多小夥伴說,你舉例幹嘛用10這麼長的數組舉例,這樣畫都要多畫一點格子了,帥丙我會做無意義的事情?因為我們在使用ArrayList的時候一般不會設置初始值的大小,那ArrayList默認的大小就剛好是10。

然後你們也可以看到,他的構造方法,如果你傳入了初始值大小,那就使用你傳入的參數,如果沒,那就使用默認的,一切都是有跡可循的。

ArrayList的默認數組大小為什麼是10?

其實我也沒找到具體原因。

據說是因為sun的程序員對一系列廣泛使用的程序代碼進行了調研,結果就是10這個長度的數組是最常用的最有效率的。也有說就是隨便起的一個數字,8個12個都沒什麼區別,只是因為10這個數組比較的圓滿而已。

我記得你說到了,他增刪很慢,你能說一下ArrayList在增刪的時候是怎麼做的麼?主要說一下他為啥慢。

誒臥*,這個我想一下,大學看的有點忘記了,我想想。

嗯嗯好的,我分別說一下他的新增的邏輯吧。

他有指定index新增,也有直接新增的,在這之前他會有一步校驗長度的判斷ensureCapacityInternal,就是說如果長度不夠,是需要擴容的。

在擴容的時候,老版本的jdk和8以後的版本是有區別的,8之後的效率更高了,採用了位運算,

右移一位,其實就是除以2這個操作。

指定位置新增的時候,在校驗之後的操作很簡單,就是數組的copy,大家可以看下代碼。

不知道大家看懂arraycopy的代碼沒有,我畫個圖解釋下,你可能就明白一點:

比如有下面這樣一個數組我需要在index 5的位置去新增一個元素A

那從代碼裡面我們可以看到,他複製了一個數組,是從index 5的位置開始的,然後把它放在了index 5+1的位置

給我們要新增的元素騰出了位置,然後在index的位置放入元素A就完成了新增的操作了

至於為啥說他效率低,我想我不說你也應該知道了,我這只是在一個這麼小的List裡面操作,要是我去一個幾百幾千幾萬大小的List新增一個元素,那就需要後面所有的元素都複製,然後如果再涉及到擴容啥的就更慢了不是嘛。

我問你個真實的場景,這個問題很少人知道,你可要好好回答喲!

ArrayList(int initialCapacity)會不會初始化數組大小?

這是什麼問題?臥槽問個ArrayList還能問到知識盲區?

不要慌,我記得丙丙說過:無論我們遇到什麼困難都不要害怕,戰勝恐懼最好的辦法就是面對他!!!奧利給!!!…

不會初始化數組大小!

而且將構造函數與initialCapacity結合使用,然後使用set()會拋出異常,儘管該數組已創建,但是大小設置不正確。

使用sureCapacity()也不起作用,因為它基於elementData數組而不是大小。

還有其他副作用,這是因為帶有sureCapacity()的靜態DEFAULT_CAPACITY。

進行此工作的唯一方法是在使用構造函數後,根據需要使用add()多次。

大家可能有點懵,我直接操作一下代碼,大家會發現我們雖然對ArrayList設置了初始大小,但是我們打印List大小的時候還是0,我們操作下標set值的時候也會報錯,數組下標越界。

再結合源碼,大家仔細品讀一下,這是Java Bug裡面的一個經典問題了,還是很有意思的,大家平時可能也不會注意這個點。

ArrayList插入刪除一定慢麼?

取決於你刪除的元素離數組末端有多遠,ArrayList拿來作為堆棧來用還是挺合適的,push和pop操作完全不涉及數據移動操作。

那他的刪除怎麼實現的呢?

刪除其實跟新增是一樣的,不過叫是叫刪除,但是在代碼裡面我們發現,他還是在copy一個數組。

為啥是copy數組呢?

繼續打個比方,我們現在要刪除下面這個數組中的index5這個位置

那代碼他就複製一個index5+1開始到最後的數組,然後把它放到index開始的位置

index5的位置就成功被”刪除“了其實就是被覆蓋了,給了你被刪除的感覺。

同理他的效率也低,因為數組如果很大的話,一樣需要複製和移動的位置就大了。

ArrayList是線程安全的麼?

當然不是,線程安全版本的數組容器是Vector。

Vector的實現很簡單,就是把所有的方法統統加上synchronized就完事了。

你也可以不使用Vector,用Collections.synchronizedList把一個普通ArrayList包裝成一個線程安全版本的數組容器也可以,原理同Vector是一樣的,就是給所有的方法套上一層synchronized。

ArrayList用來做隊列合適麼?

隊列一般是FIFO(先入先出)的,如果用ArrayList做隊列,就需要在數組尾部追加數據,數組頭部刪除數組,反過來也可以。

但是無論如何總會有一個操作會涉及到數組的數據搬遷,這個是比較耗費性能的。

結論:ArrayList不適合做隊列。

那數組適合用來做隊列麼?

這個女人是魔鬼麼?不過還是得微笑面對!

數組是非常合適的。

比如ArrayBlockingQueue內部實現就是一個環形隊列,它是一個定長隊列,內部是用一個定長數組來實現的。

另外著名的Disruptor開源Library也是用環形數組來實現的超高性能隊列,具體原理不做解釋,比較複雜。

簡單點說就是使用兩個偏移量來標記數組的讀位置和寫位置,如果超過長度就折回到數組開頭,前提是它們是定長數組。

ArrayList的遍歷和LinkedList遍歷性能比較如何?

論遍歷ArrayList要比LinkedList快得多,ArrayList遍歷最大的優勢在於內存的連續性,CPU的內部緩存結構會緩存連續的內存片段,可以大幅降低讀取內存的性能開銷。

能跟我聊一下LinkedList相關的東西麼?

可以呀,不然今天天色已晚,不然我們下次再聊?

好吧,每次你都找藉口,下次可以集合最後章節了,我們好好聊聊,你好好準備吧。

總結

ArrayList就是動態數組,用MSDN中的說法,就是Array的複雜版本,它提供了動態的增加和減少元素,實現了ICollection和IList接口,靈活的設置數組的大小等好處。

面試裡面問的時候沒HashMap,ConcurrentHashMap啥的這麼常問,但是也有一定概率問到的,還是那句話,不打沒把握的仗

我們在源碼閱讀過程中,不需要全部都讀懂,需要做的就是讀懂核心的源碼,加深自己對概念的理解就好了,用的時候不至於啥都不知道,不要為了用而用就好了。


ArrayList常用的方法總結

boolean add(E e)

將指定的元素添加到此列表的尾部。

void add(int index, E element)

將指定的元素插入此列表中的指定位置。

boolean addAll(Collection c)

按照指定 collection 的迭代器所返回的元素順序,將該 collection 中的所有元素添加到此列表的尾部。

boolean addAll(int index, Collection c)

從指定的位置開始,將指定 collection 中的所有元素插入到此列表中。

void clear()

移除此列表中的所有元素。

Object clone()

返回此 ArrayList 實例的淺表副本。

boolean contains(Object o)

如果此列表中包含指定的元素,則返回 true。

void ensureCapacity(int minCapacity)

如有必要,增加此 ArrayList 實例的容量,以確保它至少能夠容納最小容量參數所指定的元素數。

E get(int index)

返回此列表中指定位置上的元素。

int indexOf(Object o)

返回此列表中首次出現的指定元素的索引,或如果此列表不包含元素,則返回 -1。

boolean isEmpty()

如果此列表中沒有元素,則返回 true

int lastIndexOf(Object o)

返回此列表中最後一次出現的指定元素的索引,或如果此列表不包含索引,則返回 -1。

E remove(int index)

移除此列表中指定位置上的元素。

boolean remove(Object o)

移除此列表中首次出現的指定元素(如果存在)。

protected void removeRange(int fromIndex, int toIndex)

移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之間的所有元素。

E set(int index, E element)

用指定的元素替代此列表中指定位置上的元素。

int size()

返回此列表中的元素數。

Object[] toArray()

按適當順序(從第一個到最後一個元素)返回包含此列表中所有元素的數組。

T[] toArray(T[] a)

按適當順序(從第一個到最後一個元素)返回包含此列表中所有元素的數組;返回數組的運行時類型是指定數組的運行時類型。

void trimToSize()

將此 ArrayList 實例的容量調整為列表的當前大小。


我目前是在職Java開發,如果你現在正在瞭解Java技術,想要學好Java,渴望成為一名Java開發工程師,在入門學習Java的過程當中缺乏基礎的入門視頻教程,你可以關注並私信我:01。我這裡有一套最新的Java基礎JavaSE的精講視頻教程,這套視頻教程是我在年初的時候,根據市場技術棧需求錄製的,非常的系統完整。