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

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


(PS:截圖自《編程思想》)java

程序員

這個圖由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接口對象。而後咱們可使用這個迭代器對象依次訪問集合中的元素。併發

迭代器通常使用方法是這樣的:app

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接口調用其餘語言來實現對底層的訪問。

JNI是Java本機接口(Java Native Interface),是一個本機編程接口,它是Java軟件開發工具箱(java Software Development Kit,SDK)的一部分。JNI容許Java代碼使用以其餘語言編寫的代碼和代碼庫。Invocation API(JNI的一部分)能夠用來將Java虛擬機(JVM)嵌入到本機應用程序中,從而容許程序員從本機代碼內部調用Java代碼。

而後咱們來分析toArray()中須要注意的點,經過原源碼中的英文註解,toArray獲得的數組跟原collection沒有任何關係,咱們能夠對數組的每一個引用值作修改,而不會影響到原collection.這個看起來好像是多餘說明的,可是考慮到ArrayList其實就是基於數組實現的,那這個限制保證了即便是將ArrayList轉化爲數組,那也應該是分配一個新數組,而不是返回原來的數組。

若是咱們在單線程操做的狀況下,collection集合大小不變,正常應該是執行到 return it.hasNext() ? finishToArray(r, it) : r; 這條語句結束,但考慮到在複製的過程當中,collection的集合可能會有變化,多是變大也多是變小,因此方法增長了對這種狀況的處理,這就是爲何每次循環都要判斷是collection是否遍歷完,以及最後再判斷collection是否變得更長,若是是的話,還須要從新再爲array分配空間。

一般狀況下,咱們不會執行到hugeCapacity,但做爲一個框架來講,這體現了設計時的嚴謹。

問題七:用的最多的集合之一——List,說說你對它的理解

List是繼承自Collection的一個子接口,它提供了一個有序的集合,在這個集合中咱們可使用索引去獲取集合中的值,同時,咱們也能夠經過迭代器去訪問集合中的元素,第一種方法被稱爲隨機訪問,由於咱們能夠按照任意的順序去訪問元素,而使用迭代器就必須順序的去訪問元素。

相比於它的父接口Collection,並無發生很大的改動,可是因爲List是一個有序的集合,因此提供了一些基於索引進行的操做:

get(int index):獲取該集合中索引等於index的元素

set(int index, E element):將該集合中索引等於index的元素賦值爲element

add(int index, E element):在集合中索引等於index的位置將element插入,並將當前處於該位置的元素及其後續元素的索引加1。

remove(int index):刪除指定索引(index)位置的元素,並將處於該位置後面的元素索引減1

indexOf(Object o):獲取對象o在集合中的索引

lastIndexOf(Object o):獲取對象o在集合中最後一次出現的索引值,若是集合中不存在這個對象,返回-1。

同時,提供了一個Iterator的子接口ListIterator,基於這個迭代器,咱們實現了兩個默認方法replaceAll(UnaryOperator<E> operator)sort(Comparator<? super E> c)

replaceAll(UnaryOperator<E> operator)這裏和String類中replaceAll()方法並不相同,這裏的接收參數是一個函數式接口,咱們來看一下這個函數式接口的源碼:

package java.util.function;

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

用法以下:

List<String> strList = new ArrayList<>();
strList.add("Hungary");
strList.add("Foolish");

strList.replaceAll(t -> "Stay " + t);
strList.forEach(s -> System.out.println(s));

打印結果爲

Stay Hungary Stay Foolish

sort(Comparator<? super E> c)傳入的一樣是一個函數式接口,咱們能夠自定義排序規則後,調用這個方法進行排序:

List<Human> humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12));
 
humans.sort((Human h1, Human h2) -> h1.getName().compareTo(h2.getName()))

這裏是Arrays.sort的源碼,能夠看到使用了歸併算法和TimSort算法來進行排序。

public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
        sort(a);
    } else {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, c);
        else
            TimSort.sort(a, 0, a.length, c, null, 0, 0);
    }
}

問題八:剛剛你說到了ListIterator,能夠詳細說一下嘛

前面咱們已經提過,ListIterator做爲Iterator的子接口,給有序的集合List提供了一個鏈表結構下的迭代器,接下來,咱們來看一下ListIterator的源碼:

Iterator不一樣的是,ListIterator新增了一些基於鏈表數據結構的操做以及能夠用來反向遍歷鏈表的方法:

hasPrevious():當反向迭代列表時,還有可供訪問的元素,返回true

previous():返回前一個對象,若是已經到達了列表的頭部,拋出一個NoSuchElementException異常

nextIndex():返回下一次調用next方法將返回的元素索引

previousIndex():返回下一次調用previous方法將返回的元素索引

add(E newElement):在當前位置前添加一個元素。

set(E newElement):用新元素取代next或previous上次訪問的元素。若是在next或previous上次調用以後列表結構被修改了,將拋出一個IllegalStateException異常。

問題九:說說AbstractList

AbstractList是實現List接口的一個抽象類,它的地位之與List相似於AbstractCollection之與Collection,同事,AbstractList繼承了AbstractCollection,並針對List接口給出了一些默認的實現。並且它是針對隨機訪問儲存數據的方式的,若是須要使用順序訪問儲存數據方式,還有一個AbstractSequentialListAbstractList的子類,順序訪問時應該優先使用它。

接下來,咱們來看一下AbstractList的源碼,看看他針對於List接口相較於AbstractCollection給出了哪些不一樣的實現方法。

AbstractList的源碼在結構上分爲了兩種內部迭代器,兩種內部類以及AbstractList自己的代碼,它的一些實現都是基於內部類和內部的兩種迭代器:ItrListItr來完成的,下面是部分源碼的解析(因爲篇幅緣由,不能放上所有,只能拋磚引玉,寫一部分)

