日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當(dāng)前位置:首頁 > 科技  > 軟件

小小ArrayList,居然這么多坑?!

來源: 責(zé)編: 時(shí)間:2024-04-02 17:19:01 183觀看
導(dǎo)讀今天,我來和你說說 List 列表操作有哪些坑。Java 的集合類包括 Map 和 Collection 兩大類。Collection 包括 List、Set 和 Queue 三個(gè)小類,其中 List 列表集合是最重要也是所有業(yè)務(wù)代碼都會(huì)用到的。今天,我們就從把數(shù)組

今天,我來和你說說 List 列表操作有哪些坑。taz28資訊網(wǎng)——每日最新資訊28at.com

Java 的集合類包括 Map 和 Collection 兩大類。Collection 包括 List、Set 和 Queue 三個(gè)小類,其中 List 列表集合是最重要也是所有業(yè)務(wù)代碼都會(huì)用到的。今天,我們就從把數(shù)組轉(zhuǎn)換為 List 集合、對(duì) List 進(jìn)行切片操作、List 搜索的性能問題等幾個(gè)方面著手,來聊聊其中最可能遇到的一些坑。taz28資訊網(wǎng)——每日最新資訊28at.com

使用 Arrays.asList 把數(shù)據(jù)轉(zhuǎn)換為 List 的三個(gè)坑

Java 8 中 Stream 流式處理的各種功能,大大減少了集合類各種操作(投影、過濾、轉(zhuǎn)換)的代碼量。所以,在業(yè)務(wù)開發(fā)中,我們常常會(huì)把原始的數(shù)組轉(zhuǎn)換為 List 類數(shù)據(jù)結(jié)構(gòu),來繼續(xù)展開各種 Stream 操作。taz28資訊網(wǎng)——每日最新資訊28at.com

你可能也想到了,使用 Arrays.asList 方法可以把數(shù)組一鍵轉(zhuǎn)換為 List,但其實(shí)沒這么簡(jiǎn)單。接下來,就讓我們看看其中的緣由,以及使用 Arrays.asList 把數(shù)組轉(zhuǎn)換為 List 的幾個(gè)坑。taz28資訊網(wǎng)——每日最新資訊28at.com

在如下代碼中,我們初始化三個(gè)數(shù)字的 int[]數(shù)組,然后使用 Arrays.asList 把數(shù)組轉(zhuǎn)換為 List:taz28資訊網(wǎng)——每日最新資訊28at.com

int[] arr = {1, 2, 3};List list = Arrays.asList(arr);log.info("list:{} size:{} class:{}", list, list.size(), list.get(0).getClass());

但,這樣初始化的 List 并不是我們期望的包含 3 個(gè)數(shù)字的 List。通過日志可以發(fā)現(xiàn),這個(gè) List 包含的其實(shí)是一個(gè) int 數(shù)組,整個(gè) List 的元素個(gè)數(shù)是 1,元素類型是整數(shù)數(shù)組。taz28資訊網(wǎng)——每日最新資訊28at.com

12:50:39.445 [main] INFO AsListApplication - list:[[I@1c53fd30] size:1 class:class [I

其原因是,只能是把 int 裝箱為 Integer,不可能把 int 數(shù)組裝箱為 Integer 數(shù)組。我們知道,Arrays.asList 方法傳入的是一個(gè)泛型 T 類型可變參數(shù),最終 int 數(shù)組整體作為了一個(gè)對(duì)象成為了泛型類型 T:taz28資訊網(wǎng)——每日最新資訊28at.com

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

直接遍歷這樣的 List 必然會(huì)出現(xiàn) Bug,修復(fù)方式有兩種,如果使用 Java8 以上版本可以使用 Arrays.stream 方法來轉(zhuǎn)換,否則可以把 int 數(shù)組聲明為包裝類型 Integer 數(shù)組:taz28資訊網(wǎng)——每日最新資訊28at.com

int[] arr1 = {1, 2, 3};List list1 = Arrays.stream(arr1).boxed().collect(Collectors.toList());log.info("list:{} size:{} class:{}", list1, list1.size(), list1.get(0).getClass());Integer[] arr2 = {1, 2, 3};List list2 = Arrays.asList(arr2);log.info("list:{} size:{} class:{}", list2, list2.size(), list2.get(0).getClass());

修復(fù)后的代碼得到如下日志,可以看到 List 具有三個(gè)元素,元素類型是 Integer:taz28資訊網(wǎng)——每日最新資訊28at.com

13:10:57.373 [main] INFO AsListApplication - list:[1, 2, 3] size:3 class:class java.lang.Integer

可以看到第一個(gè)坑是,不能直接使用 Arrays.asList 來轉(zhuǎn)換基本類型數(shù)組。 那么,我們獲得了正確的 List,是不是就可以像普通的 List 那樣使用了呢?我們繼續(xù)往下看。taz28資訊網(wǎng)——每日最新資訊28at.com

把三個(gè)字符串 1、2、3 構(gòu)成的字符串?dāng)?shù)組,使用 Arrays.asList 轉(zhuǎn)換為 List 后,將原始字符串?dāng)?shù)組的第二個(gè)字符修改為 4,然后為 List 增加一個(gè)字符串 5,最后數(shù)組和 List 會(huì)是怎樣呢?taz28資訊網(wǎng)——每日最新資訊28at.com

