問題一:看到這個圖,你會想到什麼?
(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()
進行並行流的實現,大大提高處理效率。
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
的抽象類實現,它的方法都是基於迭代器來完成的,這裡只貼出了原始碼中幾個需要特殊的注意的點,
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
介面給出了一些預設的實現。而且它是針對隨機訪問儲存資料的方式的,如果需要使用順序訪問儲存資料方式,還有一個AbstractSequentialList
是AbstractList
的子類,順序訪問時應該優先使用它。
接下來,我們來看一下AbstractList
的原始碼,看看他針對於List
介面相較於AbstractCollection
給出了哪些不同的實現方法。
AbstractList
的原始碼在結構上分為了兩種內部迭代器,兩種內部類以及AbstractList
本身的程式碼,它的一些實現都是基於內部類和內部的兩種迭代器:Itr
和ListItr
來完成的,下面是部分原始碼的解析(由於篇幅原因,不能放上全部,只能拋磚引玉,寫一部分)
//由於該集合是不可變的,所以一切可能會改變集合元素的操作都會丟擲一個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
代表的是迭代器對物件進行結構性修改的次數,這樣的話每次進行結構性修改的時候都會將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();
}
}
這裡的設計在於,進行刪除操作後,將修改次數和迭代器物件進行同步,雖然在方法的開始進行了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原始碼的理解
答:
ArrayList
與Vector
非常相似,他們都是基於陣列實現的集合,都可以動態擴容,只不過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
介面,它本身也是一個介面,代表一種不能擁有重複元素的容器型別,更確切的說,集合不包含一對元素e1
和e2
,使得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
中提供了三個方法的重寫,分別是equals
,hashCode
,removeAll
這三個方法,equals
和hashCode
是如何重寫的這裡就不再說明,下面看看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
的內部類作為骨幹來實現的,而這個內部類Node
是Entry
的一個實現。
Node
的hashCode()
方法的實現:
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演算法也是為了讓雜湊的更均勻,減少雜湊衝突的次數
這裡的實現可以分為以下幾步:
根據傳入的hash值,可以直接計算出對應的索引(n - 1)& hash。 判斷第一個存在的節點的key是否和查詢的key相等。如果相等,直接返回該節點。 對該Node進行遍歷 判斷該集合的結構是連結串列還是紅黑樹,如果是紅黑樹呼叫內部的方法找到key對應的value。 如果結構是連結串列,遍歷獲取到對應key所對應的value。
然後,我們來看put()
方法的實現:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
首先來解釋一下方法中的引數:boolean onlyIfAbsent
表示只有在該key對應原來的value為null的時候才插入,也就是說如果value之前存在了,就不會被新put的元素覆蓋。其餘的流程和get()
方法的思路有很大層度上相似,這裡需要注意的有圈住的地方,
插入成功後,要判斷是否需要轉換為紅黑樹,因為插入後連結串列長度加1,而binCount
並不包含新節點,所以判斷時要將臨界閾值減1。當新長度滿足轉換條件時,呼叫treeifyBin
方法,將該連結串列轉換為紅黑樹。
尾聲
關於集合的內容到這裡就告一段落了,相信看到我嘔心瀝血寫的這二十個問題,一定會有很多新的收穫,如果你學到了,請給我一個點贊+關注,這是對一個原創者最大的支援和幫助。
千篇一律的皮囊,萬里挑一的靈魂,這裡是山禾,一個不太一樣的寫手。