//因爲該集合是不可變的,因此一切可能會改變集合元素的操做都會拋出一個UnsupportedOperationException()
    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }

    //獲取某個元素在集合中的索引
    public int indexOf(Object o) {
        //這裏是由AbstractList內部已經提供了Iterator, ListIterator迭代器的實現類,分別爲Itr,ListItr。這裏是調用了一個實例化ListItr的方法
        ListIterator<E> it = listIterator();
        if (o == null) {
            while (it.hasNext())
                if (it.next()==null)
                    return it.previousIndex();
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return it.previousIndex();
        }
        //若是集合中不存在該元素,返回-1
        return -1;
    }

    /**
     *    內部實現了Iterator接口的實現類Itr
     */
    private class Itr implements Iterator<E> {
       
        //光標位置
        int cursor = 0;
        
        //上一次迭代到的元素的光標位置,若是是末尾會置爲-1
        int lastRet = -1;
    
        //併發標誌,若是兩個值不一致,說明發生了併發操做,就會報錯
        int expectedModCount = modCount;

        //刪除上一次迭代器越過的元素
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                   //調用須要子類去實現的remove方法
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                //每次刪除後,將lastRet置爲-1,防止連續的刪除
                lastRet = -1;
                //將修改次數賦給迭代器對對象的結構修改次數這個會在下面進行詳解
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                //若是出現索引越界,說明發生了併發的操做致使,因此拋出一個併發操做異常。
                throw new ConcurrentModificationException();
            }
        }

        //判斷是否發生了併發操做
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

    //繼承自Itr的ListIterator的實現類ListItr
    private class ListItr extends Itr implements ListIterator<E> {

        //獲取上一位的元素,這裏在後面會有畫圖幫助理解
        public E previous() {
            checkForComodification();
            try {
                //這裏和父類的寫法略有不一樣,先將光標的位置進行減一
                int i = cursor - 1;
                E previous = get(i);
                //由於須要返回的是前一位的元素,因此這裏的光標值和上一次迭代到的光標的位置其實是同樣的
                lastRet = cursor = i;
                return previous;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        //設置元素
        public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                //默認設置的位置是上一次迭代器越過的元素
                AbstractList.this.set(lastRet, e);
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        
        //添加元素
        public void add(E e) {
            checkForComodification();
            try {
                //設置添加的位置爲當前光標所在的位置
                int i = cursor;
                AbstractList.this.add(i, e);
                //這裏講lastRet設置爲-1,即添加的元素不容許當即刪除
                lastRet = -1;
                //添加後,將光標移到
                cursor = i + 1;
                //迭代器併發標誌和集合併發標誌統一
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                //若是出現了索引越界,說明發生了併發操做
                throw new ConcurrentModificationException();
            }
        }
    }

    //切取子List
    public List<E> subList(int fromIndex, int toIndex) {
        //是否支持隨機訪問
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<>(this, fromIndex, toIndex) :
                new SubList<>(this, fromIndex, toIndex));
    }

    //使用迭代器成段刪除集合中的元素
    protected void removeRange(int fromIndex, int toIndex) {
        ListIterator<E> it = listIterator(fromIndex);
        for (int i=0, n=toIndex-fromIndex; i<n; i++) {
            it.next();
            it.remove();
        }
    }
}

//繼承自AbstractList的內部類SubList,表明了它父類的一部分
class SubList<E> extends AbstractList<E> {
   
    private final AbstractList<E> l;
    private final int offset;
    private int size;

    //根據父類來構造一個SubList
    SubList(AbstractList<E> list, int fromIndex, int toIndex) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > list.size())
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
        l = list;
        offset = fromIndex;
        size = toIndex - fromIndex;
        //修改次數(併發標誌)和父類保持一致
        this.modCount = l.modCount;
    }

    //實際上仍是調用的父類的set方法和get方法
    public E set(int index, E element) {
        rangeCheck(index);
        checkForComodification();
        return l.set(index+offset, element);
    }

    public void add(int index, E element) {
        rangeCheckForAdd(index);
        checkForComodification();
        //實際上仍是在父類上進行添加
        l.add(index+offset, element);
        this.modCount = l.modCount;
        //而後把size + 1
        size++;
    }
}

//相較於SubList內部類,多了一個是否能夠隨機訪問的標誌
class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
    RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
        super(list, fromIndex, toIndex);
    }

    public List<E> subList(int fromIndex, int toIndex) {
        return new RandomAccessSubList<>(this, fromIndex, toIndex);
    }
}

問題十:索引和遊標的關係

在這裏插入圖片描述這裏我畫了一個圖,而後對照着這個圖,咱們再來看一下ListItr中的一些代碼:

//下一位的索引值等於光標值
        public int nextIndex() {
            return cursor;
        }

        //上一位的索引值等於光標值減一
        public int previousIndex() {
            //其實這裏並不理解,爲啥不去檢查索引越界。。
            return cursor-1;
        }

假定迭代器如今運行到1所在的位置,能夠很容易的看出當迭代器處於這個位置的時候,去調用nextIndex()方法獲得的是1,而調用previousIndex獲得的就是0。這是徹底符合咱們的邏輯的,接下來,咱們再來看previous()方法的源碼:

//獲取上一位的元素,這裏在後面會有畫圖幫助理解
public E previous() {
    checkForComodification();
    try {
        //這裏和父類的寫法略有不一樣,先將光標的位置進行減一
        int i = cursor - 1;
        E previous = get(i);
        //由於須要返回的是前一位的元素,因此這裏的光標值和上一次迭代到的光標的位置其實是同樣的
        lastRet = cursor = i;
        return previous;
    } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

其實這裏我在分析的時候是存在疑問的(爲何這裏的lastRet等於cursor,而Itr中的next()方法的實現中cursor實際上等於lastRet - 1),在畫完圖分析索引和遊標的關係以後又來看一遍才恍然大悟,

這裏的lastRet表明的是上一次迭代到的元素的光標位置,因此,咱們來舉個例子,當迭代器在4的位置的時候,使用了previous()方法,這時的迭代器的位置是在3,而上次迭代到的元素的遊標位置也是3,而若是使用了next()方法,使用以後,迭代器的位置在5,而上一次迭代到的元素確是4。這也印證了nextIndex()previousIndex()的邏輯。

問題十一:expectedModCount 和 modCount

答:

從源碼中咱們能夠看到

//這個變量是transient的,也就說序列化的時候是不須要儲存的 
protected transient int modCount = 0;

這個變量表明着當前集合對象的結構性修改的次數,每次進行修改都會進行加1的操做,而expectedModCount表明的是迭代器對對象進行結構性修改的次數,這樣的話每次進行結構性修改的時候都會將expectedModCountmodCount進行對比,若是相等的話,說明沒有別的迭代器對對對象進行修改。若是不相等,說明發生了併發的操做,就會拋出一個異常。而有時也會不這樣進行判斷:

//刪除上一次迭代器越過的元素
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                   //調用須要子類去實現的remove方法
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                //每次刪除後,將lastRet置爲-1,防止連續的刪除
                lastRet = -1;
                //將修改次數賦給迭代器對對象的結構修改次數這個會在下面進行詳解
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                //若是出現索引越界,說明發生了併發的操做致使,因此拋出一個併發操做異常。
                throw new ConcurrentModificationException();
            }
        }

這裏的設計在於,進行刪除操做後,將修改次數和迭代器對象進行同步,雖然在方法的開始進行了checkForComodification()方法的判斷,可是擔憂的是再進行刪除操做的時候發生了併發的操做,因此在這裏進行了try...catch...的處理,當發生了索引越界的異常的時候,說明必定是發生了併發的操做,因此拋出一個ConcurrentModificationException()

問題十二:關於SubList和RandomAccessSubList

答:

經過閱讀源碼咱們能夠知道,這個類實際上就是一個啃老族。基本上方法全是直接去加上offset後去調用的父類的方法,而RandomAccessSubList只是在此基礎上實現了RandomAccess的接口,這個接口僅僅是一個標誌性接口,用來標誌是否能夠隨機訪問的。

問題十三:說說遠古時代的ArrayList——Vector

答:

Vector是一種實現了動態數組的集合,即長度能夠自動增加的數組,它是線程同步(安全)的,也就是說同一時刻只有一個線程能夠寫Vector,能夠避免多線程同時寫引發的不一致性,可是比較消耗資源。

因爲資源的耗費較爲嚴重,它已經逐漸的消失在了歷史的塵埃中,取而代之的是一樣基於動態數組實現的ArrayList

問題十四:簡單說一下Stack

答:

棧(Stack)是Vector的一個子類,它實現了一個標準的後進先出的棧

public class Stack<E> extends Vector<E> {

    /**
     * Stack的無參構造函數
     */
    public Stack() {
    }

    /**
     * 把項壓入堆棧頂部
     */
    public E push(E item) {
        addElement(item);
        return item;
    }

    /**
     * 移除堆棧頂部的對象,並做爲此函數的值返回該對象。
     * @return  被移除的對象
     */
    public synchronized E pop() {
        E obj;
        int len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    /**
     * 查看堆棧頂部的對象
     * @return
     */
    public synchronized E peek() {
        int len = size();
        if (len == 0) {
            throw new EmptyStackException();
        }
        return elementAt(len - 1);
    }

    /**
     * 測試堆棧是否爲空
     * @return
     */
    public boolean empty() {
        return size() == 0;
    }

    /**
     * 返回對象在堆棧中的位置,以 1 爲基數
     * @param o 須要查找位置的對象
     * @return
     */
    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    /**
     * 版本id
     */
    private static final long serialVersionUID = 1224463164541339165L;

}

Stack繼承自Vector,說明它也是經過數組實現的而非鏈表。並且Vector類具備的特性它都具備。

問題十五:說一下你對ArrayList源碼的理解

答:

ArrayListVector很是類似,他們都是基於數組實現的集合,均可以動態擴容,只不過Vector是同步的,所需的資源較多,並且比較老,有一些缺點,因此咱們如今更多的是去使用ArrayList,而不是Vector。下面,咱們在閱讀源碼的過程當中遇到的一些問題對ArrayList進行分析。

首先從構造函數提及:

/**
     * 共享空數組對象
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 和上面的區別在於,
     * 第一次添加元素時知道該 elementData 從空的構造函數仍是有參構造函數被初始化的。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 用於盛放集合元素的數組對象
     */
    transient Object[] elementData;