String[] arr = {"1", "2", "3"};List list = Arrays.asList(arr);arr[1] = "4";try {    list.add("5");} catch (Exception ex) {    ex.printStackTrace();}log.info("arr:{} list:{}", Arrays.toString(arr), list);

可以看到,日志里有一個(gè) UnsupportedOperationException,為 List 新增字符串 5 的操作失敗了,而且把原始數(shù)組的第二個(gè)元素從 2 修改為 4 后,asList 獲得的 List 中的第二個(gè)元素也被修改為 4 了:taz28資訊網(wǎng)——每日最新資訊28at.com

java.lang.UnsupportedOperationException  at java.util.AbstractList.add(AbstractList.java:148)  at java.util.AbstractList.add(AbstractList.java:108)  at AsListApplication.wrong2(AsListApplication.java:41)  at AsListApplication.main(AsListApplication.java:15)13:15:34.699 [main] INFO AsListApplication - arr:[1, 4, 3] list:[1, 4, 3]

這里,又引出了兩個(gè)坑。taz28資訊網(wǎng)——每日最新資訊28at.com

第二個(gè)坑,Arrays.asList 返回的 List 不支持增刪操作。 Arrays.asList 返回的 List 并不是我們期望的 java.util.ArrayList,而是 Arrays 的內(nèi)部類 ArrayList。ArrayList 內(nèi)部類繼承自 AbstractList 類,并沒有覆寫父類的 add 方法,而父類中 add 方法的實(shí)現(xiàn),就是拋出 UnsupportedOperationException。相關(guān)源碼如下所示:taz28資訊網(wǎng)——每日最新資訊28at.com

public static <T> List<T> asList(T... a) {    return new ArrayList<>(a);}private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable {    private final E[] a;    ArrayList(E[] array) {        a = Objects.requireNonNull(array);    }...    @Override    public E set(int index, E element) {        E oldValue = a[index];        a[index] = element;        return oldValue;    }    ...}public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {...public void add(int index, E element) {        throw new UnsupportedOperationException();    }}

第三個(gè)坑,對(duì)原始數(shù)組的修改會(huì)影響到我們獲得的那個(gè) List。 看一下 ArrayList 的實(shí)現(xiàn),可以發(fā)現(xiàn) ArrayList 其實(shí)是直接使用了原始的數(shù)組。所以,我們要特別小心,把通過 Arrays.asList 獲得的 List 交給其他方法處理,很容易因?yàn)楣蚕砹藬?shù)組,相互修改產(chǎn)生 Bug。taz28資訊網(wǎng)——每日最新資訊28at.com

修復(fù)方式比較簡(jiǎn)單,重新 new 一個(gè) ArrayList 初始化 Arrays.asList 返回的 List 即可:taz28資訊網(wǎng)——每日最新資訊28at.com

String[] arr = {"1", "2", "3"};List list = new ArrayList(Arrays.asList(arr));arr[1] = "4";try {    list.add("5");} catch (Exception ex) {    ex.printStackTrace();}log.info("arr:{} list:{}", Arrays.toString(arr), list);

修改后的代碼實(shí)現(xiàn)了原始數(shù)組和 List 的“解耦”,不再相互影響。同時(shí),因?yàn)椴僮鞯氖钦嬲?ArrayList,add 也不再出錯(cuò):taz28資訊網(wǎng)——每日最新資訊28at.com

13:34:50.829 [main] INFO AsListApplication - arr:[1, 4, 3] list:[1, 2, 3, 5]

使用 List.subList 進(jìn)行切片操作居然會(huì)導(dǎo)致 OOM?

業(yè)務(wù)開發(fā)時(shí)常常要對(duì) List 做切片處理,即取出其中部分元素構(gòu)成一個(gè)新的 List,我們通常會(huì)想到使用 List.subList 方法。但,和 Arrays.asList 的問題類似,List.subList 返回的子 List 不是一個(gè)普通的 ArrayList。這個(gè)子 List 可以認(rèn)為是原始 List 的視圖,會(huì)和原始 List 相互影響。如果不注意,很可能會(huì)因此產(chǎn)生 OOM 問題。接下來,我們就一起分析下其中的坑。taz28資訊網(wǎng)——每日最新資訊28at.com

