1. 程式人生 > >【兩萬字】面試官:聽說你很懂集合原始碼,接我二十道問題!

【兩萬字】面試官:聽說你很懂集合原始碼,接我二十道問題!

問題一:看到這個圖,你會想到什麼?

(PS:截圖自《程式設計思想》)

答:

這個圖由Map指向CollectionProduces並不是說MapCollection的一個子類(子介面),這裡的意思是指MapKeySet獲取到的一個檢視是Collection的子介面。

我們可以看到集合有兩個基本介面:MapCollection。但是我個人認為Map並不能說是一個集合,稱之為對映或許更為合適,因為它的KeySet檢視是一個Set型別的鍵集,所以我們姑且把它也當做集合。

Collection繼承了Iterator介面,而Iterator的作用是給我們提供一個只能向後遍歷集合元素的迭代器,也就是說所有實現Collection

的類都可以使用Iterator遍歷器去遍歷。

每種介面都有一個Abstract開頭的抽象子類,這個子類中包括了一些預設的實現,我們在自定義類的時候都需要去繼承這個抽象類,然後根據我們不同的需求,對於其中的方法進行重寫。

從容器角度上來說,只有四種容器:MapQueueSetList

問題二:列出常見的集合,並進行簡單的介紹

答:

  1. ArrayList: 一種可以動態增長和縮減的的索引序列
  2. LinkedList:一種可以在任何位置進行高效地插入和刪除操作的有序序列
  3. ArrayDeque:一種用迴圈陣列實現的雙端佇列
  4. HashSet:一種沒有重複元素的無序集合
  5. TreeSet:一種有序集
  6. EnumSet:一種包含列舉型別值的集
  7. LinkedHashSet:一種可以記住元素插入次序的集
  8. PriorityQueue:一種允許高效刪除最小元素的集合
  9. HashMap:一種儲存鍵/值關聯的資料結構
  10. TreeMap:一種鍵值有序排列的對映表
  11. EnumMap:一種鍵值屬於列舉型別的對映表
  12. LinkedHashMap:一種可以記住鍵/值項新增次序的對映表
  13. WeakHashMap:一種其值無用武之地後可以被垃圾回收期回收的對映表
  14. IdentityHashMap:一種用==而不是用equals比較鍵值的對映表
  15. Vector:目前使用較少,因為設計理念的陳舊和效能的問題被ArrayList所取代
  16. 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()進行並行流的實現,大大提高處理效率。

Collection()中提供了17個介面方法(除去了繼承自Object的方法)。接下來,我們來了解一下這些方法的作用:

  1. size(),返回當前儲存在集合中的元素個數。
  2. isEmpty(),如果集合中沒有元素,返回true。
  3. contains(Object obj),如果集合中包含了一個與obj相等的物件,返回true。
  4. iterator(),返回這個集合的迭代器。
  5. toArray(),返回這個集合的物件陣列
  6. toArray(T[] arrayToFill),返回這個集合的物件陣列,如果arrayToFill足夠大,就將集合中的元素填入這個陣列中。剩餘空間填補null;否則,分配一個新陣列,其成員型別與arrayToFill的成員型別相同,其長度等於集合的大小,並填充集合元素。
  7. add(Object element),將一個元素新增到集合中,如果由於這個呼叫改變了集合,返回true。
  8. remove(Object obj),從集合中刪除等於obj的物件,如果有匹配的物件被刪除,返回true。
  9. containsAll(Collection<?> other),如果這個集合包含other集合中的所有元素,返回true。
  10. addAll(Collection<? extends E> other),將other集合中的所有元素新增到這個集合,如果由於這個呼叫改變了集合,返回true。
  11. removeAll(Collection<?> other),從這個集合中刪除other集合中存在的所有元素。如果由於這個呼叫改變了集合,返回true。
  12. removeIf(Predicate<? super E> filter),從這個集合刪除filter返回true的所有元素,如果由於這個呼叫改變了集合,則返回true。
  13. retainAll(Collection<?> other),從這個集合中刪除所有與other集合中的元素不同的元素。如果由於這個呼叫改變了集合,返回true。
  14. clear(),從這個集合中刪除所有的元素。
  15. spliterator(),返回分割後的若干個小的迭代器。
  16. stream(),返回這個集合對於的流物件。
  17. parallelStream(),返回這個集合的並行流物件。

作為第一級的集合介面,Collection提供了一些基礎操作的藉口,並且可以通過實現Iterable介面獲取一個迭代器去遍歷獲取集合中的元素。

問題五:那麼AbstractCollection呢?

作為Collection的抽象類實現,它的方法都是基於迭代器來完成的,這裡只貼出了原始碼中幾個需要特殊的注意的點,

image-20200627104816442

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

第四組典型場景查看導入的圖片工作序號0012017/7/6

想要 新的 掃描 app 場景 照片 工作 背景 一個地方 場景 工作項序號001:查看導入的圖片,最後修改時間:2017/7/6 1. 背景 1) 典型用戶:羅小歐[主要]、朱小葉[主要] 2) 用戶的需求/迫切需要解決的問題 a. 羅小歐:出去玩拍了好多照片,想要在一個

面試“看簡歷上寫熟悉 Handler 機制那聊聊 IdleHandler 吧?”

一. 序 Handler 機制算是 Android 基本功,面試常客。但現在面試,多數已經不會直接讓你講講 Handler 的機制,Looper 是如何迴圈的,MessageQueue 是如何管理 Message 等,而是基於場景去提問,看看你對 Handler 機制的掌握是否紮實。 本文就來聊聊 H