    /**
     * 有參構造,參數爲初始長度,若是參數爲0,調用EMPTY_ELEMENTDATA來初始化
     *
     * @param  initialCapacity  該集合的初始化長度
     * @throws IllegalArgumentException 若是參數小於0,拋出該錯誤
     */
    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);
        }
    }

    /**
     * 無參構造,調用了DEFAULTCAPACITY_EMPTY_ELEMENTDATA來初始化
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

這裏新建了兩個空的常量數組,分別用來構造有參初始長度爲0的ArrayList實例和無參的ArrayList實例,這裏的無參構造函數實際上的默認長度是10,而有參的初始長度和參數有關。這兩個常量空數組起到的更多的是一種標記的做用,用於在後面的動態擴容中分不一樣的狀況。

/**
     * 提高該ArrayList對象容器的容量,確保能夠提供一個該容器存放數據最低所需的容量
     *
     * @param   minCapacity   最低所需容量
     */
    public void ensureCapacity(int minCapacity) {
        //這裏能夠看出,若是是默認的無參構造,最低容量是10,若是不是,最小是0。這裏體現了代碼設計的嚴謹性!
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0: DEFAULT_CAPACITY;
        //若是最低所需的容量大於容器初始的最小容量,去調用擴容的方法,這裏就體現出了兩個不一樣常量的做用
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }


    /**
     * 計算最小所需容量
     * @param elementData   須要計算的數組
     * @param minCapacity   指望的最小所需容量
     * @return  最小所需容量
     */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //判斷該容器是不是默認無參構造,若是不是,直接返回傳入的minCapacity
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //若是是,返回默認容量和指望的最小所需容量中的最大值
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    /**
     * 擴容到最小所需容量方法
     * @param minCapacity   最小容量
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    /**
     * 擴容到最小所需容量的方法,這裏的參數是通過計算後的最小容量
     * @param minCapacity   通過計算後的最小容量
     */
    private void ensureExplicitCapacity(int minCapacity) {
        //這裏關於modCount的疑問能夠去看前面AbstractList中的實現
        modCount++;
        //這裏進行一個關於最小容量和數組的長度比較,若是最小容量大於數組的長度,纔會進行擴容
        if (minCapacity - elementData.length > 0) {
            grow(minCapacity);
        }
    }

    /**
     * 數組的最大容量
     * 由於數組做爲一個對象,須要必定的內存存儲對象頭信息,對象頭信息最大佔用內存不可超過8 byte
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 動態擴容,確保數組的容量能夠裝下全部的元素
     *
     * @param 最低容量
     */
    private void grow(int minCapacity) {
        //首先獲取當前的數組的容量
        int oldCapacity = elementData.length;
        //將數組擴容50%,好比原容量是4,擴容後爲 4 + 4 / 2 = 6
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //若是擴容後的容量小於最小所需的容量
        if (newCapacity - minCapacity < 0) {
            //直接將最小容量做爲該容器的容量
            newCapacity = minCapacity;
        }
        //若是擴容後的容量大於數組最大的容量
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            //將通過處理後的最小所需容量做爲新的容量,最大不超過Integer的最大值
            newCapacity = hugeCapacity(minCapacity);
        }
        //使用Arrays.copyOf將原數組複製到一個新容量的數組,並將拷貝的結果返回給原數組,完成動態擴容。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    /**
     * 防止數組的容量過大致使內存溢出的方法,程序基本上不會走到這裏,只是以防萬一
     * @param minCapacity
     * @return
     */
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) {
            throw new OutOfMemoryError();
        }
        //若是最小所需容量大於數組最大容量,返回Integer的最大值,不然返回數組的最大容量值
        return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
    }

從上面的動態擴容部分的源碼分析中,咱們能夠看到這兩個空常量數組的做用所在,這裏又會遇到一個問題,在ArrayList的擴容過程當中,是按照50%的比例進行擴容的,這裏就有一個問題,擴容後的數組的長度必定會大於數組的長度,就會形成空間和資源的浪費,這時候可使用下列的方法。

/**
     * 清除集合中的空元素所佔的空間,通常在動態擴容後會產生空餘的空間
     */
    public void trimToSize() {
        modCount++;
        //若是數組中數據的個數小於數組所佔的空間,說明產生了多餘的空間
        if (size < elementData.length) {
            //若是沒有數據,返回一個EMPTY_ELEMENTDATA對象,不然將數據拷貝一個新的size長度的數組,並賦給原數組
            elementData = (size == 0)
                    ? EMPTY_ELEMENTDATA
                    : Arrays.copyOf(elementData, size);
        }
    }

接下來,咱們來看一下如何去獲取ArrayList中的元素,

/**
     * 返回用於存儲元素的數組位於某個索引上的元素
     * @param index 須要返回元素的索引
     * @return  返回該索引位置上的元素
     */
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

    /**
     * 返回該集合指定位置的元素
     *
     * @param  index 須要返回元素的索引
     * @return 該集合指定位置的元素
     * @throws IndexOutOfBoundsException 當索引超過集合的長度時,拋出該異常
     */
    @Override
    public E get(int index) {
        //這一步調用的是檢查索引越界的方法
        rangeCheck(index);
        //這一步調用的是上面的elementData()方法,本質上仍是根據索引去用於存儲數據的數組中取
        return elementData(index);
    }

能夠看出,本質上底層仍是經過數組來實現的,說到對於數組的操做,就必須說到這個在源碼中頻繁出現的方法

System.arraycopy(Object[] src, int srcPos, Object[] dest, int destPos, int length)

這幾個參數的意思分別是:

src:源數組;
srcPos:源數組要複製的起始位置;
dest:目的數組;
destPos:目的數組放置的起始位置;
length:複製的長度。

看着看着,咱們會發現一個問題,ArrayList中包括了兩個remove方法