如下代碼所示,定義一個(gè)名為 data 的靜態(tài) List 來存放 Integer 的 List,也就是說 data 的成員本身是包含了多個(gè)數(shù)字的 List。循環(huán) 1000 次,每次都從一個(gè)具有 10 萬個(gè) Integer 的 List 中,使用 subList 方法獲得一個(gè)只包含一個(gè)數(shù)字的子 List,并把這個(gè)子 List 加入 data 變量:taz28資訊網(wǎng)——每日最新資訊28at.com

private static List<List<Integer>> data = new ArrayList<>();private static void oom() {    for (int i = 0; i < 1000; i++) {        List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());        data.add(rawList.subList(0, 1));    }}

你可能會(huì)覺得,這個(gè) data 變量里面最終保存的只是 1000 個(gè)具有 1 個(gè)元素的 List,不會(huì)占用很大空間,但程序運(yùn)行不久就出現(xiàn)了 OOM:taz28資訊網(wǎng)——每日最新資訊28at.com

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space  at java.util.Arrays.copyOf(Arrays.java:3181)  at java.util.ArrayList.grow(ArrayList.java:265)

出現(xiàn) OOM 的原因是,循環(huán)中的 1000 個(gè)具有 10 萬個(gè)元素的 List 始終得不到回收,因?yàn)樗冀K被 subList 方法返回的 List 強(qiáng)引用。那么,返回的子 List 為什么會(huì)強(qiáng)引用原始的 List,它們又有什么關(guān)系呢?我們?cè)倮^續(xù)做實(shí)驗(yàn)觀察一下這個(gè)子 List 的特性。taz28資訊網(wǎng)——每日最新資訊28at.com

首先初始化一個(gè)包含數(shù)字 1 到 10 的 ArrayList,然后通過調(diào)用 subList 方法取出 2、3、4;隨后刪除這個(gè) SubList 中的元素?cái)?shù)字 3,并打印原始的 ArrayList;最后為原始的 ArrayList 增加一個(gè)元素?cái)?shù)字 0,遍歷 SubList 輸出所有元素:taz28資訊網(wǎng)——每日最新資訊28at.com

List<Integer> list = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList());List<Integer> subList = list.subList(1, 4);System.out.println(subList);subList.remove(1);System.out.println(list);list.add(0);try {    subList.forEach(System.out::println);} catch (Exception ex) {    ex.printStackTrace();}

代碼運(yùn)行后得到如下輸出:taz28資訊網(wǎng)——每日最新資訊28at.com

[2, 3, 4][1, 2, 4, 5, 6, 7, 8, 9, 10]java.util.ConcurrentModificationException  at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)  at java.util.ArrayList$SubList.listIterator(ArrayList.java:1099)  at java.util.AbstractList.listIterator(AbstractList.java:299)  at java.util.ArrayList$SubList.iterator(ArrayList.java:1095)  at java.lang.Iterable.forEach(Iterable.java:74)

可以看到兩個(gè)現(xiàn)象:taz28資訊網(wǎng)——每日最新資訊28at.com

原始 List 中數(shù)字 3 被刪除了,說明刪除子 List 中的元素影響到了原始 List;taz28資訊網(wǎng)——每日最新資訊28at.com

嘗試為原始 List 增加數(shù)字 0 之后再遍歷子 List,會(huì)出現(xiàn) ConcurrentModificationException。taz28資訊網(wǎng)——每日最新資訊28at.com

我們分析下 ArrayList 的源碼,看看為什么會(huì)是這樣。taz28資訊網(wǎng)——每日最新資訊28at.com

