ArrayList#subList这四个坑,一不小心就中招
今天我们来扒一扒ArrayList#subList的四大坑。
一、使用不当引起内存泄露
先给大家看一段简单但是比较有意思的代码
public class OrderService {public static void main(String[] args) {OrderService orderService = new OrderService();orderService.process();}public void process() {List orderIdList = queryOrder();List> allFailedList = new ArrayList<>();for(int i = 0; i < Integer.MAX_VALUE; i++) {System.out.println(i);List failedList = doProcess(orderIdList);allFailedList.add(failedList);}}private List doProcess(List orderIdList) {List failedList = new ArrayList<>();for (Long orderId : orderIdList) {if (orderId % 2 == 0) {failedList.add(orderId) ;}}// 只取一个失败的订单id做分析return failedList.subList(0, 1);}private List queryOrder() {List orderIdList = new ArrayList<>();for (int i = 0; i < 1000; i++) {orderIdList.add(RandomUtils.nextLong());}return orderIdList;}
}
如果你在本地的机器上运行这段代码,并且打开arthas监控内存情况:
Memory used total max usage
heap 2742M 3643M 3643M 75.28%
ps_eden_space 11M 462M 468M 2.52%
ps_survivor_space 0K 460288K 460288K 0.00%
ps_old_gen 2730M 2731M 2731M 99.99%
nonheap 28M 28M -1 97.22%
code_cache 5M 5M 240M 2.19%
metaspace 20M 20M -1 97.19%
compressed_class_space 2M 2M 1024M 0.25%
direct 0K 0K - 0.00%
mapped 0K 0K - 0.00% 不到3GB的老年代当i循环到大概60万左右的时候就已经打爆了,而我们当前堆中的最大的对象是allFailedList最多也是60万个Long型的List,粗略的计算一下也只有几十MB,完全不至于打爆内存。那我们就有理由怀疑上面的这段代码产生了内存泄露了。
回到ArrayList#subList的实现代码:
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;}
} 可以看到,每次调用ArrayList#subList的时候都会生成一个SubList对象,而这个对象的parent属性值却持有原ArrayList的引用,这样一来就说得通了,allFailedList持有历次调用queryOrder产生的List对象,这些对象最终都转移到了老年代而得不到释放。
二、使用不当引起死循环
再看一段代码:
public class SubListDemo {public static void main(String[] args) {List arrayList = init();List subList = arrayList.subList(0, 1);for (int i = 0; i < arrayList.size(); i++) {if (arrayList.get(i) % 2 == 0) {subList.add(arrayList.get(i));}}}private static List init() {List arrayList = new ArrayList<>();arrayList.add(RandomUtils.nextLong());arrayList.add(RandomUtils.nextLong());arrayList.add(RandomUtils.nextLong());arrayList.add(RandomUtils.nextLong());arrayList.add(RandomUtils.nextLong());return arrayList;}
} 如果我说上面的这段代码是一个死循环,你会感到奇怪么。回到subList的实现
// AbstractList
public boolean add(E e) {add(size(), e);return true;
} 然后会调用到ArrayList的方法
public void add(int index, E e) {rangeCheckForAdd(index);checkForComodification();parent.add(parentOffset + index, e);this.modCount = parent.modCount;this.size++;
} 可以看到,调用subList的add其实是在原ArrayList中增加元素,因此原arrayList.size()会一直变大,最终导致死循环。
三、无法对subList和原List做结构性修改
public static void main(String[] args) {List listArr = new ArrayList<>();listArr.add("Delhi");listArr.add("Bangalore");listArr.add("New York");listArr.add("London");List listArrSub = listArr.subList(1, 3);System.out.println("List-: " + listArr);System.out.println("Sub List-: " + listArrSub);//Performing Structural Change in list.listArr.add("Mumbai");System.out.println("\nAfter Structural Change...\n");System.out.println("List-: " + listArr);System.out.println("Sub List-: " + listArrSub);
} 这段代码最后会抛出ConcurrentModificationException
List-: [Delhi, Bangalore, New York, London]
Sub List-: [Bangalore, New York]After Structural Change...List-: [Delhi, Bangalore, New York, London, Mumbai]
Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)at java.util.ArrayList$SubList.listIterator(ArrayList.java:1091)at java.util.AbstractList.listIterator(AbstractList.java:299)at java.util.ArrayList$SubList.iterator(ArrayList.java:1087)at java.util.AbstractCollection.toString(AbstractCollection.java:454)at java.lang.String.valueOf(String.java:2982)at java.lang.StringBuilder.append(StringBuilder.java:131)at infosys.Research.main(Research.java:26) 简单看下ArrayList的源码:
public boolean add(E e) {ensureCapacityInternal(size + 1); // Increments modCount!!elementData[size++] = e;return true;
}private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}private void ensureExplicitCapacity(int minCapacity) {// 注意这行对原list的modCount这个变量做了自增操作modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);
} 要注意,调用原数组的add方法时已经修改了原数组的modCount属性,当程序执行到打印subList这行代码时会调用Sublist#toString方法,到最后会调用到下面这个私有方法:
private void checkForComodification() {if (ArrayList.this.modCount != this.modCount)throw new ConcurrentModificationException();
} 根据前面分析,原ArrayList的modCount属性已经自增,所以ArrayList.this.modCount != this.modCount执行的结果是true,最终抛出了ConcurrentModificationException异常。
关于modCount这个属性,Oracle的文档中也有详细的描述
The number of times this list has been structurally modified. Structural modifications are those that change the size of the list.
翻译过来就是:
modCount记录的是List被结构性修改的次数,所谓结构性修改是指能够改变List大小的操作
如果提前没有知识储备,这类异常是比较难排查的
四、作为RPC接口入参时序列化失败
从上面SubList的定义可以看出来,SubList并没有实现Serializable接口,因此在一些依赖Java原生序列化协议的RPC的框架中会序列化失败,如Dubbo等。
五、最佳实践
subList设计之初是作为原List的一个视图,经常在只读的场景下使用,这和大多数人理解的不太一样,即便只在只读的场景下使用,也容易产生内存泄露,况且这个视图的存在还不允许原List和SubList做结构性修改,个人认为subList这个Api的设计糟糕透了,尽量在代码中避免直接使用ArrayList#subList,获取List的subList有两条最佳实践:
5.1 拷贝到新的ArrayList中
ArrayList myArrayList = new ArrayList();
ArrayList part1 = new ArrayList(myArrayList.subList(0, 25));
ArrayList part2 = new ArrayList(myArrayList.subList(26, 51)); 5.2 使用lambda表达式
dataList.stream().skip(5).limit(10).collect(Collectors.toList());
dataList.stream().skip(30).limit(10).collect(Collectors.toList()); 推荐
Java面试题宝典
技术内卷群,一起来学习!!

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