/**
     * 刪除位於某個索引位置的元素
     *
     * @param index 即將被刪除的元素的索引
     * @return 返回的是被刪除的元素
     * @throws IndexOutOfBoundsException 當索引超過集合的長度時,拋出該異常
     */
    @Override
    public E remove(int index) {
        //首先進行索引越界的檢查
        rangeCheck(index);
        //因爲這個操做會引發結構的變化,因此要將modCount+1
        modCount++;
        //獲取本來位於該位置的元素,用於返回
        E oldValue = elementData(index);
        //獲取位於被刪除索引的前一位的索引
        int numMoved = size - index - 1;
        if (numMoved > 0) {
            //這裏的原理是 elementData = {1 ,2 ,3 ,4} ===>
            // 刪除索引爲1 的元素,而後 0(numMoved)的元素{1}做爲頭,將{3, 4}(index+1)後的部分拼接到本來的index的位置 ==>
            // {1, 3, 4},
            System.arraycopy(elementData, index+1, elementData, index,
                    numMoved);
        }
        //將本來最後一位,置爲null ==> {1,3,4,null},再將size-1,完成刪除
        elementData[--size] = null;
        return oldValue;
    }

    /**
     * 刪除集合中的指定元素,若是集合中包含該元素,返回true
     *
     * @param o 被刪除的元素
     * @return 若是集合中包含該指定元素,返回true
     */
    @Override
    public boolean remove(Object o) {
        //分爲兩種狀況 爲null和不爲null
        if (o == null) {
            for (int index = 0; index < size; index++) {
                //爲null的時候使用 == 判斷
                if (elementData[index] == null) {
                    //這裏調用的這個方法其實和上面的方法相似,只不過沒有返回值,並且沒有進行索引的判斷
                    fastRemove(index);
                    return true;
                }
            }
        } else {
            for (int index = 0; index < size; index++) {
                //不爲null的時候使用 equals 判斷
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 這裏沒有進行索引的越界判斷,也沒有返回被刪除的值,其餘的原理和remove(int index)相似
     * @param index 被刪除元素的索引,這裏之因此不用判斷,是由於在調用這個方法的時候就已經進行了判斷,不存在越界的可能
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0) {
            System.arraycopy(elementData, index+1, elementData, index,
                    numMoved);
        }
        elementData[--size] = null;
    }

能夠看出兩個刪除方法的區別在於,一個是根據元素找到索引進行刪除,返回的是否刪除成功,而一個是根據直接索引進行刪除,返回的是被刪除的元素,提及刪除,下面咱們還會看到一個被private修飾的batchRemove(Collection<?> c, boolean complement)方法,而調用這個私有方法的分別是removeAll(Collection<?> c)retainAll(Collection<?> c),而這兩個方法的區別在於一個是取交集,一個是取交集以外的元素,是兩個恰好對立的方法。

/**
     * 刪除指定集合與collection的交集
     *
     * @param c 須要與集合進行判斷的collection
     * @return 若是此次操做改變了集合的結構,返回true
     * @throws ClassCastException 若是集合的元素類型和該集合的元素類型不一致,拋出該異常
     * @throws NullPointerException 若是參數collection爲空,拋出空指針異常
     */
    @Override
    public boolean removeAll(Collection<?> c) {
        //首先進行非空校驗
        Objects.requireNonNull(c);
        //調用封裝好的批量刪除的方法,這裏傳入的參數爲false的時候,刪除的是交集
        return batchRemove(c, false);
    }

    /**
     * 刪除collection元素和該集合交集以外的元素
     *
     * @param c 須要將元素保留在該集合中的collection對象
     * @return 若是此次操做改變了集合,返回true
     * @throws ClassCastException 若是該集合的元素類型和collection中的元素不一致,拋出該異常
     * @throws NullPointerException 若是collection中的元素爲空,拋出該異常
     */
    @Override
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        //調用封裝好的批量刪除的方法,這裏傳入的參數爲true的時候,保留的是交集
        return batchRemove(c, true);
    }

    /**
     * 批量刪除的方法
     * @param c 須要和原集合進行對比的collection對象
     * @param complement    爲false的時候,刪除交集,爲true的時候,取交集,刪除其餘
     * @return
     */
    private boolean batchRemove(Collection<?> c, boolean complement) {
        //下面我寫了一個小例子幫助你們理解
        //假設原集合數組爲{1,2,3,4},c爲{2,3}
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            //size = 4
            for (; r < size; r++) {
                //a.當complement爲false,r = 0 和 3 的時候會進入循環
                //b.當complement爲true,r = 1 和 2 的時候會進入循環
                if (c.contains(elementData[r]) == complement) {
                    //r = 0 w = 0 elementData[0] = elementData[0]  {1,2,3,4}
                    //r = 3 w = 1 elementData[1] = elementData[3] {1,4,3,4}
                    // r = 1 w = 0 elementData[0] = elementData[1] {2,2,3,4}
                    //r = 2 w = 1 elementData[1] = elementData[2] {2,3,3,4}
                    elementData[w++] = elementData[r];
                }
            }
        } finally {
            //若是contains方法使用過程報異常,將剩餘的元素賦給該集合,若是不出現異常的話,是不會進入這個代碼塊的
            if (r != size) {
                System.arraycopy(elementData, r,
                        elementData, w,
                        size - r);
                w += size - r;
            }
            // w = 2
            if (w != size) {
                for (int i = w; i < size; i++) {
                    //a. elementData[2] = null, elementData[3] = null {1,4,null,null},null元素會被垃圾回收器回收麼?
                    //b. elmentData[2] = null, elementData[3] = null {2,3,null,null}
                    elementData[i] = null;
                }
                //修改次數+2
                modCount += size - w;
                //當前的數組數量就是符合條件的元素數量
                size = w;
                //返回操做成功的標誌
                modified = true;
            }
        }
        return modified;
    }

問題十六:簡單介紹一下Map吧

答:

Map是一個接口,表明的是將鍵映射到值的對象。一個映射不能包含重複的鍵,每一個鍵最多隻能映射到一個值。

Map 接口提供了三種collection視圖,容許以鍵集、值集或鍵-值映射關係集的形式查看某個映射的內容。映射順序 定義爲迭代器在映射的 collection 視圖上返回其元素的順序。某些映射實現可明確保證其順序,如 TreeMap 類;另外一些映射實現則不保證順序,如 HashMap 類。

問題十七:Map和Lambda結合能夠碰撞出什麼樣的火花

答:

遍歷:

/**
 * 遍歷集合,這裏的參數是一個函數式接口,能夠結合Lambda表達式去優雅的使用
 * @param action    進行的操做,函數式接口
 */