public class ArrayList<E> extends AbstractList<E>        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {    protected transient int modCount = 0;  private void ensureExplicitCapacity(int minCapacity) {        modCount++;        // overflow-conscious code        if (minCapacity - elementData.length > 0)            grow(minCapacity);    }  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++;  }  public List<E> subList(int fromIndex, int toIndex) {    subListRangeCheck(fromIndex, toIndex, size);    return new SubList(this, offset, fromIndex, toIndex);  }  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;    }        public E set(int index, E element) {            rangeCheck(index);            checkForComodification();            return l.set(index+offset, element);        }    public ListIterator<E> listIterator(final int index) {                checkForComodification();                ...    }    private void checkForComodification() {        if (ArrayList.this.modCount != this.modCount)            throw new ConcurrentModificationException();    }    ...  }}

第一,ArrayList 維護(hù)了一個(gè)叫作 modCount 的字段,表示集合結(jié)構(gòu)性修改的次數(shù)。所謂結(jié)構(gòu)性修改,指的是影響 List 大小的修改,所以 add 操作必然會(huì)改變 modCount 的值。taz28資訊網(wǎng)——每日最新資訊28at.com

第二,分析第 21 到 24 行的 subList 方法可以看到,獲得的 List 其實(shí)是內(nèi)部類 SubList,并不是普通的 ArrayList,在初始化的時(shí)候傳入了 this。taz28資訊網(wǎng)——每日最新資訊28at.com

第三,分析第 26 到 39 行代碼可以發(fā)現(xiàn),這個(gè) SubList 中的 parent 字段就是原始的 List。SubList 初始化的時(shí)候,并沒有把原始 List 中的元素復(fù)制到獨(dú)立的變量中保存。我們可以認(rèn)為 SubList 是原始 List 的視圖,并不是獨(dú)立的 List。雙方對(duì)元素的修改會(huì)相互影響,而且 SubList 強(qiáng)引用了原始的 List,所以大量保存這樣的 SubList 會(huì)導(dǎo)致 OOM。taz28資訊網(wǎng)——每日最新資訊28at.com

第四,分析第 47 到 55 行代碼可以發(fā)現(xiàn),遍歷 SubList 的時(shí)候會(huì)先獲得迭代器,比較原始 ArrayList modCount 的值和 SubList 當(dāng)前 modCount 的值。獲得了 SubList 后,我們?yōu)樵?List 新增了一個(gè)元素修改了其 modCount,所以判等失敗拋出 ConcurrentModificationException 異常。taz28資訊網(wǎng)——每日最新資訊28at.com

既然 SubList 相當(dāng)于原始 List 的視圖,那么避免相互影響的修復(fù)方式有兩種:taz28資訊網(wǎng)——每日最新資訊28at.com

一種是,不直接使用 subList 方法返回的 SubList,而是重新使用 new ArrayList,在構(gòu)造方法傳入 SubList,來構(gòu)建一個(gè)獨(dú)立的 ArrayList;taz28資訊網(wǎng)——每日最新資訊28at.com

另一種是,對(duì)于 Java 8 使用 Stream 的 skip 和 limit API 來跳過流中的元素,以及限制流中元素的個(gè)數(shù),同樣可以達(dá)到 SubList 切片的目的。taz28資訊網(wǎng)——每日最新資訊28at.com

//方式一:List<Integer> subList = new ArrayList<>(list.subList(1, 4));//方式二:List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());

修復(fù)后代碼輸出如下:taz28資訊網(wǎng)——每日最新資訊28at.com

[2, 3, 4][1, 2, 3, 4, 5, 6, 7, 8, 9, 10]24

可以看到,刪除 SubList 的元素不再影響原始 List,而對(duì)原始 List 的修改也不會(huì)再出現(xiàn) List 迭代異常。taz28資訊網(wǎng)——每日最新資訊28at.com

一定要讓合適的數(shù)據(jù)結(jié)構(gòu)做合適的事情

在使用 List 集合類的時(shí)候,不注意使用場(chǎng)景也會(huì)遇見兩個(gè)常見誤區(qū)。taz28資訊網(wǎng)——每日最新資訊28at.com

第一個(gè)誤區(qū)是,使用數(shù)據(jù)結(jié)構(gòu)不考慮平衡時(shí)間和空間。taz28資訊網(wǎng)——每日最新資訊28at.com

首先,定義一個(gè)只有一個(gè) int 類型訂單號(hào)字段的 Order 類:taz28資訊網(wǎng)——每日最新資訊28at.com

@Data@NoArgsConstructor@AllArgsConstructorstatic class Order {    private int orderId;}

然后,定義一個(gè)包含 elementCount 和 loopCount 兩個(gè)參數(shù)的 listSearch 方法,初始化一個(gè)具有 elementCount 個(gè)訂單對(duì)象的 ArrayList,循環(huán) loopCount 次搜索這個(gè) ArrayList,每次隨機(jī)搜索一個(gè)訂單號(hào):taz28資訊網(wǎng)——每日最新資訊28at.com

private static Object listSearch(int elementCount, int loopCount) {    List<Order> list = IntStream.rangeClosed(1, elementCount).mapToObj(i -> new Order(i)).collect(Collectors.toList());    IntStream.rangeClosed(1, loopCount).forEach(i -> {        int search = ThreadLocalRandom.current().nextInt(elementCount);        Order result = list.stream().filter(order -> order.getOrderId() == search).findFirst().orElse(null);        Assert.assertTrue(result != null && result.getOrderId() == search);    });    return list;}

隨后,定義另一個(gè) mapSearch 方法,從一個(gè)具有 elementCount 個(gè)元素的 Map 中循環(huán) loopCount 次查找隨機(jī)訂單號(hào)。Map 的 Key 是訂單號(hào),Value 是訂單對(duì)象:taz28資訊網(wǎng)——每日最新資訊28at.com

private static Object mapSearch(int elementCount, int loopCount) {    Map<Integer, Order> map = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toMap(Function.identity(), i -> new Order(i)));    IntStream.rangeClosed(1, loopCount).forEach(i -> {        int search = ThreadLocalRandom.current().nextInt(elementCount);        Order result = map.get(search);        Assert.assertTrue(result != null && result.getOrderId() == search);    });    return map;}

