当前位置:网站首页>ArrayList#subList这四个坑,一不小心就中招
ArrayList#subList这四个坑,一不小心就中招
2022-06-24 10:22:00 【Hollis Chuang】
Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记!
一、使用不当引起内存泄露
先给大家看一段简单但是比较有意思的代码
public class OrderService {
public static void main(String[] args) {
OrderService orderService = new OrderService();
orderService.process();
}
public void process() {
List<Long> orderIdList = queryOrder();
List<List<Long>> allFailedList = new ArrayList<>();
for(int i = 0; i < Integer.MAX_VALUE; i++) {
System.out.println(i);
List<Long> failedList = doProcess(orderIdList);
allFailedList.add(failedList);
}
}
private List<Long> doProcess(List<Long> orderIdList) {
List<Long> failedList = new ArrayList<>();
for (Long orderId : orderIdList) {
if (orderId % 2 == 0) {
failedList.add(orderId) ;
}
}
// 只取一个失败的订单id做分析
return failedList.subList(0, 1);
}
private List<Long> queryOrder() {
List<Long> 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<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;
}
}可以看到,每次调用ArrayList#subList的时候都会生成一个SubList对象,而这个对象的parent属性值却持有原ArrayList的引用,这样一来就说得通了,allFailedList持有历次调用queryOrder产生的List对象,这些对象最终都转移到了老年代而得不到释放。
二、使用不当引起死循环
再看一段代码:
public class SubListDemo {
public static void main(String[] args) {
List<Long> arrayList = init();
List<Long> 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<Long> init() {
List<Long> 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<String> listArr = new ArrayList<>();
listArr.add("Delhi");
listArr.add("Bangalore");
listArr.add("New York");
listArr.add("London");
List<String> 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.ConcurrentModificationException
at 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 code
if (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核心技术》已经上市了,上市后一直蝉联京东畅销榜中,目前正在6折优惠中,想要入手的朋友千万不要错过哦~长按二维码即可购买~

长按扫码享受6折优惠
往期推荐
3000帧动画图解MySQL为什么需要binlog、redo log和undo log
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号

好文章,我在看️
边栏推荐
- Base64 decoding method three ways for non professionals
- Redis
- TP-LINK 1208 router tutorial (2)
- 夜晚读书 -- 关于微服务和容器
- Today in history: Turing's birth day; The birth of the founder of the Internet; Reddit goes online
- Remote desktop copy paste exception
- 《opencv学习笔记》-- 分离颜色通道、多通道混合
- Coinbase将推出首个针对零售交易员的加密衍生产品
- Understanding of homogeneous coordinates
- Google ranging for PHP wechat development
猜你喜欢

Today's sleep quality record 76 points

Fais ce que tu veux.

《梦华录》要大结局了,看超前点映不如先来学学它!

计组_cpu的结构和工作流程

math_ Summation and derivation of proportional series & derivation of sum and difference of equal powers / difference between two nth power numbers/

《opencv学习笔记》-- 感兴趣区域(ROI)、图像混合

"One good programmer is worth five ordinary programmers!"

First acquaintance with string+ simple usage (I)

《opencv学习笔记》-- 图像的载入和保存

Programmers spend most of their time not writing code, but...
随机推荐
软件测试 对前一日函数的基本路径测试
Apple's legendary design team disbanded after jobs refused to obey cook
Any and typevar make the automatic completion of IDE better
为什么虚拟机ping的通主机,主机ping不通虚拟机
Install wpr Exe command
Base64 decoding method three ways for non professionals
[206] use PHP language to generate the code of go language
Istio best practice: graceful termination
What code did the full stack programmer write this month?
08. Tencent cloud IOT device side learning - device shadow and attributes
Libuv的安装及运行使用
Step 3: access the API interface for inquiry of SF express doc No. [express 100api interface]
Tencent geek challenge small - endless!
It's so difficult for me. Have you met these interview questions?
qt -- QTabWidget 中支持拖拽TabBar项
Give you a server. Can you deploy your code online?
Extremenet: target detection through poles, more detailed target area | CVPR 2019
Programmers spend most of their time not writing code, but...
Using the collaboration database query of Poole in laravel5.6
初识string+简单用法(一)