default void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    //其實本質上仍是用entrySet()獲取鍵值對後進行遍歷的
    for (Entry<K, V> entry : entrySet()) {
        K k;
        V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch(IllegalStateException ise) {
            throw new ConcurrentModificationException(ise);
        }
        action.accept(k, v);
    }
}

排序

/**
         * 根據映射的鍵進行排序
         */
        public static <K extends Comparable<? super K>, V> Comparator<Entry<K,V>> comparingByKey() {
            return (Comparator<Entry<K, V>> & Serializable)
                    (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }

         /**
         * 經過指定的比較器根據映射的鍵進行排序
         */
        public static <K, V> Comparator<Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Entry<K, V>> & Serializable)
                    (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
        }

首先來講的是Map的子接口Entry中的comparingByKey()方法,這個方法所起到的做用是按照映射的鍵進行排序,咱們接下來來看一下怎麼取用:

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String,String>();
        map.put("A","test1");
        map.put("B","test2");
        map.put("E","test5");
        map.put("D","test4");
        map.put("C","test3");
        Stream<Map.Entry<String, String>> sorted = map.entrySet().stream().sorted(Map.Entry.comparingByKey());
        Stream<Map.Entry<String, String>> sorted2 = map.entrySet().stream().sorted(Map.Entry.comparingByKey(String::compareTo));
        sorted.forEach(entry -> System.out.println(entry.getValue()));
        System.out.println("===============");
        sorted2.forEach(entry -> System.out.println(entry.getValue()));
    }
}

輸出結果爲:

test1
test2
test3
test4
test5
===============
test1
test2
test3
test4
test5

替換:

/**
     * 對映射中的全部鍵值對執行計算,並將返回結果做爲value覆蓋
     * map.replaceAll((k,v)->((String)k).length());
     * @param function  執行的操做,函數式接口
     */
    default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        ...
    }

    /**
     * 當且僅當 key 存在,而且對應值與 oldValue 不相等,才用 newValue 做爲 key 的新相關聯值,返回值爲是否進行了替換。
     * @param key   與指定值相關聯的鍵
     * @param oldValue  預期與指定鍵相關聯的值
     * @param newValue  與指定鍵相關聯的值
     * @return  若是該值被替換,返回true
     */
    default boolean replace(K key, V oldValue, V newValue) {
        ...
    }


    /**
     * 只有當目標映射到某個值時,才能替換指定鍵的條目。
     * @param key    與指定值相關聯的鍵
     * @param value 與指定鍵相關聯的值
     * @return  與指定鍵相關聯的上一個值,若是沒有鍵的映射,返回null
     */
    default V replace(K key, V value) {
       ...
    }

demo:

public static void main(String[] args) {
        Map<String, String> map = new HashMap<String,String>();
        map.put("A","test1");
        map.put("B","test2");
        map.replaceAll((s, s2) -> {
            return s + s2;
        });
        printMap(map);
        map.replace("A","test1");
        printMap(map);
        map.replace("A","test2","test1");
        printMap(map);
        map.replace("A","test1","test2");
        printMap(map);
    }

    public static void printMap(Map<String,String> map){
       map.forEach((key, value) -> System.out.print(key + ":" + value + "    "));
        System.out.println();
    }

打印結果:

A:Atest1    B:Btest2    
A:test1    B:Btest2    
A:test1    B:Btest2    
A:test2    B:Btest2

compute:

/**
     * 若是指定的鍵還沒有與值相關聯(或映射到null),則嘗試使用給定的映射函數計算其值,並將其輸入到此映射中,除非null 。
     * @param key   指定值與之關聯的鍵
     * @param mappingFunction   計算值的函數
     * @return  與指定鍵相關聯的當前(現有或計算)值,若是計算值爲空,則爲null
     */
    default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
          ...
    }

    /**
     * 若是指定的key的值存在且非空,則嘗試計算給定鍵及其當前映射值的新映射。
     * @param key   指定值與之關聯的鍵
     * @param remappingFunction 計算值的函數
     * @return  與指定鍵相關的新值,若是沒有則爲null
     */
    default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
          ...
    }


    /**
     * 嘗試計算指定key及其當前映射值的映射(若是沒有當前映射,則null )。
     * @param key   指定值與之關聯的鍵
     * @param remappingFunction 計算值的函數
     * @return 與指定鍵相關的新值,若是沒有則爲null
     */
    default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
       ...
    }

如今,咱們來看一下,這三個方法是怎麼用的,以及他們的不一樣。

public static void main(String[] args) {
        Map<String, String> map = new HashMap<String,String>();
        map.put("A","test1");
        map.put("B","test2");
        map.compute("A", (key, value) -> { return key + value;});
        printMap(map);
        //由於,集合中存在「A」,因此這裏沒有進行相應的操做
        map.computeIfAbsent("A", (key) -> { return key + 2;});
        printMap(map);
         //這裏由於集合中不存在「C」,因此進行了賦值的操做
        map.computeIfAbsent("C", (key) -> { return key + 2;});
        printMap(map);
         //這裏因爲集合存在「A」,根據方法定義,會計算後返回給原值
        map.computeIfPresent("A", (key, value) -> { return key + value;});
        printMap(map);
         //這裏因爲不存在「D」,根據方法定義,不作任何操做
          map.computeIfPresent("D", (key, value) -> { return key + value;});
        printMap(map);
    }

    public static void printMap(Map<String,String> map){
       map.forEach((key, value) -> System.out.print(key + ":" + value + "    "));
        System.out.println();
    }

輸出結果:

A:Atest1    B:test2    
A:Atest1    B:test2    
A:Atest1    B:test2    C:C2    
A:AAtest1    B:test2    C:C2    
A:AAtest1    B:test2    C:C2