我們知道,搜索 ArrayList 的時(shí)間復(fù)雜度是 O(n),而 HashMap 的 get 操作的時(shí)間復(fù)雜度是 O(1)。所以,要對(duì)大 List 進(jìn)行單值搜索的話,可以考慮使用 HashMap,其中 Key 是要搜索的值,Value 是原始對(duì)象,會(huì)比使用 ArrayList 有非常明顯的性能優(yōu)勢(shì)。taz28資訊網(wǎng)——每日最新資訊28at.com

如下代碼所示,對(duì) 100 萬個(gè)元素的 ArrayList 和 HashMap,分別調(diào)用 listSearch 和 mapSearch 方法進(jìn)行 1000 次搜索:taz28資訊網(wǎng)——每日最新資訊28at.com

int elementCount = 1000000;int loopCount = 1000;StopWatch stopWatch = new StopWatch();stopWatch.start("listSearch");Object list = listSearch(elementCount, loopCount);System.out.println(ObjectSizeCalculator.getObjectSize(list));stopWatch.stop();stopWatch.start("mapSearch");Object map = mapSearch(elementCount, loopCount);stopWatch.stop();System.out.println(ObjectSizeCalculator.getObjectSize(map));System.out.println(stopWatch.prettyPrint());

通過運(yùn)行結(jié)果可以看到,僅僅是 1000 次搜索,listSearch 方法耗時(shí) 3.3 秒,而 mapSearch 耗時(shí)僅僅 108 毫秒。taz28資訊網(wǎng)——每日最新資訊28at.com

即使我們要搜索的不是單值而是條件區(qū)間,也可以嘗試使用 HashMap 來進(jìn)行“搜索性能優(yōu)化”。如果你的條件區(qū)間是固定的話,可以提前把 HashMap 按照條件區(qū)間進(jìn)行分組,Key 就是不同的區(qū)間。taz28資訊網(wǎng)——每日最新資訊28at.com

的確,如果業(yè)務(wù)代碼中有頻繁的大 ArrayList 搜索,使用 HashMap 性能會(huì)好很多。類似,如果要對(duì)大 ArrayList 進(jìn)行去重操作,也不建議使用 contains 方法,而是可以考慮使用 HashSet 進(jìn)行去重。說到這里,還有一個(gè)問題,使用 HashMap 是否會(huì)犧牲空間呢?taz28資訊網(wǎng)——每日最新資訊28at.com

為此,我們使用 ObjectSizeCalculator 工具打印 ArrayList 和 HashMap 的內(nèi)存占用,可以看到 ArrayList 占用內(nèi)存 21M,而 HashMap 占用的內(nèi)存達(dá)到了 72M,是 List 的三倍多。進(jìn)一步使用 MAT 工具分析堆可以再次證明,ArrayList 在內(nèi)存占用上性價(jià)比很高,77% 是實(shí)際的數(shù)據(jù)(如第 1 個(gè)圖所示,16000000/20861992),而 HashMap 的“含金量”只有 22%。taz28資訊網(wǎng)——每日最新資訊28at.com

所以,在應(yīng)用內(nèi)存吃緊的情況下,我們需要考慮是否值得使用更多的內(nèi)存消耗來換取更高的性能。這里我們看到的是平衡的藝術(shù),空間換時(shí)間,還是時(shí)間換空間,只考慮任何一個(gè)方面都是不對(duì)的。taz28資訊網(wǎng)——每日最新資訊28at.com

第二個(gè)誤區(qū)是,過于迷信教科書的大 O 時(shí)間復(fù)雜度。taz28資訊網(wǎng)——每日最新資訊28at.com

數(shù)據(jù)結(jié)構(gòu)中要實(shí)現(xiàn)一個(gè)列表,有基于連續(xù)存儲(chǔ)的數(shù)組和基于指針串聯(lián)的鏈表兩種方式。在 Java 中,有代表性的實(shí)現(xiàn)是 ArrayList 和 LinkedList,前者背后的數(shù)據(jù)結(jié)構(gòu)是數(shù)組,后者則是(雙向)鏈表。taz28資訊網(wǎng)——每日最新資訊28at.com

