第一次用subList()时,我也以为它和String.substring()一样会创建独立副本。直到某次线上事故让我彻底明白了它的本质——这根本不是拷贝操作,而是给原列表开了个"观察窗口"。想象你拿着放大镜看地图,移动放大镜不会改变地图本身,但透过镜片做的标记会直接反映在地图上。
来看个实际案例。我们有个用户行为日志分析的场景:
java复制List<UserAction> rawLogs = getUserActions(); // 获取10万条日志
List<UserAction> hourLogs = rawLogs.subList(0, 3600); // 取第1小时数据
// 分析时发现脏数据需要清理
hourLogs.removeIf(action -> action.isInvalid());
执行后惊讶地发现:原始rawLogs也被修改了!这就是视图操作的典型特征——所有对子列表的修改都会穿透到父列表。其底层实现是通过SubList类持有原列表引用,所有操作都委托给原列表执行:
java复制// JDK源码节选
class SubList extends AbstractList {
private final AbstractList parent;
private final int offset;
public E set(int index, E e) {
rangeCheck(index);
return parent.set(index+offset, e); // 实际操作原列表
}
}
去年我们系统出现过一次OOM,追踪发现竟与subList()有关。当时做分页查询的代码是这样的:
java复制List<Data> fullData = loadHugeDataFromDB(); // 加载百万级数据
for(int i=0; i<fullData.size(); i+=100) {
List<Data> page = fullData.subList(i, Math.min(i+100, fullData.size()));
processPage(page);
}
看起来没问题?问题在于fullData始终被所有page子列表强引用着。即使processPage()处理完,GC也无法回收fullData,因为SubList内部持有this$0外部类引用。这就好比租房时,二房东拿着你的钥匙不还,导致你永远无法退租。
解决方案有三:
java复制IntStream.range(0, fullData.size()/100)
.mapToObj(i -> new ArrayList<>(fullData.subList(i*100, (i+1)*100)))
.forEach(this::processPage);
java复制List<Data> page = new ArrayList<>(fullData.subList(start, end));
List.copyOf(Java10+):java复制List<Data> page = List.copyOf(fullData.subList(start, end));
最让人头疼的莫过于ConcurrentModificationException。有一次我写滑动窗口算法时就栽在这:
java复制List<Integer> data = new ArrayList<>(Arrays.asList(1,2,3,4,5));
List<Integer> window = data.subList(0, 2);
// 在另一个线程中
data.add(6); // 结构修改
window.get(0); // 抛出异常!
根本原因是modCount机制。ArrayList维护着modCount计数器,任何结构性修改(add/remove等)都会使其递增。SubList会检查自己的expectedModCount是否与父列表一致:
java复制final void checkForComodification() {
if (parent.modCount != expectedModCount)
throw new ConcurrentModificationException();
}
安全操作守则:
java复制List<Integer> safeWindow = Collections.unmodifiableList(data.subList(0, 2));
经过多次踩坑,我总结出这些最佳实践:
场景一:分块处理大列表
java复制// 错误示范
List<List<Data>> chunks = IntStream.range(0, data.size()/chunkSize)
.mapToObj(i -> data.subList(i*chunkSize, (i+1)*chunkSize))
.collect(Collectors.toList());
// 正确做法
List<List<Data>> safeChunks = IntStream.range(0, data.size()/chunkSize)
.mapToObj(i -> new ArrayList<>(data.subList(i*chunkSize, (i+1)*chunkSize)))
.collect(Collectors.toList());
场景二:滑动窗口算法
java复制// 创建窗口快照
List<Data> windowSnapshot = new ArrayList<>(data.subList(start, end));
// 或者使用Java8的skip/limit
List<Data> window = data.stream()
.skip(start)
.limit(end-start)
.collect(Collectors.toList());
场景三:范围清除
java复制// 优雅的清空列表前半部分
data.subList(0, data.size()/2).clear();
// 比传统循环删除更高效
for(int i=0; i<data.size()/2; ) {
data.remove(0); // 每次remove都要移动元素
}
打开ArrayList的源码,会发现subList()返回的是内部类SubList实例:
java复制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> {
private final AbstractList<E> parent;
private final int offset;
public E get(int index) {
rangeCheck(index);
return parent.get(index + offset);
}
public int size() {
return this.size;
}
}
关键点在于:
LinkedList的实现略有不同,但其SubList同样通过节点引用来避免数据拷贝:
java复制// LinkedList.SubList部分实现
public E get(int index) {
checkForComodification();
return LinkedList.this.node(offset + index).item;
}
通过JMH基准测试对比不同场景下的性能(ns/op):
| 操作类型 | ArrayList直接访问 | SubList视图 | 新建副本 |
|---|---|---|---|
| 连续读取100次 | 125 | 132 | 145 |
| 批量删除50个元素 | 2850 | 120 | 4800 |
| 并发修改稳定性 | 稳定 | 不稳定 | 稳定 |
选型建议:
subList().clear()/replaceAll()不同List实现类的subList行为有细微差别:
CopyOnWriteArrayList
java复制// 每次修改都会创建新数组
List<Integer> cowList = new CopyOnWriteArrayList<>(Arrays.asList(1,2,3));
List<Integer> sub = cowList.subList(0,1);
cowList.add(4); // 不会抛出ConcurrentModificationException
sub.get(0); // 安全访问
Guava的ImmutableList
java复制ImmutableList<Integer> imList = ImmutableList.of(1,2,3);
List<Integer> sub = imList.subList(0,1);
// 所有修改操作都会直接抛出UnsupportedOperationException
sub.add(4);
Kotlin的List
kotlin复制val list = listOf(1,2,3,4)
val sub = list.subList(0,2)
// Kotlin的subList返回的是不可变视图
sub.add(5) // 编译错误
问题现象一:ClassCastException: cannot be cast to ArrayList
new ArrayList<>(list.subList())问题现象二:遍历子列表时偶发ConcurrentModificationException
Collections.synchronizedList包装问题现象三:内存持续增长不释放
this$0引用链为避免团队重复踩坑,我封装了安全工具类:
java复制public class ListUtils {
/**
* 安全获取列表副本
*/
public static <T> List<T> safeSubList(List<T> list, int from, int to) {
return new ArrayList<>(list.subList(from, to));
}
/**
* 批量处理时自动释放引用
*/
public static <T> void processInBatches(List<T> source, int batchSize, Consumer<List<T>> processor) {
for (int i = 0; i < source.size(); i += batchSize) {
List<T> batch = safeSubList(source, i, Math.min(i + batchSize, source.size()));
processor.accept(batch);
}
}
}
使用示例:
java复制List<Data> hugeList = ...;
ListUtils.processInBatches(hugeList, 1000, batch -> {
// 安全处理每个批次
});
SubList的实现体现了视图模式(View Pattern)的经典应用:
对比数据库的视图概念,会发现惊人的相似性——两者都是逻辑层面的数据展现,而非物理存储的复制。这种设计哲学在JDK集合框架中随处可见,比如Map.keySet()、Collections.unmodifiableList()等。