Others

/**
     * 若是key在集合中的value爲空或則鍵值對不存在,則用參數value覆蓋
     * @param key   若是key存在且不爲null,返回key對應的value,若是不存在,調用put(key,value)
     * @param value 若是key對應的值不存在或者爲null,將該value與key進行對應
     * @return  返回的是被替代的值
     */
    default V putIfAbsent(K key, V value) {
       ...
    }
    
     /**
     * key 與 value 都匹配時才刪除。
     * @param key   被刪除的映射關係的key
     * @param value 被刪除的映射關係的value
     * @return  返回的是否刪除成功
     */
    default boolean remove(Object key, Object value) {
       ...
    }
    
     /**
     * 若是指定的鍵還沒有與值相關聯或與null相關聯,則將其與給定的非空值相關聯。
     * @param key   結合值與之關聯的鍵
     * @param value 要與與key相關聯的現有值合併的非空值,或者若是沒有現有值或空值與key相關聯,則與該key相關聯
     * @param remappingFunction 從新計算值(若是存在)的功能
     * @return 與指定鍵相關聯的新值,若是沒有值與該鍵相關聯,則返回null
     */
    default V merge(K key, V value,
                    BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
      
    }

接下來,咱們接着來看一個例子:

public static void main(String[] args) {
        Map<String, String> map = new HashMap<String,String>();
        map.put("A","test1");
        map.put("B","test2");
        map.putIfAbsent("A","test2");
        map.putIfAbsent("C","test3");
        printMap(map);
        map.remove("A","test1");
        printMap(map);
        map.merge("A","test1",(oldValue, newValue) ->{
            return oldValue + newValue;
        } );
        printMap(map);
        map.merge("A","test4",(oldValue, newValue) ->{
            return  newValue;
        } );
        printMap(map);
    }

輸出的是:

A:test1    B:test2    C:test3    
B:test2    C:test3    
A:test1    B:test2    C:test3    
A:test4    B:test2    C:test3

問題十八:Set 的源碼你瞭解多少

答:

Set繼承了Collection接口,它自己也是一個接口,表明一種不能擁有重複元素的容器類型,更確切的說,集合不包含一對元素e1e2 ,使得e1.equals(e2)

經過Set的一些實現,咱們能夠發現,Set是基於Map進行實現的,因此Set取值時不保證數據和存入的時候順序一致,而且不容許空值,不容許重複值。下面咱們來看一下Set都給咱們提供了哪些方法。

首先,Set提供一些關於自己屬性的接口:

/**
 * 返回 set 中的元素個數
 * @return  set中元素個數
 */
int size();

/**
 * 若是set中不包含任何元素,返回true
 * @return  若是set中不包含任何元素,返回true
 */
boolean isEmpty();

固然,也提供了去該集合中查詢元素是否存在的接口:

/**
 * 若是set包含指定的元素,則返回 true
 * @param o 指定的元素
 * @return  若是 set 包含指定的元素,則返回 true。
 */
boolean contains(Object o);

/**
 * 若是此 set 包含指定 collection 的全部元素,則返回 true。
 * 若是指定的 collection 也是一個 set,那麼當該 collection 是此 set 的 子集 時返回 true。
 * @param c 檢查是否包含在此 set 中的 collection
 * @return 若是此 set 包含指定 collection 中的全部元素,則返回 true
 */
boolean containsAll(Collection<?> c);

對於元素進行結構性操做的接口也有幾個,這裏須要注意的是,在添加元素的時候,若是該元素在集合中已經存在,會致使添加失敗並返回一個false。

/**
 * 若是 set 中還沒有存在指定的元素,則添加此元素
 * @param e 被添加的元素
 * @return  若是set中存在該元素,添加失敗並返回false
 */
boolean add(E e);

/**
 * 若是 set 中沒有指定 collection 中的全部元素,則將其添加到此 set 中
 * 若是指定的 collection 也是一個 set,則 addAll 操做會實際修改此 set,
 * 這樣其值是兩個 set 的一個 並集。若是操做正在進行的同時修改了指定的 collection,則此操做的行爲是不肯定的。
 * @param c
 * @return
 */
boolean addAll(Collection<? extends E> c);

/**
 * 若是 set 中存在指定的元素,則將其移除(可選操做)。
 * @param o 被刪除的元素
 * @return  若是此 set 包含指定的對象,則返回true
 */
boolean remove(Object o);

/**
 * 僅保留 set 中那些包含在指定 collection 中的元素,換句話說,只取二者交集,其他的無論
 * @param c 與set進行判斷的集合
 * @return  若是此 set 因爲調用而發生更改,則返回 true
 */
boolean retainAll(Collection<?> c);


/**
 * 移除 set 中那些包含在指定 collection 中的元素,也就是說,取交集以外的全部元素
 * @param c 與set進行判斷的集合
 * @return  若是此 set 因爲調用而發生更改,則返回 true
 */
boolean removeAll(Collection<?> c);


/**
 * 移除此 set 中的全部元素,此調用返回後該 set 將是空的。
 */
void clear();

Set中提供了一個默認的獲取可切割迭代器的一個實例,是經過Spliterators方法進行獲取

/**
 * 可切割的迭代器,返回的是該set集合的可切割迭代器的一個實例
 * @return
 */
@Override
default Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, Spliterator.DISTINCT);
}

問題十九:那麼AbstractSet的源碼呢,有沒有什麼瞭解

答:

經過源碼咱們能夠看到,AbstractSet中提供了三個方法的重寫,分別是equalshashCoderemoveAll這三個方法,equalshashCode是如何重寫的這裏就再也不說明,下面看看removeAll