在選擇數(shù)據(jù)結(jié)構(gòu)的時(shí)候,我們通常會(huì)考慮每種數(shù)據(jù)結(jié)構(gòu)不同操作的時(shí)間復(fù)雜度,以及使用場(chǎng)景兩個(gè)因素。查看這里,你可以看到數(shù)組和鏈表大 O 時(shí)間復(fù)雜度的顯著差異:taz28資訊網(wǎng)——每日最新資訊28at.com

對(duì)于數(shù)組,隨機(jī)元素訪問的時(shí)間復(fù)雜度是 O(1),元素插入操作是 O(n);taz28資訊網(wǎng)——每日最新資訊28at.com

對(duì)于鏈表,隨機(jī)元素訪問的時(shí)間復(fù)雜度是 O(n),元素插入操作是 O(1)。taz28資訊網(wǎng)——每日最新資訊28at.com

那么,在大量的元素插入、很少的隨機(jī)訪問的業(yè)務(wù)場(chǎng)景下,是不是就應(yīng)該使用 LinkedList 呢?接下來,我們寫一段代碼測(cè)試下兩者隨機(jī)訪問和插入的性能吧。taz28資訊網(wǎng)——每日最新資訊28at.com

定義四個(gè)參數(shù)一致的方法,分別對(duì)元素個(gè)數(shù)為 elementCount 的 LinkedList 和 ArrayList,循環(huán) loopCount 次,進(jìn)行隨機(jī)訪問和增加元素到隨機(jī)位置的操作:taz28資訊網(wǎng)——每日最新資訊28at.com

//LinkedList訪問private static void linkedListGet(int elementCount, int loopCount) {    List<Integer> list = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(LinkedList::new));    IntStream.rangeClosed(1, loopCount).forEach(i -> list.get(ThreadLocalRandom.current().nextInt(elementCount)));}//ArrayList訪問private static void arrayListGet(int elementCount, int loopCount) {    List<Integer> list = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(ArrayList::new));    IntStream.rangeClosed(1, loopCount).forEach(i -> list.get(ThreadLocalRandom.current().nextInt(elementCount)));}//LinkedList插入private static void linkedListAdd(int elementCount, int loopCount) {    List<Integer> list = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(LinkedList::new));    IntStream.rangeClosed(1, loopCount).forEach(i -> list.add(ThreadLocalRandom.current().nextInt(elementCount),1));}//ArrayList插入private static void arrayListAdd(int elementCount, int loopCount) {    List<Integer> list = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(ArrayList::new));    IntStream.rangeClosed(1, loopCount).forEach(i -> list.add(ThreadLocalRandom.current().nextInt(elementCount),1));}

測(cè)試代碼如下,10 萬個(gè)元素,循環(huán) 10 萬次:taz28資訊網(wǎng)——每日最新資訊28at.com

int elementCount = 100000;int loopCount = 100000;StopWatch stopWatch = new StopWatch();stopWatch.start("linkedListGet");linkedListGet(elementCount, loopCount);stopWatch.stop();stopWatch.start("arrayListGet");arrayListGet(elementCount, loopCount);stopWatch.stop();System.out.println(stopWatch.prettyPrint());StopWatch stopWatch2 = new StopWatch();stopWatch2.start("linkedListAdd");linkedListAdd(elementCount, loopCount);stopWatch2.stop();stopWatch2.start("arrayListAdd");arrayListAdd(elementCount, loopCount);stopWatch2.stop();System.out.println(stopWatch2.prettyPrint());

運(yùn)行結(jié)果可能會(huì)讓你大跌眼鏡。在隨機(jī)訪問方面,我們看到了 ArrayList 的絕對(duì)優(yōu)勢(shì),耗時(shí)只有 11 毫秒,而 LinkedList 耗時(shí) 6.6 秒,這符合上面我們所說的時(shí)間復(fù)雜度;但,隨機(jī)插入操作居然也是 LinkedList 落敗,耗時(shí) 9.3 秒,ArrayList 只要 1.5 秒:taz28資訊網(wǎng)——每日最新資訊28at.com

---------------------------------------------ns         %     Task name---------------------------------------------6604199591  100%  linkedListGet011494583  000%  arrayListGetStopWatch '': running time = 10729378832 ns---------------------------------------------ns         %     Task name---------------------------------------------9253355484  086%  linkedListAdd1476023348  014%  arrayListAdd

翻看 LinkedList 源碼發(fā)現(xiàn),插入操作的時(shí)間復(fù)雜度是 O(1) 的前提是,你已經(jīng)有了那個(gè)要插入節(jié)點(diǎn)的指針。但,在實(shí)現(xiàn)的時(shí)候,我們需要先通過循環(huán)獲取到那個(gè)節(jié)點(diǎn)的 Node,然后再執(zhí)行插入操作。前者也是有開銷的,不可能只考慮插入操作本身的代價(jià):taz28資訊網(wǎng)——每日最新資訊28at.com

