Java List相關我學習疑惑以及問題
文章目錄
- Java List相關我學習疑惑以及問題
- iterator的set和add的區別
- expectedModCount = modCount操作,關於快速失敗和安全失敗
- 快速失敗機制的一個漏洞
- lastRet變量作用
- protected transient int modCount = 0
- 迭代器模式的好處
- 爲什麼AbstractList已經實現了List接口,而Arraylist還要去實現List接口
- Arraylist和Vector的區別
- ArrayList傳入集合的有參構造器的問題
- EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的區別(霧)
- new 和Clone的區別
- Arrays.copyOf和System.arraycopy的聯繫
- grow方法不會修改size的值嗎
- batchRemove()方法的一些解答
- 爲什麼elementData要修飾爲transient
- readObject和writeobject
- forEach()方法與forEachRemaining()方法的區別和聯繫
- 爲什麼一定要去實現Iterable這個接口呢?爲什麼不直接實現Iterator接口呢?
- spliterator方法
- ArrayList的replaceAll方法
- System.arraycopy方法
- 分析一下removeIf方法
- 關於ArrayList以及Vector和CopyOnWriteArrayList的聯繫
- 關於爲什麼ArrayLiset是線程不安全的解釋
iterator的set和add的區別
Set方法:用指定元素替換 next 或 previous 返回的最後一個元素
add方法將指定的元素插入列表,該元素直接插入到 next 返回的元素的後面
List<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
list.add(2);
ListIterator it = list.listIterator();
it.next();
//it.add(10);
//it.set(10);
System.out.println(list);
如果取消註釋掉的add方法,結果就是[0, 10, 1, 2]
如果取消註釋掉的set方法,結果就是[10, 1, 2]
expectedModCount = modCount操作,關於快速失敗和安全失敗
https://www.cnblogs.com/hasse/p/5024193.html
https://juejin.im/post/5be62527f265da617369cdc8
快速失敗機制的一個漏洞
https://www.cnblogs.com/Xieyang-blog/p/9320943.html
簡單來說,就是在用迭代器刪除一個list的倒數第2個元素的時候,調用list的remove()方法並不會報錯,原因在於remove方法修改了size(),導致下一次的hasNext()返回false,使得不會進入checkForComodification()方法,也就不會報錯。
lastRet變量作用
官方解釋:最近一次調用next或 previous返回的元素的索引,沒找到作用在哪
next方法:修改lastRet
previous方法:修改lastRet
remove方法:重置爲-1,且不允許在-1下操作
add方法:重置爲-1
set方法:不允許在-1下操作
protected transient int modCount = 0
這中間有一個transient 關鍵字,這個關鍵字 的用處在於**當某個字段被聲明爲transient後,默認序列化機制就會忽略該字段。**順帶一提,在java序列化的時候,靜態變量是不能被序列化的。這裏的不能序列化的意思,是序列化信息中不包含這個靜態成員域。
這個modCount
變量代表着當前集合對象的結構性修改的次數,每次進行修改都會進行加1的操作,而expectedModCount
代表的是迭代器對對象進行結構性修改的次數,這樣的話每次進行結構性修改的時候都會將expectedModCount
和modCount
進行對比,如果相等的話,說明沒有別的迭代器對對對象進行修改。如果不相等,說明發生了併發的操作,就會拋出一個異常。
迭代器模式的好處
**迭代器模式是爲容器而生。**我們知道,對容器對象的訪問必然涉及到遍歷算法。你可以一股腦的將遍歷方法塞到容器對象中去,或者,根本不去提供什麼遍歷算法,讓使用容器的人自己去實現。這兩種情況好像都能夠解決問題。然而,對於前一種情況,容器承受了過多的功能,它不僅要負責自己“容器”內的元素維護(增、刪、改、查 等),而且還要提供遍歷自身的接口;而且最重要的是, 由於遍歷狀態保存的問題,不能對同一個容器對象同時進行多個遍歷,並且還需增加 reset 操作。第二種方式倒是省事,卻又將容器的內部細節暴露無遺。
爲什麼AbstractList已經實現了List接口,而Arraylist還要去實現List接口
參考鏈接:https://stackoverflow.com/questions/4387419/why-does-arraylist-have-implements-list
根據stackflow所說,雖然在實不實現List都能夠使得代碼正常工作,但是爲了方便觀察繼承結構,幫助理解,所以還是把List接口加上了。
Arraylist和Vector的區別
1、Vector是線程安全的,ArrayList不是線程安全的。
2、ArrayList在底層數組不夠用時在原來的基礎上擴展0.5倍,Vector是擴展1倍。
在代碼的底層實現中,只要是關鍵性的操作,Vector方法前面都加了synchronized關鍵字,來保證線程的安全性。
ArrayList傳入集合的有參構造器的問題
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
其中, c.toArray might (incorrectly) not return Object[] (see 6260652),意思是存在一個bug,使得c.toArray 返回的可能不是一個Object。
經過實測,也的確是這樣的,有問題。
System.out.println(Arrays.asList("element1", "element2").toArray().getClass() == Object[].class);
在1.8及以前這是false,但是這個bug已經在1.9的時候已經修復了。
EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的區別(霧)
EMPTY_ELEMENTDATA在ArrayList中是一個static修飾的字段,然後我們也會發現,ArrayList的構造器總共有種。
其中如果傳入了容量,且容量爲0,就會把EMPTY_ELEMENTDATA數組的引用賦給當前數組。
如果傳入的是一個Collection,並且傳入的集合爲空,那麼也會把EMPTY_ELEMENTDATA數組的引用賦給當前數組。
所以可以看出EMPTY_ELEMENTDATA的最大作用可以理解爲減少內存消耗。因爲在jdk1.8以前的版本,對於傳入容量爲0的構造器,內部實現都是new了一個空數組,一定程度上增加了內存的消耗。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA則用在了無參構造器上的初始化,又或者是在擴容的時候用於比較。當然這也是一個static字段,也有着減少內存消耗的作用。它與EMPTY_ELEMENTDATA區分開來,也許在某種程度上來說也是爲了區分度。
new 和Clone的區別
-
使用new操作符創建一個對象
-
使用clone方法複製一個對象
關於clone的步驟:
分配內存,調用clone方法時,分配的內存和源對象(即調用clone方法的對象)相同,然後再使用原對象中對應的各個域,填充新對象的域, 填充完成之後,clone方法返回,一個新的相同的對象被創建,同樣可以把這個新對象的引用發佈到外部。
另外關於深拷貝和淺拷貝,當前clone的對象引用是不同的,屬於深拷貝。但是如果對象裏面還有子對象,那麼子對象是屬於淺拷貝,也就是拷貝對象和被拷貝對象中的子對象是同一個引用。
因此,要想實現徹底的深拷貝,需要對對象的引用鏈上所有子對象都實現Cloneable接口。
Arrays.copyOf和System.arraycopy的聯繫
聯繫: 看兩者源代碼可以發現copyOf()
內部調用了System.arraycopy()
方法 區別:
- arraycopy()需要目標數組,將原數組拷貝到你自己定義的數組裏,而且可以選擇拷貝的起點和長度以及放入新數組中的位置
- copyOf()是系統自動在內部新建一個數組,並返回該數組。
grow方法不會修改size的值嗎
grow又不增加size大小
batchRemove()方法的一些解答
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
可以看到,裏面有一段(r != size)的判斷,就覺得很奇怪。
註釋上解釋爲保持兼容一致性因爲contains是抽象方法,有可能爲發生異常
,也就意味着如果發生異常,後面的arraycopy會把異常位置後面的代碼全部拷貝到新的數組,不再判斷這部分代碼的contain()。
可以發現,這裏寫的是比較嚴謹的。
爲什麼elementData要修飾爲transient
elementData數組相當於容器,當容器不足時就會再擴充容量,但是容器的容量往往都是大於或者等於ArrayList所存元素的個數。
比如,現在實際有了8個元素,那麼elementData數組的容量可能是8x1.5=12,如果直接序列化elementData數組,那麼就會浪費4個元素的空間,特別是當元素個數非常多時,這種浪費是非常不合算的。所以ArrayList的設計者將elementData設計爲transient,然後在writeObject方法中手動將其序列化,並且只序列化了實際存儲的那些元素,而不是整個數組。
總的來說就是節省空間。
readObject和writeobject
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
因爲elementData是由transient修飾的,也就是不能進行序列化操作,因爲極端情況下會很浪費空間。那麼我們就需要手動序列化。這就要依靠readObject方法。首先會執行defaultWriteObject方法,用於序列化所有非靜態和非transient修飾的字段。然後獲得size,之後將長度爲size的數組寫入,而不是數組的所有長度。
而readObject同理,先獲得size,然後只讀取size個元素。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
forEach()方法與forEachRemaining()方法的區別和聯繫
相似之處:
- 都可以遍歷集合
- 都是接口的默認方法
- 都是1.8版本引入的
不同之處:
-
forEach()方法位於的是Iterable接口,forEachRemaining()方法位於的是Iterator接口
-
可以看下具體的默認實現
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
/*********************************************************************/
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
可以看到,作用基本都是遍歷集合,對每一個遍歷到的元素都調用Consumer的accept方法。
但是,forEachRemaining是通過當前迭代器遍歷的,而且遍歷到最後之後,再次調用就會沒有任何作用。
而forEach方法,是可以多次調用的,而且都是遍歷全部內容。
除此之外,forEachRemaining在ArrayList中是被重寫了的,這就會導致一個問題,看如下代碼
public static void main(String[] args) {
ArrayList<String> list = new ArrayList();
for (int i = 0; i < 10; i++) {
list.add(String.valueOf(i));
}
Iterator iterator = list.iterator();
iterator.forEachRemaining(new Consumer() {
@Override
public void accept(Object o) {
System.out.println(o);
if (o.equals("3")) {
System.out.println("remove");
iterator.remove();
}
}
});
}
如果是默認的代碼,這顯然沒有問題的,但是實際上是有問題的,這段代碼會拋出IllegalStateException
異常,原因在於迭代器的remove方法基於lastRet的值,只有在這個值!=-1 的前提下,才能夠正常執行。
而重寫過後的forEachRemaining
ublic void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
// 注意這裏
cursor = i;
lastRet = i - 1;
checkForComodification();
}
它是通過for的主動遍歷,而不再通過next方法,也就導致沒有更新lastRet,所以在forEachRemaining過程中調用remove方法,就會報錯。
爲什麼一定要去實現Iterable這個接口呢?爲什麼不直接實現Iterator接口呢?
因爲Iterator接口的核心方法next()或者hasNext() 是依賴於迭代器的當前迭代位置的。 如果Collection直接實現Iterator接口,勢必導致集合對象中包含當前迭代位置的數據(指針)。 當集合在不同方法間被傳遞時,由於當前迭代位置不可預置,那麼next()方法的結果會變成不可預知。 除非再爲Iterator接口添加一個reset()方法,用來重置當前迭代位置。 但即時這樣,Collection也只能同時存在一個當前迭代位置。 而Iterable則不然,每次調用都會返回一個從頭開始計數的迭代器。 多個迭代器是互不干擾的。
spliterator方法
這個方法也是來自於 Collection 接口,ArrayList 對此方法進行了重寫。該方法會返回 ListSpliterator 實例,該實例用於遍歷和分離容器所存儲的元素。
它的主要操作方法有下面三種:
tryAdvance
迭代單個元素,類似於iterator.next()
forEachRemaining
迭代剩餘元素trySplit
將元素切分成兩部分並行處理,但需要注意的 Spliterator 並不是線程安全的。
下面是例子:
public static void main(String[] args) {
ArrayList<Integer> numberss = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
Spliterator<Integer> numbers1 = numberss.spliterator();
Spliterator<Integer> numbers2 = numberss.spliterator();
numbers1.forEachRemaining(e -> System.out.println(e + " num1"));//1 2 3 4 5 6
numbers2.tryAdvance(e -> System.out.println(e + " num2"));//1
Spliterator<Integer> numbers3 = numbers2.trySplit();//返回的是左半部分,如果是技術,則是較少的
numbers2.forEachRemaining(e -> System.out.println(e + " num2"));//4 5 6
numbers3.forEachRemaining(e -> System.out.println(e + " num3"));//2 3
}
ArrayList的replaceAll方法
源碼時這樣的
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
elementData[i] = operator.apply((E) elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
這個方法傳入的是一個第一次見到的參數UnaryOperator<E> operator
,點擊發現父類是java1.8新增加的一個接口
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
/**************************************************************/
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
也就是說,想要使用replaceAll方法,只需要傳入一個實現了apply方法的類就行了,當然最方便的還是使用lamda表達式。
舉個例子:
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
list.add(i);
}
list.replaceAll(x -> x + 1);//裏面是lamda表達式
System.out.println(list);
}
也就是說,這個replaceAll方法作用是能夠統一對集合中的元素進行某種操作。
System.arraycopy方法
這個方法的類型是native的,這就意味着這不是java的方法實現,它調用的本地方法。而且這個放個相對於其他的遍歷方式,比如說for,迭代器之類的,速度優勢會比較明顯。
根據對底層的理解,System.arraycopy是對內存直接進行復制,減少了for循環過程中的尋址時間,從而提高了效能。
這個方法不是線程安全的。
分析一下removeIf方法
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
這個方法的作用是如果滿足某個條件,那麼在這個集合中就會移除這個值。
這個裏面出現了一個新的東西BitSet。這個對象在這裏的作用主要標記集合的第幾個是要被移除的。
還有一個set方法,傳入i,標記第i位是true
然後下面有一個方法叫nextClearBit,傳入一個i,返回i這個下標(包括)後面的false的是第幾位。那麼再經過一遍循環,就能夠完成目標了。
關於ArrayList以及Vector和CopyOnWriteArrayList的聯繫
參考自:https://juejin.im/post/5aaa2ba8f265da239530b69e
首先講一下什麼是Copy-On-Write
,顧名思義,在計算機中就是當你想要對一塊內存進行修改時,我們不在原有內存塊中進行寫
操作,而是將內存拷貝一份,在新的內存中進行寫
操作,寫
完之後呢,就將指向原來內存指針指向新的內存,原來的內存就可以被回收掉嘛!
其中Vector和CopyOnWriteArrayList是線程安全的,ArrayList是線程不安全的。
CopyOnWriteArrayList
優缺點
缺點:
- 1、耗內存(集合複製)
- 2、實時性不高
優點:
- 1、數據一致性完整,爲什麼?因爲加鎖了,併發數據不會亂
- 2、解決了
像ArrayList
、Vector
這種集合多線程遍歷迭代問題,記住,Vector
雖然線程安全,只不過是加了synchronized
關鍵字,迭代問題完全沒有解決!
CopyOnWriteArrayList
使用場景
- 1、讀多寫少(白名單,黑名單,商品類目的訪問和更新場景),爲什麼?因爲寫的時候會複製新集合
- 2、集合不大,爲什麼?因爲寫的時候會複製新集合
- 實時性要求不高,爲什麼,因爲有可能會讀取到舊的集合數據
關於爲什麼ArrayLiset是線程不安全的解釋
https://blog.csdn.net/u012859681/article/details/78206494