/**
 * 今後 set 中移除包含在指定 collection 中的全部元素
 * 若是指定 collection 也是一個 set,則此操做有效地修改此 set,從而其值成爲兩個 set 的 不對稱差集。
 *
 * @param c 包含將今後 set 中移除的元素的 collection
 * @return 若是此 set 因爲調用而發生更改,則返回 true
 */
@Override
public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    boolean modified = false;
    //經過在此 set 和指定 collection 上調用 size 方法,此實現能夠肯定哪個更小。
    if (size() > c.size()) {
        // 若是此 set 中的元素更少,則該實現將在此 set 上進行迭代,依次檢查迭代器返回的每一個元素,查看它是否包含在指定的 collection 中。
        for (Iterator<?> i = c.iterator(); i.hasNext(); ) {
            //若是包含它,則使用迭代器的 remove 方法今後 set 中將其移除。
            modified |= remove(i.next());
        }
    } else {
        //若是指定 collection 中的元素更少,則該實現將在指定的 collection 上進行迭代,並使用此 set 的 remove 方法,今後 set 中移除迭代器返回的每一個元素。
        for (Iterator<?> i = iterator(); i.hasNext(); ) {
            if (c.contains(i.next())) {
                i.remove();
                modified = true;
            }
        }
    }
    return modified;
}

問題二十:最後一個問題:說說HashMap

答:

提及HashMap,你們確定都不會陌生,咱們用的最多的大概就是這個容器類來存儲k-v數據,正如它的名字所說的那樣,它是基於散列表實現的,散列表的強大之處在於查找時的時間複雜度爲O(1),由於每一個對象都有一個對應的索引,咱們能夠直接根據對象的索引去訪問這個對象,而這個索引就是咱們對象的hash值。

在Java中散列表是經過鏈表 + 數組進行實現的,每一個鏈表能夠稱之爲一個桶,而對象的位置就是經過計算該對象的哈希值,而後與桶的總數(也就是HashMap的長度)取餘,所獲得的結果就是保存這個元素的桶的索引,若是出現兩個對象具備一樣的哈希值,就會出現Hash衝突的現象,這個時候就須要用新的對象與鏈表(桶)中的對象進行比較,查看這個對象是否已經存在。若是不存在,就新增一個。

可是這裏遇到了一個問題,若是說桶的數量頗有限(好比只有三個桶),可是數據量卻很大,好比有10000個數據,這樣就會致使哈希衝突很是的嚴重,這時,JDK 8之後的版本爲咱們提供了一種新的思路,當鏈表的長度大於8的時候,就會將後續的元素存儲到紅黑樹(也叫平衡二叉樹)當中,這樣能夠大大的提升咱們的查詢效率。

static final int TREEIFY_THRESHOLD = 8;

構造

首先,咱們來看一下源碼的構造函數

能夠看出,源碼中給出了四種構造函數,第一個表示的給定初始化Map長度(桶數)和裝填因子的構造函數,裝填因子的做用是決定什麼時候對散列表進行再散列,好比,初始化裝填因子是0.75,當表中75%的位置已經填入了元素,這個表就會用雙倍的桶數進行再散列。若是沒有設置初始值,就會採用默認的值(長度爲16,裝填因子爲0.75)

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final int MAXIMUM_CAPACITY = 1 << 30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

第四個表明的是構造一個包含該Map的HashMap對象。

Node

經過觀察源碼,咱們能夠發現HashMap是基於一個叫Node的內部類做爲骨幹來實現的,而這個內部類NodeEntry的一個實現。

NodehashCode()方法的實現:

public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

這裏之因此進行了異或運算,是爲了讓散列的更均勻,以減小哈希衝突的次數。
關於TreeNode是一些有關於紅黑樹的實現,這裏再也不多作篇幅進行講解,後面會在數據結構和算法中進行詳細的學習。

關於GET,PUT的實現

咱們首先來看一些get()方法的實現:

public V get(Object key) {
  Node<K,V> e;
  return (e = getNode(hash(key), key)) == null ? null : e.value;
}

這裏能夠看到,首先經過key和計算出的hash值來找到對應的Node,而後獲取到該Node的Value或者是null。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

這裏的hash算法也是爲了讓散列的更均勻,減小散列衝突的次數

image-20200627124413534

這裏的實現能夠分爲如下幾步:

  1. 根據傳入的hash值,能夠直接計算出對應的索引(n - 1)& hash。
  2. 判斷第一個存在的節點的key是否和查詢的key相等。若是相等,直接返回該節點。
  3. 對該Node進行遍歷
  4. 判斷該集合的結構是鏈表仍是紅黑樹,若是是紅黑樹調用內部的方法找到key對應的value。
  5. 若是結構是鏈表,遍歷獲取到對應key所對應的value。

而後,咱們來看put()方法的實現:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

image-20200627124707107

首先來解釋一下方法中的參數:boolean onlyIfAbsent表示只有在該key對應原來的value爲null的時候才插入,也就是說若是value以前存在了,就不會被新put的元素覆蓋。其他的流程和get()方法的思路有很大層度上類似,這裏須要注意的有圈住的地方,

插入成功後,要判斷是否須要轉換爲紅黑樹,由於插入後鏈表長度加1,而binCount並不包含新節點,因此判斷時要將臨界閾值減1。當新長度知足轉換條件時,調用treeifyBin方法,將該鏈表轉換爲紅黑樹。

尾聲

關於集合的內容到這裏就告一段落了,相信看到我嘔心瀝血寫的這二十個問題,必定會有不少新的收穫,若是你學到了,請給我一個點贊+關注,這是對一個原創者最大的支持和幫助。

千篇一概的皮囊,萬里挑一的靈魂,這裏是山禾,一個不太同樣的寫手。

qrcode

相關文章
相關標籤/搜索