public void add(int index, E element) {    checkPositionIndex(index);    if (index == size)        linkLast(element);    else        linkBefore(element, node(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;    }}

所以,對(duì)于插入操作,LinkedList 的時(shí)間復(fù)雜度其實(shí)也是 O(n)。繼續(xù)做更多實(shí)驗(yàn)的話你會(huì)發(fā)現(xiàn),在各種常用場(chǎng)景下,LinkedList 幾乎都不能在性能上勝出 ArrayList。taz28資訊網(wǎng)——每日最新資訊28at.com

諷刺的是,LinkedList 的作者約書亞 · 布洛克(Josh Bloch),在其推特上回復(fù)別人時(shí)說,雖然 LinkedList 是我寫的但我從來不用,有誰會(huì)真的用嗎?taz28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片taz28資訊網(wǎng)——每日最新資訊28at.com

這告訴我們,任何東西理論上和實(shí)際上是有差距的,請(qǐng)勿迷信教科書的理論,最好在下定論之前實(shí)際測(cè)試一下。拋開算法層面不談,由于 CPU 緩存、內(nèi)存連續(xù)性等問題,鏈表這種數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)方式對(duì)性能并不友好,即使在它最擅長(zhǎng)的場(chǎng)景都不一定可以發(fā)揮威力。taz28資訊網(wǎng)——每日最新資訊28at.com

小結(jié)

今天,我分享了若干和 List 列表相關(guān)的錯(cuò)誤案例,基本都是由“想當(dāng)然”導(dǎo)致的。taz28資訊網(wǎng)——每日最新資訊28at.com

第一,想當(dāng)然認(rèn)為,Arrays.asList 和 List.subList 得到的 List 是普通的、獨(dú)立的 ArrayList,在使用時(shí)出現(xiàn)各種奇怪的問題。taz28資訊網(wǎng)——每日最新資訊28at.com

Arrays.asList 得到的是 Arrays 的內(nèi)部類 ArrayList,List.subList 得到的是 ArrayList 的內(nèi)部類 SubList,不能把這兩個(gè)內(nèi)部類轉(zhuǎn)換為 ArrayList 使用。taz28資訊網(wǎng)——每日最新資訊28at.com

Arrays.asList 直接使用了原始數(shù)組,可以認(rèn)為是共享“存儲(chǔ)”,而且不支持增刪元素;List.subList 直接引用了原始的 List,也可以認(rèn)為是共享“存儲(chǔ)”,而且對(duì)原始 List 直接進(jìn)行結(jié)構(gòu)性修改會(huì)導(dǎo)致 SubList 出現(xiàn)異常。taz28資訊網(wǎng)——每日最新資訊28at.com

對(duì) Arrays.asList 和 List.subList 容易忽略的是,新的 List 持有了原始數(shù)據(jù)的引用,可能會(huì)導(dǎo)致原始數(shù)據(jù)也無法 GC 的問題,最終導(dǎo)致 OOM。taz28資訊網(wǎng)——每日最新資訊28at.com

第二,想當(dāng)然認(rèn)為,Arrays.asList 一定可以把所有數(shù)組轉(zhuǎn)換為正確的 List。當(dāng)傳入基本類型數(shù)組的時(shí)候,List 的元素是數(shù)組本身,而不是數(shù)組中的元素。taz28資訊網(wǎng)——每日最新資訊28at.com

第三,想當(dāng)然認(rèn)為,內(nèi)存中任何集合的搜索都是很快的,結(jié)果在搜索超大 ArrayList 的時(shí)候遇到性能問題。我們考慮利用 HashMap 哈希表隨機(jī)查找的時(shí)間復(fù)雜度為 O(1) 這個(gè)特性來優(yōu)化性能,不過也要考慮 HashMap 存儲(chǔ)空間上的代價(jià),要平衡時(shí)間和空間。taz28資訊網(wǎng)——每日最新資訊28at.com

第四,想當(dāng)然認(rèn)為,鏈表適合元素增刪的場(chǎng)景,選用 LinkedList 作為數(shù)據(jù)結(jié)構(gòu)。在真實(shí)場(chǎng)景中讀寫增刪一般是平衡的,而且增刪不可能只是對(duì)頭尾對(duì)象進(jìn)行操作,可能在 90% 的情況下都得不到性能增益,建議使用之前通過性能測(cè)試評(píng)估一下。taz28資訊網(wǎng)——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-80842-0.html小小ArrayList,居然這么多坑?!

聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com

上一篇: Vue3 中有些場(chǎng)景,真不想用 Pinia !

下一篇: 2024年最受歡迎的十個(gè) Vue.js UI 庫

