<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          滿地坑!細(xì)數(shù)List的10個坑!

          共 23182字,需瀏覽 47分鐘

           ·

          2022-10-15 12:31

          來源:juejin.cn/post/7143266514722881544
          滿地坑!細(xì)數(shù)List的10個坑

          大家好,我是胖虎,今天我們主要來說一說List操作在實際使用中有哪些坑,以及面對這些坑的時候我們要怎么解決。

          1. Arrays.asList轉(zhuǎn)換基本類型數(shù)組的坑

          在實際的業(yè)務(wù)開發(fā)中,我們通常會進(jìn)行數(shù)組轉(zhuǎn)List的操作,通常我們會使用Arrays.asList來進(jìn)行轉(zhuǎn)換

          但是在轉(zhuǎn)換基本類型的數(shù)組的時候,卻出現(xiàn)轉(zhuǎn)換的結(jié)果和我們想象的不一致。

          上代碼

          int[] arr = {123}; 
          List list = Arrays.asList(arr); 
          System.out.println(list.size()); 
          // 1

          實際上,我們想要轉(zhuǎn)成的List應(yīng)該是有三個對象而現(xiàn)在只有一個

          public static List asList(T... a) 
              return new ArrayList<>(a); 
          }

          可以觀察到 asList方法 接收的是一個泛型T類型的參數(shù),T繼承Object對象

          所以通過斷點我們可以看到把 int數(shù)組 整體作為一個對象,返回了一個 List<int[]>

          image1.png

          那我們該如何解決呢?

          方案一:Java8以上,利用Arrays.stream(arr).boxed()將裝箱為Integer數(shù)組

          List collect = Arrays.stream(arr).boxed().collect(Collectors.toList()); System.out.println(collect.size()); 
          System.out.println(collect.get(0).getClass()); 
          // 3 
          // class java.lang.Integer

          方案二:聲明數(shù)組的時候,聲明類型改為包裝類型

          Integer[] integerArr = {123}; 
          List integerList = Arrays.asList(integerArr); 
          System.out.println(integerList.size()); System.out.println(integerList.get(0).getClass()); 
          // 3 
          // class java.lang.Integer

          2. Arrays.asList返回的List不支持增刪操作

          我們將數(shù)組對象轉(zhuǎn)成List數(shù)據(jù)結(jié)構(gòu)之后,竟然不能進(jìn)行增刪操作了

          private static void asListAdd(){
              String[] arr = {"1""2""3"};
              List<String> strings = new ArrayList<>(Arrays.asList(arr));
              arr[2] = "4";
              System.out.println(strings.toString());
              Iterator<String> iterator = strings.iterator();
              while (iterator.hasNext()){
                  if ("4".equals(iterator.next())){
                      iterator.remove();
                  }
              }
              strings.forEach(val ->{
                  strings.remove("4");
                  strings.add("3");
              });


              System.out.println(Arrays.asList(arr).toString());
          }

          [124
          Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.remove(AbstractList.java:161) at java.util.AbstractList$Itr.remove(AbstractList.java:374) at java.util.AbstractCollection.remove(AbstractCollection.java:293) at JavaBase.List.AsListTest.lambda$asListAdd$0(AsListTest.java:47) at java.util.Arrays$ArrayList.forEach(Arrays.java:3880) at JavaBase.List.AsListTest.asListAdd(AsListTest.java:46) at JavaBase.List.AsListTest.main(AsListTest.java:20)

          初始化一個字符串?dāng)?shù)組,將字符串?dāng)?shù)組轉(zhuǎn)換為 List,在遍歷List的時候進(jìn)行移除和新增的操作

          拋出異常信息UnsupportedOperationException。

          根據(jù)異常信息java.lang.UnsupportedOperationException,我們看到他是從AbstractList里面出來的,讓我們進(jìn)入源碼一看究竟

          我們在什么時候調(diào)用到了這個 AbstractList 呢?

          其實 Arrays.asList(arr) 返回的 ArrayList 不是 java.util.ArrayList,而是 Arrays的內(nèi)部類

          private static class ArrayList<Eextends AbstractList<E>
                  implements RandomAccessjava.io.Serializable
          {
              private static final long serialVersionUID = -2764017481108945198L;
              private final E[] a;
              ArrayList(E[] array) {
                  a = Objects.requireNonNull(array);
              }

              @Override
              public E get(int index) {}

              @Override
              public E set(int index, E element) {...}

          ...
          }
          public abstract class AbstractList<Eextends AbstractCollection<Eimplements List<E{
              public boolean add(E e) {
                  add(size(), e);
                  return true;
              }
              public void add(int index, E element) {
                  throw new UnsupportedOperationException();
              }

              public E remove(int index) {
                  throw new UnsupportedOperationException();
              }

          }

          他是沒有實現(xiàn) AbstractList 中的 add() 和 remove() 方法,這里就很清晰了為什么不支持新增和刪除,因為根本沒有實現(xiàn)。

          3. 對原始數(shù)組的修改會影響到我們獲得的那個List

          一不小心修改了父List,卻影響到了子List,在業(yè)務(wù)代碼中,這會導(dǎo)致產(chǎn)生的數(shù)據(jù)發(fā)生變化,嚴(yán)重的話會造成影響較大的生產(chǎn)問題。

          第二個坑的源碼中,完成字符串?dāng)?shù)組轉(zhuǎn)換為List之后,

          我們將字符串?dāng)?shù)組的第三個對象的值修改為4,但是很奇怪在打印List的時候,發(fā)現(xiàn)List也發(fā)生了變化。

          public static <T> List<T> asList(T... a) {

              return new ArrayList<>(a);

          }

          ArrayList(E[] array) {
              a = Objects.requireNonNull(array);
          }

          asList中創(chuàng)建了 ArrayList,但是他直接引用了原本的數(shù)組對象

          所以只要原本的數(shù)組對象一發(fā)生變化,List也跟著變化

          所以在使用到引用的時候,我們需要特別的注意。

          解決方案:

          重新new一個新的 ArrayList 來裝返回的 List

          List strings = new ArrayList<>(Arrays.asList(arr));

          4. java.util.ArrayList如果不正確操作也不支持增刪操作

          在第二個坑的時候,我們說到了 Arrays.asList 返回的 List 不支持增刪操作,

          是因為他的自己實現(xiàn)了一個內(nèi)部類 ArrayList,這個內(nèi)部類繼承了 AbstractList 沒有實現(xiàn) add() 和 remove() 方法導(dǎo)致操作失敗。

          但是第三個坑的時候,我們利用 java.util.ArrayList 包裝了返回的 List,進(jìn)行增刪操作還是會失敗,那是為什么呢?

          刪除方法邏輯:

          刪除方法.png

          在foreach中操作增刪,因為因為 modCount 會被修改,與第一步保存的數(shù)組修改次數(shù)不一致,拋出異常 ConcurrentModificationException

          在正確操作是什么?我總結(jié)了四種方式

          正確操作.png

          5. ArrayList中的 subList 強轉(zhuǎn) ArrayList 導(dǎo)致異常

          阿里《Java開發(fā)手冊》上提過

          [強制] ArrayList的sublist結(jié)果不可強轉(zhuǎn)成ArrayList,否則會拋出ClassCastException

          異常,即java.util.RandomAccesSubList cannot be cast to java. util.ArrayList.

          說明: subList 返回的是ArrayList 的內(nèi)部類SubList, 并不是ArrayList ,而是

          ArrayList的一個視圖,対于SubList子列表的所有操作最終會反映到原列表上。

          private static void subListTest(){

              List<String> names = new ArrayList<String>() {{

              add("one");

              add("two");

              add("three");

          }};
              ArrayList strings = (ArrayList) names.subList(01);
              System.out.println(strings.toString());
          }

          Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

          我猜問題是有八九就是出現(xiàn)在subList這個方法上了

          private class SubList extends AbstractList<Eimplements RandomAccess {

              private final AbstractList<E> parent;
              private final int parentOffset;
              private final int offset;
              int size;
              SubList(AbstractList<E> parent,

              int offset, int fromIndex, int toIndex) {
              this.parent = parent;
              this.parentOffset = fromIndex;
              this.offset = offset + fromIndex;
              this.size = toIndex - fromIndex;
              this.modCount = ArrayList.this.modCount;
          }
          }

          其實 SubList 是一個繼承 AbstractList 的內(nèi)部類,在 SubList 的構(gòu)建函數(shù)中的將 List 中的部分屬性直接賦予給自己

          SubList 沒有創(chuàng)建一個新的 List,而是直接引用了原來的 List(this.parent = parent),指定了元素的范圍

          所以 subList 方法不能直接轉(zhuǎn)成 ArrayList,他只是ArrayList的內(nèi)部類,沒有其他的關(guān)系

          因為是引用的關(guān)系,所以在這里也需要特別的注意,如果對原來的List進(jìn)行修改,會對產(chǎn)生的 subList結(jié)果產(chǎn)生影響。

          List<String> names = new ArrayList<String>() {{
              add("one");
              add("two");
              add("three");
          }};

          List strings = names.subList(01);

          strings.add(0"ongChange");

          System.out.println(strings.toString());

          System.out.println(names.toString());

          [ongChange, one]

          [ongChange, one, two, three]

          對subList產(chǎn)生的List做出結(jié)構(gòu)型修改,操作會反應(yīng)到原來的List上,ongChange也添加到了names中

          如果修改原來的List則會拋出異常ConcurrentModificationException

          List<String> names = new ArrayList<String>() {{

              add("one");
              add("two");
              add("three");

          }};

          List strings = names.subList(01);

          names.add("four");

          System.out.println(strings.toString());

          System.out.println(names.toString());

          Exception in thread "main" java.util.ConcurrentModificationException

          原因:

          subList的時候記錄this.modCount為3

          ConcurrentModificationException1.png

          原來的List插入了一個新元素,導(dǎo)致this.modCount不第一次保存的不一致則拋出異常

          解決方案:在操作SubList的時候,new一個新的ArrayList來接收創(chuàng)建subList結(jié)果的拷貝

          List strings = new ArrayList(names.subList(01));

          6. ArrayList中的subList切片造成OOM

          在業(yè)務(wù)開發(fā)中的時候,他們經(jīng)常通過subList來獲取所需要的那部分?jǐn)?shù)據(jù)

          在上面的例子中,我們知道了subList所產(chǎn)生的List,其實是對原來List對象的引用

          這個產(chǎn)生的List只是原來List對象的視圖,也就是說雖然值切片獲取了一小段數(shù)據(jù),但是原來的List對象卻得不到回收,這個原來的List對象可能是一個很大的對象

          為了方便我們測試,將vm調(diào)整一下 -Xms20m -Xmx40m

          private static void subListOomTest(){

          IntStream.range(01000).forEach(i ->{

          List<Integer> collect = IntStream.range(0100000).boxed().collect(Collectors.toList());

          data.add(collect.subList(01));

          });

          }}

          Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

          出現(xiàn)OOM的原因,循環(huán)1000次創(chuàng)建了1000個具有10萬個元素的List

          因為始終被collect.subList(0, 1)強引用,得不到回收

          解決方式:

          1. 在subList方法返回SubList,重新使用new ArrayList,來構(gòu)建一個獨立的ArrayList
          List list = new ArrayList<>(collect.subList(01));

          1. 利用Java8的Stream中的skip和limit來達(dá)到切片的目的
          List list = collect.stream().skip(0).limit(1).collect(Collectors.toList());

          在這里我們可以看到,只要用一個新的容器來裝結(jié)果,就可以切斷與原始List的關(guān)系

          7. LinkedList的插入速度不一定比ArrayList快

          學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的時候,我們就已經(jīng)得出了結(jié)論

          ●對于數(shù)組,隨機元素訪問的時間復(fù)雜度是0(1), 元素插入操作是O(n);

          ●對于鏈表,隨機元素訪問的時間復(fù)雜度是O(n), 元素插入操作是0(1).

          元素插入對于鏈表來說應(yīng)該是他的優(yōu)勢

          但是他就一定比數(shù)組快? 我們執(zhí)行插入1000w次的操作

          private static void test(){
              StopWatch stopWatch = new StopWatch();
              int elementCount = 100000;
              stopWatch.start("ArrayList add");
              List<Integer> arrayList = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(ArrayList::new));
              // ArrayList插入數(shù)據(jù)
              IntStream.rangeClosed(0, elementCount).forEach(i ->arrayList.add(ThreadLocalRandom.current().nextInt(elementCount), 1));
              stopWatch.stop();

              stopWatch.start("linkedList add");
              List<Integer> linkedList = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(LinkedList::new));
              // ArrayList插入數(shù)據(jù)
              IntStream.rangeClosed(0, elementCount).forEach(i -> linkedList.add(ThreadLocalRandom.current().nextInt(elementCount), 1));
              stopWatch.stop();
              System.out.println(stopWatch.prettyPrint());
          }

          StopWatch '': running time = 44507882 ns
          ---------------------------------------------
          ns         %     Task name
          ---------------------------------------------
          043836412  098%  elementCount 100 ArrayList add
          000671470  002%  elementCount 100 linkedList add

          StopWatch '': running time = 196325261 ns
          ---------------------------------------------
          ns         %     Task name
          ---------------------------------------------
          053848980  027%  elementCount 10000 ArrayList add
          142476281  073%  elementCount 10000 linkedList add

          StopWatch '': running time = 26384216979 ns
          ---------------------------------------------
          ns         %     Task name
          ---------------------------------------------
          978501580  004%  elementCount 100000 ArrayList add
          25405715399  096%  elementCount 100000 linkedList add

          看到在執(zhí)行插入1萬、10完次操作的時候,LinkedList的插入操作時間是 ArrayList的兩倍以上

          那問題主要就是出現(xiàn)在linkedList的 add()方法上

          public void add(int index, E element) {

              checkPositionIndex(index);

              if (index == size)
                  linkLast(element);
              else
                  linkBefore(element, node(index));
          }
              
          /**
          * Returns the (non-null) Node at the specified element index.
              */

          Node<E> node(int index) {

              // assert isElementIndex(index);

              if(index < (size >> 1)) {
                  Node<E> x = first;
                  for (int i = 0; i < index; i++)
                      x = x.next;
                  return x;
              } else {
                  Node<E> x = last;
                  for (int i = size - 1; i > index; i--)
                      x = x.prev;
                  return x;
              }
          }

          linkedList的 add()方法主要邏輯

          1. 通過遍歷找到那個節(jié)點的Node
          2. 執(zhí)行插入操作

          ArrayList的 add()方法

          public void add(int index, E element) {
              rangeCheckForAdd(index);

              ensureCapacityInternal(size + 1);  // Increments modCount!!
              System.arraycopy(elementData, index, elementData, index + 1,
                               size - index);
              elementData[index] = element;
              size++;
          }
          復(fù)制代碼
          1. 計算最小容量
          2. 最小容量大于數(shù)組對象,則進(jìn)行擴容
          3. 進(jìn)行數(shù)組復(fù)制,根據(jù)插入的index將數(shù)組向后移動一位
          4. 最后在空位上插入新值

          根據(jù)試驗的測試,我們得出了在實際的隨機插入中,LinkedList并沒有比ArrayList的速度快

          所以在實際的使用中,如果涉及到頭尾對象的操作,可以使用LinkedList數(shù)據(jù)結(jié)構(gòu)來進(jìn)行增刪的操作,發(fā)揮LinkedList的優(yōu)勢

          最好再進(jìn)行實際的性能測試評估,來得到最合適的數(shù)據(jù)結(jié)構(gòu)。

          8. CopyOnWriteArrayList內(nèi)存占用過多

          CopyOnWrite,顧名思義就是寫的時候會將共享變量新復(fù)制一份出來,這樣做的好處是讀操作完全無鎖。

          CopyOnWriteArrayList的add()方法

          public boolean add(E e) {
              // 獲取獨占鎖
              final ReentrantLock lock = this.lock;
              lock.lock();
              try {
                  // 獲取array
                  Object[] elements = getArray();
                  // 復(fù)制array到新數(shù)組,添加元素到新數(shù)組
                  int len = elements.length;
                  Object[] newElements = Arrays.copyOf(elements, len + 1);
                  newElements[len] = e;
                  // 替換數(shù)組
                  setArray(newElements);
                  return true;
              } finally {
                  // 釋放鎖
                  lock.unlock();
              }
          }

          CopyOnWriteArrayList 內(nèi)部維護了一個數(shù)組,成員變量 array 就指向這個內(nèi)部數(shù)組,所有的讀操作都是基于新的array對象進(jìn)行的。

          因為上了獨占鎖,所以如果多個線程調(diào)用add()方法只有一個線程會獲得到該鎖,其他線程被阻塞,知道鎖被釋放, 由于加了鎖,所以整個操作的過程是原子性操作

          CopyOnWriteArrayList 會將 新的array復(fù)制一份,然后在新復(fù)制處理的數(shù)組上執(zhí)行增加元素的操作,執(zhí)行完之后再將復(fù)制的結(jié)果指向這個新的數(shù)組。

          由于每次寫入的時候都會對數(shù)組對象進(jìn)行復(fù)制,復(fù)制過程不僅會占用雙倍內(nèi)存,還需要消耗 CPU 等資源,所以當(dāng)列表中的元素比較少的時候,這對內(nèi)存和 GC 并沒有多大影響,但是當(dāng)列表保存了大量元素的時候,

          對 CopyOnWriteArrayList 每一次修改,都會重新創(chuàng)建一個大對象,并且原來的大對象也需要回收,這都可能會觸發(fā) GC,如果超過老年代的大小則容易觸發(fā)Full GC,引起應(yīng)用程序長時間停頓。

          9. CopyOnWriteArrayList是弱一致性的

          public Iterator<E> iterator() {
              return new COWIterator<E>(getArray(), 0);
          }

          static final class COWIterator<Eimplements ListIterator<E{
              /** Snapshot of the array */
              private final Object[] snapshot;
              /** Index of element to be returned by subsequent call to next.  */
              private int cursor;

              private COWIterator(Object[] elements, int initialCursor) {
                  cursor = initialCursor;
                  snapshot = elements;
              }

              public boolean hasNext() {
                  return cursor < snapshot.length;
              }

              public boolean hasPrevious() {
                  return cursor > 0;
              }

              @SuppressWarnings("unchecked")
              public E next() {
                  if (! hasNext())
                      throw new NoSuchElementException();
                  return (E) snapshot[cursor++];
              }

          調(diào)用iterator方法獲取迭代器返回一個COWIterator對象

          COWIterator的構(gòu)造器里主要是 保存了當(dāng)前的list對象的內(nèi)容和遍歷list時數(shù)據(jù)的下標(biāo)。

          snapshot是list的快照信息,因為CopyOnWriteArrayList的讀寫策略中都會使用getArray()來獲取一個快照信息,生成一個新的數(shù)組。

          所以在使用該迭代器元素時,其他線程對該lsit操作是不可見的,因為操作的是兩個不同的數(shù)組所以造成弱一致性。

          private static void CopyOnWriteArrayListTest(){
              CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList();
              list.add("test1");
              list.add("test2");
              list.add("test3");
              list.add("test4");
              
              Thread thread = new Thread(() -> {
                  System.out.println(">>>> start");
                  list.add(1"replaceTest");
                  list.remove(2);
              });
              
              // 在啟動線程前獲取迭代器
              Iterator<String> iterator = list.iterator();

              thread.start();

              try {
                  // 等待線程執(zhí)行完畢
                  thread.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }

              while (iterator.hasNext()){
                  System.out.println(iterator.next());
              }
          }

          >>>> start
          test1
          test2
          test3
          test4

          上面的demo中在啟動線程前獲取到了原來list的迭代器,

          在之后啟動新建一個線程,在線程里面修改了第一個元素的值,移除了第二個元素

          在執(zhí)行完子線程之后,遍歷了迭代器的元素,發(fā)現(xiàn)子線程里面操作的一個都沒有生效,這里提現(xiàn)了迭代器弱一致性。

          10. CopyOnWriteArrayList的迭代器不支持增刪改

          private static void CopyOnWriteArrayListTest(){
              CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
              list.add("test1");
              list.add("test2");
              list.add("test3");
              list.add("test4");

              Iterator<String> iterator = list.iterator();

              while (iterator.hasNext()){
                  if ("test1".equals(iterator.next())){
                      iterator.remove();
                  }
              }

              System.out.println(list.toString());
          }

          Exception in thread "main" java.lang.UnsupportedOperationException
           at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)

          CopyOnWriteArrayList 迭代器是只讀的,不支持增刪操作

          CopyOnWriteArrayList迭代器中的 remove()和 add()方法,沒有支持增刪而是直接拋出了異常

          因為迭代器遍歷的僅僅是一個快照,而對快照進(jìn)行增刪改是沒有意義的。

          /**
           * Not supported. Always throws UnsupportedOperationException.
           * @throws UnsupportedOperationException always; {@code remove}
           *         is not supported by this iterator.
           */

          public void remove() {
              throw new UnsupportedOperationException();
          }

          /**
           * Not supported. Always throws UnsupportedOperationException.
           * @throws UnsupportedOperationException always; {@code set}
           *         is not supported by this iterator.
           */

          public void set(E e) {
              throw new UnsupportedOperationException();
          }

          /**
           * Not supported. Always throws UnsupportedOperationException.
           * @throws UnsupportedOperationException always; {@code add}
           *         is not supported by this iterator.
           */

          public void add(E e) {
              throw new UnsupportedOperationException();
          }

          總結(jié)

          由于篇幅的限制,我們只對一些在業(yè)務(wù)開發(fā)中常見的關(guān)鍵點進(jìn)行梳理和介紹

          在實際的工作中,我們不單單是要清除不同類型容器的特性,還要選擇適合的容器才能做到事半功倍。

          我們主要介紹了Arrays.asList轉(zhuǎn)換過程中的一些坑,以及因為操作不當(dāng)造成的OOM和異常,

          到最后介紹了線程安全類CopyOnWriteArrayList的一些坑,讓我們認(rèn)識到在豐富的API下藏著許多的陷阱。

          在使用的過程中,需要更加充分的考慮避免這些隱患的發(fā)生。

          最后一張思維導(dǎo)圖來回顧一下~

          List中的坑.png

          加小編微信,回復(fù) 40 白嫖40套 java/spring/kafka/redis/netty 教程/代碼/視頻 等


          掃二維碼,加我微信,回復(fù):40

           注意,不要亂回復(fù) 

          沒錯,不是機器人
          記得一定要等待,等待才有好東西

          瀏覽 51
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲黄色影片 | 亚洲区色情区激情区小说纯熟调抖 | 国产又粗又猛视频 | 欧美日韩视频高清 | 黄色视频在线免费直播 |