欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一篇文章細數(shù)Java中List的10個坑

 更新時間:2025年11月07日 10:11:42   作者:吳名氏.  
在Java中List是一個有序集合,允許存儲重復的元素,它是Java集合框架的一部分,提供了對集合進行各種操作的接口,這篇文章主要介紹了Java中List10個坑的相關資料,文中通過代碼介紹的非常詳細,需要的朋友可以參考下

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

1.1 錯誤還原

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

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

看代碼:

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

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

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

可以觀察到 asList方法 接收的是一個泛型T類型的參數(shù),T繼承Object對象
所以通過斷點我們可以看到把 int數(shù)組 整體作為一個對象,返回了一個 List<int[]>

1.2 解決方案

方案一: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 = {1, 2, 3};
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ù)結構之后,竟然不能進行增刪操作了

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());
}

[1, 2, 4]
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)

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

拋出異常信息UnsupportedOperationException。

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

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

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

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.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<E> extends AbstractCollection<E> implements 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

3.1 錯誤還原

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

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

我們將字符串數(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也跟著變化

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

3.2 解決方案

重新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() 方法導致操作失敗。

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

刪除方法邏輯:

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

在正確操作是什么?總結了四種方式

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

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

[強制] ArrayList的sublist結果不可強轉(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(0, 1);
    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<E> implements 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 的構建函數(shù)中的將 List 中的部分屬性直接賦予給自己

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

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

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

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

List strings = names.subList(0, 1);

strings.add(0, "ongChange");

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

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

[ongChange, one]

[ongChange, one, two, three]

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

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

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

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

}};

List strings = names.subList(0, 1);

names.add("four");

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

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

Exception in thread "main" java.util.ConcurrentModificationException

原因:

subList的時候記錄this.modCount為3

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

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

List strings = new ArrayList(names.subList(0, 1));

6 ArrayList中的subList切片造成OOM

6.1 問題還原

在業(yè)務開發(fā)中的時候,他們經(jīng)常通過subList來獲取所需要的那部分數(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(0, 1000).forEach(i ->{
List<Integer> collect = IntStream.range(0, 100000).boxed().collect(Collectors.toList());
data.add(collect.subList(0, 1));
});
}}
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)強引用,得不到回收

6.2 解決方案

1.在subList方法返回SubList,重新使用new ArrayList,來構建一個獨立的ArrayList

List list = new ArrayList<>(collect.subList(0, 1));

2.利用Java8的Stream中的skip和limit來達到切片的目的

List list = collect.stream().skip(0).limit(1).collect(Collectors.toList());

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

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

學習數(shù)據(jù)結構的時候,我們就已經(jīng)得出了結論

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

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

元素插入對于鏈表來說應該是他的優(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()方法主要邏輯

  • 通過遍歷找到那個節(jié)點的Node

  • 執(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++;
}
  • 計算最小容量
  • 最小容量大于數(shù)組對象,則進行擴容
  • 進行數(shù)組復制,根據(jù)插入的index將數(shù)組向后移動一位
  • 最后在空位上插入新值

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

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

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

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

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

CopyOnWriteArrayList的add()方法

public boolean add(E e) {
    // 獲取獨占鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 獲取array
        Object[] elements = getArray();
        // 復制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對象進行的。

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

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

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

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

9 CopyOnWriteArrayList是弱一致性的

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

static final class COWIterator<E> implements 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的構造器里主要是 保存了當前的list對象的內(nèi)容和遍歷list時數(shù)據(jù)的下標。

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()方法,沒有支持增刪而是直接拋出了異常

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

/**
 * 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();
}

11 總結

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

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

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

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

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

最后一張思維導圖來回顧一下~

到此這篇關于Java中List的10個坑的文章就介紹到這了,更多相關Java中List坑內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Java使用EasyExcel動態(tài)添加自增序號列

    Java使用EasyExcel動態(tài)添加自增序號列

    本文將介紹如何通過使用EasyExcel自定義攔截器實現(xiàn)在最終的Excel文件中新增一列自增的序號列,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • SpringBoot依賴注入的三種方式

    SpringBoot依賴注入的三種方式

    本文將通過代碼示例詳細介紹SpringBoot依賴注入的三種方式,對學習依賴注入有一定的參考價值,需要的朋友可以參考一下
    2023-04-04
  • java操作Apache druid的實例代碼

    java操作Apache druid的實例代碼

    這篇文章主要介紹了java操作Apache druid的實例代碼,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-11-11
  • Springboot?返回文件給前端的示例代碼

    Springboot?返回文件給前端的示例代碼

    這篇文章主要介紹了Springboot?返回文件給前端的示例代碼,本文結合示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-07-07
  • java Person,Student,GoodStudent 三個類的繼承、構造函數(shù)的執(zhí)行

    java Person,Student,GoodStudent 三個類的繼承、構造函數(shù)的執(zhí)行

    這篇文章主要介紹了java Person,Student,GoodStudent 三個類的繼承、構造函數(shù)的執(zhí)行,需要的朋友可以參考下
    2017-02-02
  • Java語法關于泛型與類型擦除的分析

    Java語法關于泛型與類型擦除的分析

    泛型沒有其看起來那么深不可測,它并不神秘與神奇,泛型是Java 中一個很小巧的概念,但同時也是一個很容易讓人迷惑的知識點,它讓人迷惑的地方在于它的許多表現(xiàn)有點違反直覺
    2021-09-09
  • Java DOM4J方式生成XML的方法

    Java DOM4J方式生成XML的方法

    今天小編就為大家分享一篇Java DOM4J方式生成XML的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-07-07
  • Spring狀態(tài)機 Statemachine使用小結

    Spring狀態(tài)機 Statemachine使用小結

    狀態(tài)機通過狀態(tài)、事件、配置管理流程,分離業(yè)務邏輯與狀態(tài)控制,實現(xiàn)結構化流轉(zhuǎn),本文主要介紹了Spring狀態(tài)機Statemachine使用小結,感興趣的可以了解一下
    2025-09-09
  • Java中Validated、Valid 、Validator區(qū)別詳解

    Java中Validated、Valid 、Validator區(qū)別詳解

    本文主要介紹了Java中Validated、Valid 、Validator區(qū)別,有時候面試的時候會被問到,他們的區(qū)別你知道幾個,本文就來詳細的介紹一下
    2021-08-08
  • Java實現(xiàn)獲取內(nèi)網(wǎng)的所有IP地址

    Java實現(xiàn)獲取內(nèi)網(wǎng)的所有IP地址

    這篇文章主要介紹了如何利用Java語言實現(xiàn)獲取內(nèi)網(wǎng)的所有IP地址,文中的示例代碼講解詳細,對我們學習有一定的參考價值,快跟隨小編一起學習一下吧
    2022-06-06

最新評論