Java List相關集合問題(長期更新)

Java List相關我學習疑惑以及問題

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代表的是迭代器對對象進行結構性修改的次數,這樣的話每次進行結構性修改的時候都會將expectedModCountmodCount進行對比,如果相等的話,說明沒有別的迭代器對對對象進行修改。如果不相等,說明發生了併發的操作,就會拋出一個異常。

迭代器模式的好處

**迭代器模式是爲容器而生。**我們知道,對容器對象的訪問必然涉及到遍歷算法。你可以一股腦的將遍歷方法塞到容器對象中去,或者,根本不去提供什麼遍歷算法,讓使用容器的人自己去實現。這兩種情況好像都能夠解決問題。然而,對於前一種情況,容器承受了過多的功能,它不僅要負責自己“容器”內的元素維護(增、刪、改、查 等),而且還要提供遍歷自身的接口;而且最重要的是, 由於遍歷狀態保存的問題,不能對同一個容器對象同時進行多個遍歷,並且還需增加 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的區別

  1. 使用new操作符創建一個對象

  2. 使用clone方法複製一個對象

關於clone的步驟:

分配內存,調用clone方法時,分配的內存和源對象(即調用clone方法的對象)相同,然後再使用原對象中對應的各個域,填充新對象的域, 填充完成之後,clone方法返回,一個新的相同的對象被創建,同樣可以把這個新對象的引用發佈到外部。

另外關於深拷貝和淺拷貝,當前clone的對象引用是不同的,屬於深拷貝。但是如果對象裏面還有子對象,那麼子對象是屬於淺拷貝,也就是拷貝對象和被拷貝對象中的子對象是同一個引用。

因此,要想實現徹底的深拷貝,需要對對象的引用鏈上所有子對象都實現Cloneable接口。

Arrays.copyOf和System.arraycopy的聯繫

聯繫: 看兩者源代碼可以發現copyOf()內部調用了System.arraycopy()方法 區別:

  1. arraycopy()需要目標數組,將原數組拷貝到你自己定義的數組裏,而且可以選擇拷貝的起點和長度以及放入新數組中的位置
  2. 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、解決了像ArrayListVector這種集合多線程遍歷迭代問題,記住,Vector雖然線程安全,只不過是加了synchronized關鍵字,迭代問題完全沒有解決!

CopyOnWriteArrayList使用場景

  • 1、讀多寫少(白名單,黑名單,商品類目的訪問和更新場景),爲什麼?因爲寫的時候會複製新集合
  • 2、集合不大,爲什麼?因爲寫的時候會複製新集合
  • 實時性要求不高,爲什麼,因爲有可能會讀取到舊的集合數據

關於爲什麼ArrayLiset是線程不安全的解釋

https://blog.csdn.net/u012859681/article/details/78206494

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章