【兩萬字】面試官:聽說你很懂集合原始碼,接我二十道問題!
問題一:看到這個圖,你會想到什麼?
(PS:截圖自《程式設計思想》)
答:
這個圖由Map
指向Collection
的Produces
並不是說Map
是Collection
的一個子類(子介面),這裡的意思是指Map
的KeySet
獲取到的一個檢視是Collection
的子介面。
我們可以看到集合有兩個基本介面:Map
和Collection
。但是我個人認為Map
並不能說是一個集合,稱之為對映或許更為合適,因為它的KeySet
檢視是一個Set
型別的鍵集,所以我們姑且把它也當做集合。
Collection
繼承了Iterator
介面,而Iterator
的作用是給我們提供一個只能向後遍歷集合元素的迭代器,也就是說所有實現Collection
Iterator
遍歷器去遍歷。
每種介面都有一個Abstract
開頭的抽象子類,這個子類中包括了一些預設的實現,我們在自定義類的時候都需要去繼承這個抽象類,然後根據我們不同的需求,對於其中的方法進行重寫。
從容器角度上來說,只有四種容器:Map
,Queue
,Set
,List
。
問題二:列出常見的集合,並進行簡單的介紹
答:
ArrayList: 一種可以動態增長和縮減的的索引序列 LinkedList:一種可以在任何位置進行高效地插入和刪除操作的有序序列 ArrayDeque:一種用迴圈陣列實現的雙端佇列 HashSet:一種沒有重複元素的無序集合 TreeSet:一種有序集 EnumSet:一種包含列舉型別值的集 LinkedHashSet:一種可以記住元素插入次序的集 PriorityQueue:一種允許高效刪除最小元素的集合 HashMap:一種儲存鍵/值關聯的資料結構 TreeMap:一種鍵值有序排列的對映表 EnumMap:一種鍵值屬於列舉型別的對映表 LinkedHashMap:一種可以記住鍵/值項新增次序的對映表 WeakHashMap:一種其值無用武之地後可以被垃圾回收期回收的對映表 IdentityHashMap:一種用==而不是用equals比較鍵值的對映表 Vector:目前使用較少,因為設計理念的陳舊和效能的問題被ArrayList所取代 Hashtable:執行緒非同步可以使用HashMap來替代,同步的話可以使用ConcurrentHashMap來替代
問題三:關於Iterator,聊聊你的看法
從鳥瞰圖中我們可以看到,所有實現Collection
的子類都繼承了Iterable
介面。這個介面提供了一個iterator()
方法可以構造一個Iterator
介面物件。然後我們可以使用這個迭代器物件依次訪問集合中的元素。
迭代器一般使用方法是這樣的:
Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while (iter.hasNext()) {
String s = iter.next();
System.out.println(s);
}
或者是這樣的:
//適用於JDK1.8以後的版本
iter.forEachRemaining(element -> System.out.println(element));
迭代器的next()
工作原理是這樣的:
迭代器是位於兩個集合元素之間的位置,當我們呼叫next()
方法的時候迭代器指標就會越過一個元素,並且返回剛剛越過的元素,所以,當我們迭代器的指標在最後一個元素的時候,就會丟擲會丟擲一個NoSuchElementException
的異常。所以,在呼叫next()
之前需要呼叫hasNext()
去判斷這個集合的迭代器是否走到了最後一個元素。
通過呼叫next()
方法可以逐個的去訪問集合中的每個元素,而訪問元素的順序跟該容器的資料結構有關,比如ArrayList
就是按照索引值開始,每次迭代都會使索引值加1,而對於HashSet這種資料結構是散列表的集合,就會按照某種隨機的次序出現。
Iterator
的介面中還有一個remove()
方法,這個方法實際上刪除的是上次呼叫next()方法返回的元素,下面我來展示一下remove()
方法的使用方法
Collection<String> c = ...;
Iterator<String> iter = c.iterator();
iter.next();
iter.remove();
這樣就可以刪除該集合中的第一個元素,但是需要注意一點,如果我們需要刪除兩個元素,必須這樣做:
iter.remove();
iter.next();
iter.remove();
而不能這麼做:
iter.remove();
iter.remove();
因為next()
方法和remove()
方法之間是有依賴性的,如果呼叫remove
之前沒有呼叫next
就會丟擲一個IllegalStateException
的異常。
問題四:對於Collection,你瞭解多少?
可以看出,作為頂級的框架,Collection
僅僅是繼承了Iterable
介面,接下來,我們來看一下Iterable
的原始碼,看看有什麼收穫。
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
可以看到這個介面中有三個方法,其中iterator()
方法可以給我們提供一個迭代器,這個在之前的教程就已經說過了,而forEach()
方法提供了一個函式式介面的引數,我們可以使用lambda
表示式結合來使用:
Collection<String> collection = ...;
collection.forEach(String s -> System.out.println(s));
這樣就可以獲取到每個值,它的底層實現是加強for
迴圈,實際上也是迭代器去遍歷,因為編譯器會把加強for
迴圈編譯為迭代遍歷。
Spliterator()
是1.8
新加的方法,字面意思可分割的迭代器,不同以往的iterator()
需要順序迭代,Spliterator()
可以分割為若干個小的迭代器進行並行操作,既可以實現多執行緒操作提高效率,又可以避免普通迭代器的fail-fast
(fail-fast
機制是java
集合中的一種錯誤機制。當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast
事件)機制所帶來的異常。Spliterator()
可以配合1.8
新加的Stream()
進行並行流的實現,大大提高處理效率。
![](http://viyoungblog.oss-cn-beijing.aliyuncs.com/viyoung/2020-06-27-023505.jpg)
Collection()
中提供了17個介面方法(除去了繼承自Object
的方法)。接下來,我們來了解一下這些方法的作用:
size()
,返回當前儲存在集合中的元素個數。isEmpty()
,如果集合中沒有元素,返回true。contains(Object obj)
,如果集合中包含了一個與obj相等的物件,返回true。iterator()
,返回這個集合的迭代器。toArray()
,返回這個集合的物件陣列toArray(T[] arrayToFill)
,返回這個集合的物件陣列,如果arrayToFill足夠大,就將集合中的元素填入這個陣列中。剩餘空間填補null;否則,分配一個新陣列,其成員型別與arrayToFill的成員型別相同,其長度等於集合的大小,並填充集合元素。add(Object element)
,將一個元素新增到集合中,如果由於這個呼叫改變了集合,返回true。remove(Object obj)
,從集合中刪除等於obj的物件,如果有匹配的物件被刪除,返回true。containsAll(Collection<?> other)
,如果這個集合包含other集合中的所有元素,返回true。addAll(Collection<? extends E> other)
,將other集合中的所有元素新增到這個集合,如果由於這個呼叫改變了集合,返回true。removeAll(Collection<?> other)
,從這個集合中刪除other集合中存在的所有元素。如果由於這個呼叫改變了集合,返回true。removeIf(Predicate<? super E> filter)
,從這個集合刪除filter返回true的所有元素,如果由於這個呼叫改變了集合,則返回true。retainAll(Collection<?> other)
,從這個集合中刪除所有與other集合中的元素不同的元素。如果由於這個呼叫改變了集合,返回true。clear()
,從這個集合中刪除所有的元素。spliterator()
,返回分割後的若干個小的迭代器。stream()
,返回這個集合對於的流物件。parallelStream()
,返回這個集合的並行流物件。
作為第一級的集合介面,Collection
提供了一些基礎操作的藉口,並且可以通過實現Iterable
介面獲取一個迭代器去遍歷獲取集合中的元素。
問題五:那麼AbstractCollection呢?
作為Collection
的抽象類實現,它的方法都是基於迭代器來完成的,這裡只貼出了原始碼中幾個需要特殊的注意的點,
![](http://viyoungblog.oss-cn-beijing.aliyuncs.com/viyoung/2020-06-27-024816.png)
TAG 1 :
陣列作為一個物件,需要一定的記憶體儲存物件頭資訊,物件頭資訊最大佔用記憶體不可超過8 byte。
TAG 2 :
finishToArray(T[] r, Iterator<?> it)
方法用於陣列擴容,當陣列索引指向最後一個元素+1時,對陣列進行擴容:即建立一個大小為(cap + cap/2 +1)的陣列,然後將原陣列的內容複製到新陣列中。擴容前需要先判斷是否陣列長度是否溢位。這裡的迭代器是從上層的方法(toArray(T[] t)
)傳過來的,並且這個迭代器已執行了一部分,而不是從頭開始迭代的
TAG 3 :
hugeCapacity(int minCapacity)
方法用來判斷該容器是否已經超過了該集合類預設的最大值即(Integer.MAX_VALUE -8
),一般我們用到這個方法的時候比較少,後面我們會在ArrayList
類的學習中,看到ArrayList
動態擴容用到了這個方法。
TAG 4 :
這裡的add(E)
方法預設丟擲了一個異常,這是因為如果我們想修改一個不可變的集合時,丟擲 UnsupportedOperationException
是正常的行為,比如當你用 Collections.unmodifiableXXX()
方法對某個集合進行處理後,再呼叫這個集合的修改方法(add
,remove
,set…
),都會報這個錯。因此 AbstractCollection.add(E)
丟擲這個錯誤是準從標準。
問題六: 能否詳細說一下toArray方法的實現?
高能預警:廢話不多說,直接上原始碼
/**
* 分配了一個等大空間的陣列,然後依次對陣列元素進行賦值
*/
public Object[] toArray() {
//新建等大的陣列
Object[] r = new Object[size()];
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
//判斷是否遍歷結束,以防多執行緒操作的時候集合變得更小
if (! it.hasNext())
return Arrays.copyOf(r, i);
r[i] = it.next();
}
//判斷是否遍歷未結束,以防多執行緒操作的時候集合變得更大,進行擴容
return it.hasNext() ? finishToArray(r, it) : r;
}
/**
* 泛型方法的`toArray(T[] a)`方法在處理裡,會先判斷引數陣列的大小,
* 如果空間足夠就使用引數作為元素儲存,如果不夠則新分配一個。
* 在迴圈中的判斷也是一樣,如果引數a能夠儲存則返回a,如果不能再新分配。
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
//當陣列a的長度大於等於a,直接將a賦予給r,否則使用反射API獲取一個長度為size的陣列
T[] r = a.length >= size ? a :
(T[])java.lang.reflect.Array
.newInstance(a.getClass().getComponentType(), size);
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
//判斷是否遍歷結束
if (! it.hasNext()) {
//如果 a == r,將r的每項值賦空,並將a返回
if (a == r) {
r[i] = null;
} else if (a.length < i) {
//如果a的長度小於r,直接呼叫Arrays.copyOf進行復制獲取一個新的陣列
return Arrays.copyOf(r, i);
} else {
System.arraycopy(r, 0, a, 0, i);
if (a.length > i) {
a[i] = null;
}
}
return a;
}
//如果遍歷結束,將迭代器獲取的值賦給r
r[i] = (T)it.next();
}
//判斷是否遍歷未結束,以防多執行緒操作的時候集合變得更大,進行擴容
return it.hasNext() ? finishToArray(r, it) : r;
}
/**
* 設定該容器的最大值
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 用於動態擴容
*/
@SuppressWarnings("unchecked")
private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
int i = r.length;
while (it.hasNext()) {
int cap = r.length;
if (i == cap) {
int newCap = cap + (cap >> 1) + 1;
if (newCap - MAX_ARRAY_SIZE > 0)
newCap = hugeCapacity(cap + 1);
r = Arrays.copyOf(r, newCap);
}
r[i++] = (T)it.next();
}
return (i == r.length) ? r : Arrays.copyOf(r, i);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError
("Required array size too large");
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
為了幫助瞭解,我把Arrays.copyOf(r.i)
的原始碼也貼出來:
//引數original代表你傳入的需要複製的泛型陣列,newLength複製得到陣列的大小
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
我們可以觀察到其中呼叫了System.arraycopy
方法,為了保持刨根問底的態度,我們又去翻看了這個方法的原始碼:
//src數組裡從索引為srcPos的元素開始, 複製到陣列dest裡的索引為destPos的位置, 複製的元素個數為length個.
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos,int length);
可以看到這個方式是由關鍵字native
修飾的方法,那麼native
修飾的方法有什麼含義呢?
native
關鍵字說明其修飾的方法是一個原生態方法,方法對應的實現不是在當前檔案,而是在用其他語言(如C和C++)實現的檔案中。Java語言本身不能對作業系統底層進行訪問和操作,但是可以通過JNI介面呼叫其他語言來實現對底層的訪問。
相關推薦
【兩萬字】面試官:聽說你很懂集合原始碼,接我二十道問題!
問題一:看到這個圖,你會想到什麼? (PS:截圖自《程式設計思想》) 答: 這個圖由Map指向Collection的Produces並不是說Map是Collection的一個子類(子介面),這裡的意思是指Map的KeySet獲取到的一個檢視是Collection的子介面。 我們可以看到集合有兩個基本介面:
【Java8新特性】面試官:談談Java8中的Stream API有哪些終止操作?
## 寫在前面 > 如果你出去面試,面試官問了你關於Java8 Stream API的一些問題,比如:Java8中建立Stream流有哪幾種方式?(可以參見:《[【Java8新特性】面試官問我:Java8中建立Stream流有哪幾種方式?](https://www.cnblogs.com/binghe
【Spring註解驅動開發】面試官:如何將Service注入到Servlet中?朋友又栽了!!
## 寫在前面 > 最近,一位讀者出去面試前準備了很久,信心滿滿的去面試。沒想到面試官的一個問題把他難住了。面試官的問題是這樣的:如何使用Spring將Service注入到Servlet中呢?這位讀者平時也是很努力的,看什麼原始碼啊、多執行緒啊、高併發啊、設計模式啊等等。沒想到卻在一個很簡單的問題上栽
【BAT面試題系列】面試官:你了解樂觀鎖和悲觀鎖嗎?
次數 catch val util overflow info 基本概念 因此 問題 前言 樂觀鎖和悲觀鎖問題,是出現頻率比較高的面試題。本文將由淺入深,逐步介紹它們的基本概念、實現方式(含實例)、適用場景,以及可能遇到的面試官追問,希望能夠幫助你打動面試官。 目錄
【搞定Jvm面試】 面試官:談談 JVM 類檔案結構的認識
類檔案結構 一 概述 在 Java 中,JVM 可以理解的程式碼就叫做位元組碼(即副檔名為 .class 的檔案),它不面向任何特定的處理器,只面向虛擬機器。Java 語言通過位元組碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以 Java 程式執行時比較高
【搞定Jvm面試】 面試官:談談 JVM 類載入過程是怎樣的?
類載入過程 Class 檔案需要載入到虛擬機器中之後才能執行和使用,那麼虛擬機器是如何載入這些 Class 檔案呢? 系統載入 Class 型別的檔案主要三步:載入->連線->初始化。連線過程又可分為三步:驗證->準備->解析。 載入 類載入過程的第一步,主要完成下面3件事情:
【原創】面試官:你回去等通知吧!
這是why技術的第37篇原創文章 老規矩,先聊聊生活,上面這張圖片是我週一拍的。 週一晚上下班後發現公司樓下推著三輪車賣花的阿姨又開始買花了。整個路口只有她一個人在做生意,整條路上也沒有幾個人,大家都低著頭匆匆走著,繁花中帶著點憂傷。 於是,我去買了一把白玫瑰。 上週日把《霍亂時期的愛情》看完了,就剛好當
【Java8新特性】面試官問我:Java8中建立Stream流有哪幾種方式?
## 寫在前面 > 先說點題外話:不少讀者工作幾年後,仍然在使用Java7之前版本的方法,對於Java8版本的新特性,甚至是Java7的新特性幾乎沒有接觸過。真心想對這些讀者說:你真的需要了解下Java8甚至以後版本的新特性了。 ># > 今天,一名讀者出去面試,面試官問他:說說Java8
【Nginx】面試官:給我講講Nginx如何實現四層負載均衡?
## 寫在前面 > 這次又被問到Nginx四層負載均衡的問題了,別慌,我們一起來細細分析這個看似簡單的問題。 > > 如果文章對你有點幫助,請關注 **冰河技術** 微信公眾號,點贊、在看、留言和轉發,大家的四連是我持續創作的最大動力。 負載均衡可以分為靜態負載均衡和動態負載均衡,接下來
【高併發】面試官:講講什麼是快取穿透?擊穿?雪崩?如何解決?
## 寫在前面 > 在前面的《[【高併發】Redis如何助力高併發秒殺系統?看完這篇我徹底懂了!!](https://mp.weixin.qq.com/s?__biz=Mzg3MzE1NTIzNA==&mid=2247487271&idx=1&sn=6bd9f4627357b1
【高併發】面試官:Java中提供了synchronized,為什麼還要提供Lock呢?
## 寫在前面 > 在Java中提供了synchronized關鍵字來保證只有一個執行緒能夠訪問同步程式碼塊。既然已經提供了synchronized關鍵字,那為何在Java的SDK包中,還會提供Lock介面呢?這是不是重複造輪子,多此一舉呢?今天,我們就一起來探討下這個問題。 ## 再造輪子? 既
【高併發】面試官:效能優化有哪些衡量指標?需要注意什麼?
## 寫在前面 > 最近,很多小夥伴都在說,我沒做過效能優化的工作,在公司只是做些CRUD的工作,接觸不到效能優化相關的工作。現在出去找工作面試的時候,面試官總是問些很刁鑽的問題來為難我,很多我都不會啊!那怎麼辦呢?那我就專門寫一些與高併發系統相關的面試容易問到的問題吧。今天,我們就來說說在高併發場景
【效能優化】面試官:Java中的物件都是在堆上分配的嗎?
## 寫在前面 > 從開始學習Java的時候,我們就接觸了這樣一種觀點:Java中的物件是在堆上建立的,物件的引用是放在棧裡的,那這個觀點就真的是正確的嗎?如果是正確的,那麼,面試官為啥會問:“Java中的物件就一定是在堆上分配的嗎?”這個問題呢?看來,我們從接觸Java就被灌輸的這個觀點值得我們懷疑
【高併發】面試官:講講高併發場景下如何優化加鎖方式?
## 寫在前面 > 很多時候,我們在併發程式設計中,涉及到加鎖操作時,對程式碼塊的加鎖操作真的合理嗎?還有沒有需要優化的地方呢? ## 前言 在《[【高併發】優化加鎖方式時竟然死鎖了!!](https://mp.weixin.qq.com/s?__biz=Mzg3MzE1NTIzNA==&
【面經】面試官:做過效能優化的工作嗎?你會從哪些方面入手做效能優化呢?
## 寫在前面 > 隨著網際網路的高速發展,網際網路行業已經從IT時代慢慢步入到DT時代。對於Java程式設計師的要求越來越高,只是單純的掌握CRUD以不足以勝任網際網路公司的相關職位,大量招聘崗位顯示:如果是面試中高階的Java崗,基本上都需要懂效能優化的相關知識。今天,我們就一起來聊聊一個經典的面
【面經】面試官:如何以最高的效率從MySQL中隨機查詢一條記錄?
## 寫在前面 > MySQL資料庫在網際網路行業使用的比較多,有些小夥伴可能會認為MySQL資料庫比較小,儲存不了很多的資料。其實,這些小夥伴是真的不瞭解MySQL。MySQL的小不是說使用MySQL儲存的資料少,而是說其體積小,比較輕量。使用MySQL完全可以儲存千億級別的資料,這個我會在後面的文
【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(乾貨 | 建議珍藏)
是資料結構而非型別 很多文章都會說,redis支援5種常用的資料型別,這其實是存在很大的歧義。redis裡存的都
【魯班學院】面試總結:Java高階篇(上):集合的型別以及重新認識HashMap
1.你用過哪些集合類? 大公司最喜歡問的Java集合類面試題 4
【第四組】典型場景:查看導入的圖片,工作序號:001,2017/7/6
想要 新的 掃描 app 場景 照片 工作 背景 一個地方 場景 工作項序號001:查看導入的圖片,最後修改時間:2017/7/6 1. 背景 1) 典型用戶:羅小歐[主要]、朱小葉[主要] 2) 用戶的需求/迫切需要解決的問題 a. 羅小歐:出去玩拍了好多照片,想要在一個
面試官:“看你簡歷上寫熟悉 Handler 機制,那聊聊 IdleHandler 吧?”
一. 序 Handler 機制算是 Android 基本功,面試常客。但現在面試,多數已經不會直接讓你講講 Handler 的機制,Looper 是如何迴圈的,MessageQueue 是如何管理 Message 等,而是基於場景去提問,看看你對 Handler 機制的掌握是否紮實。 本文就來聊聊 H