我们有这么一个场景,给你一个列表,可以动态的新增,但是最终要求列表升序,要求长度小于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. 测试验证

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

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

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

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

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

2. SubList 方法揭秘

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

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公用一个存储数据的数组,但是多了两个记录子列表起始的偏移;

然后再看下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 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;
}

再次测试,代码一直在顺利的执行,看下后面的计数,都已经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() {
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返回的列表和原列表公用一个底层数组,所以这两个列表的增删,都是相互影响的

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

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


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

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

老哥你真的知道ArrayList#sublist的正确用法么的更多相关文章

  1. Exception in thread "main" java.lang.StackOverflowError at java.util.ArrayList$SubList.rangeCheckForAdd(Unknown Source)

    Exception in thread "main" java.lang.StackOverflowError at java.util.ArrayList$SubList.ran ...

  2. ArrayList.subList方法使用总结

    ArrayList.subList方法使用总结 示例 List<String> list=new ArrayList<>(); list.add("d"); ...

  3. ClassNotFoundException: java.util.ArrayList$SubList 错误

    ClassNotFoundException: java.util.ArrayList$SubListjava.lang.RuntimeException: java.lang.ClassNotFou ...

  4. CPU:网卡老哥,你到底怎么工作的?

    阿Q造访 我是一个网卡,居住在一个机箱内的主板上,负责整台计算机的网络通信,要是没有我,这里就成了一个信息孤岛了,那也太无聊了- 上个周末,服务器断电维护了,这是我难得的休息时间,我准备打个盹儿眯一会 ...

  5. ArrayList源码解析,老哥,来一起复习一哈?

    前言 JDK源码解析系列文章,都是基于JDK8分析的,虽然JDK14已经出来,但是JDK8我还不会,我... 类图 实现了RandomAccess接口,可以随机访问 实现了Cloneable接口,可以 ...

  6. 老哥,您看我这篇Java集合,还有机会评优吗?

    集合在我们日常开发使用的次数数不胜数,ArrayList/LinkedList/HashMap/HashSet······信手拈来,抬手就拿来用,在 IDE 上龙飞凤舞,但是作为一名合格的优雅的程序猿 ...

  7. 老哥你能写篇 SpringCloud Alibaba 全家桶吗? 看视频太累 太枯燥了 !

    最喜欢的一句话: 1.01的365次方=37.78343433289 >>>1 0.99的365次方= 0.02551796445229, 每天进步一点点的目标,贵在坚持 前端时间有 ...

  8. git学习 c的某位老哥的,(侵删)

    Git命令行基本操作 安装Git 网上有很多Git安装教程,如果需要图形界面,windows下建议使用TortoiseGit,linux建议使用Git GUI或者GITK. Git基本配置 git c ...

  9. 给用过SAP CRM中间件的老哥老姐们讲讲SAP CPI

    最近Jerry由于项目需要,又得学习一个新工具:SAP Cloud Platform Integration,简称CPI,以前又叫做HCI - HANA Cloud Platform Integrat ...

随机推荐

  1. manjaro kde tim QQ

    deepin-wine-tim

  2. rs485多主

    因复位时I/O口都输出高电平.如果把I/O口直接与RS-485接口芯片的驱动器使能端DE端相连,会在CPU复位其间DE为高,从而使本节点处于发送状态.如果此时总线上其它节点在发送数据,则此次数据传输将 ...

  3. 通过IAR工程文件查看对应IAR版本号

    IAR使用技巧——如何使用合适的版本打开IAR工程 2014年07月05日 23:49:08 xukai871105 阅读数:12895 标签: IAR 更多 个人分类: 嵌入式ARM   0.前言 ...

  4. C++中重载、覆盖和隐藏的区别,以及适用场景

    一.重载.覆盖和隐藏的区别 二.适用场景 1.重载: 适用于不同的数据类型都需要使用到的功能函数.以数据相加的函数为例,可以在同一个文件内提供以下的重载函数以支持同样的功能: int add(int, ...

  5. Linuxx学习-特殊文件与进程

    具有 SUID/SGID 权限的指令执行状态 SUID 的权限其实与进程的相关性非常的大!为什么呢?先来看看 SUID 的程序是如何被一般用户 执行,且具有什么特色呢? SUID 权限仅对二进制程序( ...

  6. Java + golang 爬取B站up主粉丝数

    自从学习了爬虫,就想在B站爬取点什么数据,最近看到一些个up主涨粉很快,于是对up主的粉丝数量产生了好奇,所以就有了标题~ 首先,我天真的以为通过up主个人空间的地址就能爬到 https://spac ...

  7. jquery如何获取某一个兄弟节点

    $('#id').siblings() 当前元素所有的兄弟节点 $('#id').prev() 当前元素前一个兄弟节点 $('#id').prevaAll() 当前元素之前所有的兄弟节点 $('#id ...

  8. STL学习笔记1--vector

    C++STL(Standard Template Library)标准模板库是通用类模板和算法的集合.包含一些标准的数据结构的实现如queues(队列),lists(链表),stacks(栈)等.ST ...

  9. SpringMVC 页面传递参数到controller的五种方式

    一共是五种传参方式: 一:直接将请求参数名作为Controller中方法的形参 public  String login (String username,String password)   : 解 ...

  10. Python 开启线程的2中方式,线程VS进程(守护线程、互斥锁)

    知识点一: 进程:资源单位 线程:才是CPU的执行单位 进程的运行: 开一个进程就意味着开一个内存空间,存数据用,产生的数据往里面丢 线程的运行: 代码的运行过程就相当于运行了一个线程 辅助理解:一座 ...