標(biāo)簽:
  • 熱門焦點(diǎn)
  • K60 Pro官方停產(chǎn) 第三方瞬間漲價(jià)

    雖然沒有官方宣布,但Redmi的一些高管也已經(jīng)透露了,Redmi K60 Pro已經(jīng)停產(chǎn)且不會(huì)補(bǔ)貨,這一切都是為了即將到來的K60 Ultra鋪路,屬于廠家的正常操作。但有意思的是該機(jī)在停產(chǎn)之后
  • Redmi Pad評(píng)測(cè):紅米充滿野心的一次嘗試

    從Note系列到K系列,從藍(lán)牙耳機(jī)到筆記本電腦,紅米不知不覺之間也已經(jīng)形成了自己頗有競(jìng)爭(zhēng)力的產(chǎn)品體系,在中端和次旗艦市場(chǎng)上甚至要比小米新機(jī)的表現(xiàn)來得更好,正所謂“大丈夫生居
  • 如何通過Python線程池實(shí)現(xiàn)異步編程?

    線程池的概念和基本原理線程池是一種并發(fā)處理機(jī)制,它可以在程序啟動(dòng)時(shí)創(chuàng)建一組線程,并將它們置于等待任務(wù)的狀態(tài)。當(dāng)任務(wù)到達(dá)時(shí),線程池中的某個(gè)線程會(huì)被喚醒并執(zhí)行任務(wù),執(zhí)行完任
  • JavaScript學(xué)習(xí) -AES加密算法

    引言在當(dāng)今數(shù)字化時(shí)代,前端應(yīng)用程序扮演著重要角色,用戶的敏感數(shù)據(jù)經(jīng)常在前端進(jìn)行加密和解密操作。然而,這樣的操作在網(wǎng)絡(luò)傳輸和存儲(chǔ)中可能會(huì)受到惡意攻擊的威脅。為了確保數(shù)據(jù)
  • 一個(gè)注解實(shí)現(xiàn)接口冪等,這樣才優(yōu)雅!

    場(chǎng)景碼猿慢病云管理系統(tǒng)中其實(shí)高并發(fā)的場(chǎng)景不是很多,沒有必要每個(gè)接口都去考慮并發(fā)高的場(chǎng)景,比如添加住院患者的這個(gè)接口,具體的業(yè)務(wù)代碼就不貼了,業(yè)務(wù)偽代碼如下:圖片上述代碼有
  • 一文搞定Java NIO,以及各種奇葩流

    大家好,我是哪吒。很多朋友問我,如何才能學(xué)好IO流,對(duì)各種流的概念,云里霧里的,不求甚解。用到的時(shí)候,現(xiàn)百度,功能雖然實(shí)現(xiàn)了,但是為什么用這個(gè)?不知道。更別說效率問題了~下次再遇到,
  • 共享單車的故事講到哪了?

    來源丨??素?cái)經(jīng)與共享充電寶相差不多,共享單車已很久沒有被國(guó)內(nèi)熱點(diǎn)新聞關(guān)照到了。除了一再漲價(jià)和用戶直呼用不起了。近日多家媒體再發(fā)報(bào)道稱,成都、天津、鄭州等地多個(gè)共享單
  • iQOO 11S或7月上市:搭載“雞血版”驍龍8Gen2 史上最強(qiáng)5G Soc

    去年底,iQOO推出了“電競(jìng)旗艦”iQOO 11系列,作為一款性能強(qiáng)機(jī),iQOO 11不僅全球首發(fā)2K 144Hz E6全感屏,搭載了第二代驍龍8平臺(tái)及144Hz電競(jìng)屏,同時(shí)在快充
  • 由于成本持續(xù)增加,筆記本產(chǎn)品價(jià)格預(yù)計(jì)將明顯上漲

    根據(jù)知情人士透露,由于材料、物流等成本持續(xù)增加,筆記本產(chǎn)品價(jià)格預(yù)計(jì)將在2021年下半年有明顯上漲。進(jìn)入6月下旬以來,全球半導(dǎo)體芯片缺貨情況加劇,顯卡、處理器
Top 主站蜘蛛池模板: 宜州市| 平顺县| 丰宁| 鄄城县| 花莲市| 灌阳县| 松江区| 景洪市| 芜湖市| 延津县| 竹北市| 夏邑县| 彰化市| 沙雅县| 金湖县| 万宁市| 克拉玛依市| 陆良县| 嘉定区| 克什克腾旗| 根河市| 台安县| 北宁市| 桦川县| 河东区| 淳安县| 青阳县| 勃利县| 庆阳市| 海口市| 衡水市| 沙雅县| 临泉县| 台安县| 江安县| 抚松县| 垫江县| 红原县| 娄烦县| 商城县| 恩施市|