摘要:我們有這么一個場景,給你一個列表,可以動態的新增,但是最終要求列表升序,要求長度小于,可以怎么做這個還不簡單,幾行代碼就可以了測試驗證上面的代碼先不考慮性能的優化方面,有沒有問題寫了個簡單的測試,我們來看下會出現什么情況啟動參數修改
我們有這么一個場景,給你一個列表,可以動態的新增,但是最終要求列表升序,要求長度小于20,可以怎么做?
這個還不簡單,幾行代碼就可以了
public List1. 測試驗證trimList(List list, int add) { list.add(add); list.sort(null); if (list.size() > 20) { list = list.subList(0, 20); } return list; }
上面的代碼先不考慮性能的優化方面,有沒有問題?
寫了個簡單的測試case,我們來看下會出現什么情況
@Test public void testTri() throws InterruptedException { Listlist = new ArrayList<>(30); Random random = new Random(); int cnt = 0; while (true) { list = trimList(list, random.nextInt(100000)); Thread.sleep(1); ++cnt; System.out.println(list + " >> " + cnt); } }
啟動參數修改下,添加jvm最大內存條件 -Xmx3m, 然后跑上面代碼,一段時間之后居然出現stack over flow
有意思的問題來了,從邏輯上看,這個數組固定長度為20,頂多有21條數據,怎么就會內存溢出呢?
2. SubList 方法揭秘我們看下ArrayList#sublis方法的實現邏輯,就可以發現獲取子列表,居然只是重置了一下內部數組的索引
public ListsubList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); } private class SubList extends AbstractList implements RandomAccess { private final AbstractList parent; private final int parentOffset; private final int offset; int size; SubList(AbstractList 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類型對象,這個對象和原來的List公用一個存儲數據的數組,但是多了兩個記錄子列表起始的偏移;
然后再看下SubList的add方法,也是直接在原來的數組中新增數據,想到與原來的列表在指定位置插入數據
public void add(int index, E e) { rangeCheckForAdd(index); checkForComodification(); parent.add(parentOffset + index, e); this.modCount = parent.modCount; this.size++; }
所以上面實現的代碼中 list = list.subList(0, 20); 這一行,有內存泄露,貌似是只返回了一個20長度大小的列表,但是這個列表中的數組長度,可能遠遠不止20
為了驗證上面的說法,debug下上面的測試用例
動圖演示如下
3. 正確使用姿勢上面知道sublist并不會新創建一個列表,舊的數據依然還在,只是我們用不了而已,所以改動也很簡單,根據sublist的結果創建一個新的數組就好了
public ListtrimList(List list, int add) { list.add(add); list.sort(null); if (list.size() > 20) { list = new ArrayList<>(list.subList(0, 20)); } return list; }
再次測試,代碼一直在順利的執行,看下后面的計數,都已經5w多,前面1w多久報錯了
雖然上面解決了內存泄露,但是gc也很頻繁了,本篇的重點主要是指出sublist的錯誤使用姿勢,所以上面算法的優化就不詳細展開了
4. 知識點擴展看下下面的測試代碼輸出應該是什么
@ToString public static class InnerC { private String name; private Integer id; public InnerC(String name, Integer id) { this.name = name; this.id = id; } } @Test public void subList() { Listlist = new ArrayList<>(); for (int i = 0; i < 20; i++) { list.add(i); } // case 1 List sub = list.subList(10, 15); sub.add(100); System.out.println("list: " + list); System.out.println("sub: " + sub); // case 2 list.set(11, 200); System.out.println("list: " + list); System.out.println("sub: " + sub); // case 3 list = new ArrayList<>(sub); sub.set(0, 999); System.out.println("list: " + list); System.out.println("sub: " + sub); // case 4 List cl = new ArrayList<>(); cl.add(new InnerC("a", 1)); cl.add(new InnerC("a2", 2)); cl.add(new InnerC("a3", 3)); cl.add(new InnerC("a4", 4)); List cl2 = new ArrayList<>(cl.subList(1, 3)); cl2.get(0).name = "a5"; cl2.get(0).id = 5; System.out.println("list cl: " + cl); System.out.println("list cl2: " + cl2); }
再看具體的答案之前,先分析一下
針對case1/2,我們知道sublist返回的列表和原列表公用一個底層數組,所以這兩個列表的增刪,都是相互影響的
case1 執行之后相當于在list數組的下標15這里,插入數據100
case2 執行之后,list的下標11,相當于sub的下標1,也就是說sub[1] 變成了200
對于case3/4 而言,根據sub創建了一個新的列表,這個時候修改新的列表中的值,會影響到原來的列表中的值么?
分析這個場景,就需要看一下源碼了
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; } } // 對應的核心邏輯就在 Arrays.copyOf,而這個方法主要調用的是native方法`System.arraycopy` public staticT[] copyOf(U[] original, int newLength, Class extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
從上面的源碼分析,會不會相互影響就看這個數組拷貝是怎么實現的了(深拷貝?淺拷貝?)
接下來看下實際的輸出結果
list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 100, 15, 16, 17, 18, 19] sub: [10, 11, 12, 13, 14, 100] list: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 200, 12, 13, 14, 100, 15, 16, 17, 18, 19] sub: [10, 200, 12, 13, 14, 100] list: [10, 200, 12, 13, 14, 100] sub: [999, 200, 12, 13, 14, 100] list cl: [BasicTest.InnerC(name=a, id=1), BasicTest.InnerC(name=a5, id=5), BasicTest.InnerC(name=a3, id=3), BasicTest.InnerC(name=a4, id=4)] list cl2: [BasicTest.InnerC(name=a5, id=5), BasicTest.InnerC(name=a3, id=3)]
從上面可以知道,case1/2的分析沒啥問題,case3、4的輸出有點意思了
數組內為Integer時,兩者互不影響
數組內為普通對象時,修改其中一個,會影響另外一個
關從輸出結果來看 System.arraycopy 是淺拷貝,至于為什么int不影響呢,這個就和方法調用傳參是基本數據類型時,在方法內部修改參數不會影響到外部一個道理了
II. 其他盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
微博地址: 小灰灰Blog
QQ: 一灰灰/3302797840
個人博客站點 一灰灰Blog: https://liuyueyi.github.io/he...
一灰灰blog
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74653.html
摘要:有沒有更快的方法如果分隔符不是單字符而且也不需要按正則分隔的話,使用的方法還會和一樣使用正則表達式。使用分隔字符串,針對不需要按正則分隔的場景提供更好的實現,分隔符支持字符串。 String.split 是Java里很常用的字符串操作,在普通業務操作里使用的話并沒有什么問題,但如果需要追求高性能的分割的話,需要花一點心思找出可以提高性能的方法。 String.split方法的分割參數r...
摘要:體現的就是適配器模式。數組對象集合世界中的機制機制集合世界中比較常見的錯誤檢測機制,防止在對集合進行遍歷過程當中,出現意料之外的修改,會通過異常暴力的反應出來。而在增強循環中,集合遍歷是通過進行的。 前言 學習情況記錄 時間:week 2 SMART子目標 :Java 容器 記錄在學習Java容器 知識點中,關于List的重點知識點。 知識點概覽: 容器中的設計模式 從Array...
摘要:畢業兩個星期了,開始成為一名正式的碼農了。將指定位置的數據移除。但是問題是,為時,并不是直接一個大小為的數組,而是使用靜態變量來代替。此外,函數還做了越界檢查。返回迭代器,與之有一個搭配的輔助類。 畢業兩個星期了,開始成為一名正式的java碼農了。一直對偏底層比較感興趣,想著深入自己的java技能,看書、讀源碼、總結、造輪子實踐都是付諸行動的方法。說到看源碼,就應該由簡入難,逐漸加深,...
閱讀 1072·2021-11-25 09:43
閱讀 696·2021-11-22 14:45
閱讀 3816·2021-09-30 09:48
閱讀 1061·2021-08-31 09:41
閱讀 1970·2019-08-30 13:52
閱讀 1976·2019-08-30 11:24
閱讀 1341·2019-08-30 11:07
閱讀 950·2019-08-29 12:15