老哥你真的知道ArrayList#sublist的正確用法么

我們有這么一個場景,給你一個列表蠢琳,可以動態(tài)的新增,但是最終要求列表升序镜豹,要求長度小于20傲须,可以怎么做?

這個還不簡單趟脂,幾行代碼就可以了

public List<Integer> trimList(List<Integer> list, int add) {
    list.add(add);
    list.sort(null);
    if (list.size() > 20) {
        list = list.subList(0, 20);
    }
    return list;
}

1. 測試驗(yàn)證

上面的代碼先不考慮性能的優(yōu)化方面泰讽,有沒有問題?

寫了個簡單的測試case,我們來看下會出現(xiàn)什么情況

@Test
public void testTri() throws InterruptedException {
    List<Integer> list = 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);
    }
}

啟動參數(shù)修改下已卸,添加jvm最大內(nèi)存條件 -Xmx3m佛玄, 然后跑上面代碼,一段時間之后居然出現(xiàn)stack over flow

sof

有意思的問題來了累澡,從邏輯上看梦抢,這個數(shù)組固定長度為20,頂多有21條數(shù)據(jù)愧哟,怎么就會內(nèi)存溢出呢奥吩?

2. SubList 方法揭秘

我們看下ArrayList#sublis方法的實(shí)現(xiàn)邏輯,就可以發(fā)現(xiàn)獲取子列表蕊梧,居然只是重置了一下內(nèi)部數(shù)組的索引

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, 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;
    }
    ...
}

返回的是一個SubList類型對象霞赫,這個對象和原來的List公用一個存儲數(shù)據(jù)的數(shù)組,但是多了兩個記錄子列表起始的偏移;

然后再看下SubList的add方法肥矢,也是直接在原來的數(shù)組中新增數(shù)據(jù)绩脆,想到與原來的列表在指定位置插入數(shù)據(jù)

public void add(int index, E e) {
    rangeCheckForAdd(index);
    checkForComodification();
    parent.add(parentOffset + index, e);
    this.modCount = parent.modCount;
    this.size++;
}

所以上面實(shí)現(xiàn)的代碼中 list = list.subList(0, 20); 這一行,有內(nèi)存泄露橄抹,貌似是只返回了一個20長度大小的列表,但是這個列表中的數(shù)組長度惕味,可能遠(yuǎn)遠(yuǎn)不止20

為了驗(yàn)證上面的說法楼誓,debug下上面的測試用例

debug

動圖演示如下

image

3. 正確使用姿勢

上面知道sublist并不會新創(chuàng)建一個列表,舊的數(shù)據(jù)依然還在名挥,只是我們用不了而已疟羹,所以改動也很簡單,根據(jù)sublist的結(jié)果創(chuàng)建一個新的數(shù)組就好了

public List<Integer> trimList(List<Integer> list, int add) {
    list.add(add);
    list.sort(null);
    if (list.size() > 20) {
        list = new ArrayList<>(list.subList(0, 20));
    }
    return list;
}

再次測試禀倔,代碼一直在順利的執(zhí)行榄融,看下后面的計(jì)數(shù),都已經(jīng)5w多救湖,前面1w多久報錯了

show

雖然上面解決了內(nèi)存泄露愧杯,但是gc也很頻繁了,本篇的重點(diǎn)主要是指出sublist的錯誤使用姿勢鞋既,所以上面算法的優(yōu)化就不詳細(xì)展開了

sof

4. 知識點(diǎn)擴(kuò)展

看下下面的測試代碼輸出應(yīng)該是什么

@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() {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
        list.add(i);
    }

    // case 1
    List<Integer> 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<InnerC> 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<InnerC> 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返回的列表和原列表公用一個底層數(shù)組邑闺,所以這兩個列表的增刪跌前,都是相互影響的

  • case1 執(zhí)行之后相當(dāng)于在list數(shù)組的下標(biāo)15這里,插入數(shù)據(jù)100
  • case2 執(zhí)行之后陡舅,list的下標(biāo)11抵乓,相當(dāng)于sub的下標(biāo)1,也就是說sub[1] 變成了200

對于case3/4 而言,根據(jù)sub創(chuàng)建了一個新的列表灾炭,這個時候修改新的列表中的值茎芋,會影響到原來的列表中的值么?

分析這個場景咆贬,就需要看一下源碼了

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

// 對應(yīng)的核心邏輯就在 Arrays.copyOf败徊,而這個方法主要調(diào)用的是native方法`System.arraycopy`

public static <T,U> T[] 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;
}

從上面的源碼分析,會不會相互影響就看這個數(shù)組拷貝是怎么實(shí)現(xiàn)的了(深拷貝掏缎?淺拷貝皱蹦?)


接下來看下實(shí)際的輸出結(jié)果

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)]
out

從上面可以知道,case1/2的分析沒啥問題眷蜈,case3沪哺、4的輸出有點(diǎn)意思了

  • 數(shù)組內(nèi)為Integer時,兩者互不影響
  • 數(shù)組內(nèi)為普通對象時酌儒,修改其中一個辜妓,會影響另外一個

關(guān)從輸出結(jié)果來看 System.arraycopy 是淺拷貝,至于為什么int不影響呢忌怎,這個就和方法調(diào)用傳參是基本數(shù)據(jù)類型時籍滴,在方法內(nèi)部修改參數(shù)不會影響到外部一個道理了

II. 其他

盡信書則不如,已上內(nèi)容榴啸,純屬一家之言孽惰,因個人能力有限,難免有疏漏和錯誤之處鸥印,如發(fā)現(xiàn)bug或者有更好的建議勋功,歡迎批評指正,不吝感激

一灰灰blog

QrCode
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末库说,一起剝皮案震驚了整個濱河市狂鞋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌潜的,老刑警劉巖骚揍,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異夏块,居然都是意外死亡疏咐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門脐供,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浑塞,“玉大人,你說我怎么就攤上這事政己∽煤荆” “怎么了掏愁?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長卵牍。 經(jīng)常有香客問我果港,道長,這世上最難降的妖魔是什么糊昙? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任辛掠,我火速辦了婚禮,結(jié)果婚禮上释牺,老公的妹妹穿的比我還像新娘萝衩。我一直安慰自己,他們只是感情好没咙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布猩谊。 她就那樣靜靜地躺著,像睡著了一般祭刚。 火紅的嫁衣襯著肌膚如雪牌捷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛忙干,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼徙硅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了焰情?” 一聲冷哼從身側(cè)響起陌凳,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎内舟,沒想到半個月后合敦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡验游,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年充岛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耕蝉。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡崔梗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出垒在,到底是詐尸還是另有隱情蒜魄,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站谈为,受9級特大地震影響旅挤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伞鲫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一粘茄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧秕脓,春花似錦柒瓣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诵肛,卻和暖如春屹培,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怔檩。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工褪秀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人薛训。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓媒吗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乙埃。 傳聞我的和親對象是個殘疾皇子闸英,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容