java list trimSize_老哥你真的知道ArrayList#sublist的正确用法么

我们有这么一个场景,给你一个列表,可以动态的新增,但是最终要求列表升序,要求长度小于20,可以怎么做?

这个还不简单,几行代码就可以了

public List trimList(List list, int add) {

list.add(add);

list.sort(null);

if (list.size() > 20) {

list = list.subList(0, 20);

}

return list;

}

1. 测试验证

上面的代码先不考虑性能的优化方面,有没有问题?

写了个简单的测试case,我们来看下会出现什么情况

@Test

public void testTri() throws InterruptedException {

List 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);

}

}

启动参数修改下,添加jvm最大内存条件 -Xmx3m, 然后跑上面代码,一段时间之后居然出现stack over flow

04e884f65ba0c0e2ea9b7f12b0e13228.png

有意思的问题来了,从逻辑上看,这个数组固定长度为20,顶多有21条数据,怎么就会内存溢出呢?

2. SubList 方法揭秘

我们看下ArrayList#sublis方法的实现逻辑,就可以发现获取子列表,居然只是重置了一下内部数组的索引

public List subList(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下上面的测试用例

981e5388693c43c85ef5f9dcdac2a3c6.png

动图演示如下

02.gif

3. 正确使用姿势

上面知道sublist并不会新创建一个列表,旧的数据依然还在,只是我们用不了而已,所以改动也很简单,根据sublist的结果创建一个新的数组就好了

public List trimList(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多久报错了

72565442759a3c09c08f9f1cc9fa1477.png

虽然上面解决了内存泄露,但是gc也很频繁了,本篇的重点主要是指出sublist的错误使用姿势,所以上面算法的优化就不详细展开了

f1fdabd5fee6fe952c79f223c1654cf6.png

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() {

List list = 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 static 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;

}

从上面的源码分析,会不会相互影响就看这个数组拷贝是怎么实现的了(深拷贝?浅拷贝?)

接下来看下实际的输出结果

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

d8543a74ea52cab38015a55c8fded079.png

从上面可以知道,case1/2的分析没啥问题,case3、4的输出有点意思了

数组内为Integer时,两者互不影响

数组内为普通对象时,修改其中一个,会影响另外一个

关从输出结果来看 System.arraycopy 是浅拷贝,至于为什么int不影响呢,这个就和方法调用传参是基本数据类型时,在方法内部修改参数不会影响到外部一个道理了

II. 其他

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

一灰灰blog

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值