问题一:Arrays.asList()返回的list不支持增删操作
问题复现
package com.geekmice.onetomany.list;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
t1();
}
/**
* @description 数组转换list
*/
public static void t1() {
ArrayList<Object> objects = new ArrayList<>();
String[] array = {"张三", "李四", "王五"};
List<String> list = Arrays.asList(array);
// Exception in thread "main" java.lang.UnsupportedOperationException
list.add("小三");
}
}
错误提示
Exception in thread “main” java.lang.UnsupportedOperationException
分析:上面这几行代码,主要是将数组数组转换list,转换后list,进行添加操作提示错误 UnsupportedOperationException,刚开始很不解,Arrays#asList 返回明明也是一个 ArrayList,为什么添加一个元素就会报错?这以后还能好好新增元素吗;后面看了一下java.util.ArrayList和Arrays#asList 内部情况,其实不一样的;
对于Arrays#asList 而言
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
发现这个Arrays#asList 返回的 ArrayList 其实是个赝品,仅仅只是 Arrays 一个内部类,并非真正的 java.util.ArrayList
从上图我们发现,add/remove 等方法实际都来自 AbstractList,而 java.util.Arrays$ArrayList 并没有重写父类的方法。而父类方法恰恰都会抛出 UnsupportedOperationException。
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
真正ArrayList,重写add,delete方法,可以正常使用
问题二:List,为什么却还互相影响
问题复现
public static void t2() {
String[] array = {"张三", "李四", "王五"};
List<String> list = Arrays.asList(array);
list.set(0, "001");
array[1] = "002";
System.out.println("array:" + Arrays.toString(array));
System.out.println("list:" + list);
}
效果展示
array:[001, 002, 王五]
list:[001, 002, 王五]
从日志输出可以看到,不管我们是修改原数组,还是新 List 集合,两者都会互相影响。
查看 java.util.Arrays$ArrayList 实现,我们可以发现底层实际使用了原始数组
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
知道了实际原因,修复的办法也很简单,套娃一层 ArrayList 呗!
List<String> list = new ArrayList<>(Arrays.asList(arrays));
public static void t2() {
String[] array = {"张三", "李四", "王五"};
List<String> list = new ArrayList<>(Arrays.asList(array));
list.set(0, "001");
array[1] = "002";
System.out.println("array:" + Arrays.toString(array));
System.out.println("list:" + list);
}
array:[张三, 002, 王五]
list:[001, 李四, 王五]
不过这么写感觉十分繁琐,推荐使用 Guava Lists 提供的方法。
List<String> list = Lists.newArrayList(arrays);
引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
完善后代码
public static void t2() {
String[] array = {"张三", "李四", "王五"};
ArrayList<String> list = Lists.newArrayList(array);
list.set(0, "001");
array[1] = "002";
System.out.println("array:" + Arrays.toString(array));
System.out.println("list:" + list);
}
最终效果
array:[张三, 002, 王五]
list:[001, 李四, 王五]
问题复现二
除了 Arrays#asList产生新集合与原始数组互相影响之外,JDK 另一个方法 List#subList 生成新集合也会与原始 List 互相影响;
public static void t2() {
ArrayList<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
integerList.add(3);
// Returns a view of the portion of this list between the specified
List<Integer> subList = integerList.subList(0, 2);
subList.set(0, 10);
integerList.set(1, 20);
System.out.println("integerList:" + integerList);
System.out.println("subList:" + subList);
}
效果展示
integerList:[10, 20, 3]
subList:[10, 20]
查看 List#subList 实现方式,可以发现这个 SubList 内部有一个 parent 字段保存保存最原始 List
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
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 中,比如 add 方法
出现OOM问题场景
private static List<List<Integer>> data = new ArrayList<>();
private static void oom() {
for (int i = 0; i < 1000; i++) {
List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());
data.add(rawList.subList(0, 1));
}
}
data 看起来最终保存的只是 1000 个具有 1 个元素的 List,不会占用很大空间。但是程序很快就会 OOM。
OOM 的原因正是因为每个 SubList 都强引用个一个 10 万个元素的原始 List,导致 GC 无法回收。
这里修复的办法也很简单,跟上面一样,也来个套娃呗,加一层 ArrayList
问题三:不可变集合,说好不变,你怎么就变了
为了防止 List 集合被误操作,我们可以使用 Collections#unmodifiableList 生成一个不可变(immutable)集合,进行防御性编程。
这个不可变集合只能被读取,不能做任何修改,包括增加,删除,修改,从而保护不可变集合的安全
问题复现
public static void t3() {
ArrayList<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
List<String> unmodifiableList = Collections.unmodifiableList(list);
// Exception in thread "main" java.lang.UnsupportedOperationException
// 看起来没什么问题
unmodifiableList.add("1");
unmodifiableList.remove(1);
unmodifiableList.set(0, "t");
// 以下进行测试
// list.set(0, "first_modify");
// Assertions.assertEquals(list.get(0), unmodifiableList.get(0));
// list.add("fourth");
// Assertions.assertEquals(list.get(3), unmodifiableList.get(3));
// Assertions.assertEquals(list.size(), unmodifiableList.size());
}
效果展示
上面单元测试结果将会全部通过,这就代表 Collections#unmodifiableList 产生不可变集合将会被原始 List 所影响。
分析
查看 Collections#unmodifiableList 底层实现
UnmodifiableList(List<? extends E> list) {
super(list);
// 这里面引入原始list
this.list = list;
}
可以看到这跟上面 SubList 其实是同一个问题,新集合底层实际使用了原始 List。
解决方案
使用 JDK9 List#of 方法。
List<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
List<String> unmodifiableList = List.of(list.toArray(new String[]{}));
使用 Guava immutable list
List<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
List<String> unmodifiableList = ImmutableList.copyOf(list);
相比而言 Guava 方式比较清爽,使用也比较简单,推荐使用 Guava 这种方式生成不可变集合。
问题四:foreach 增加/删除元素大坑
问题复现
public static void t4(){
String[] array = {"1","2","3"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
for (String s : list) {
if("1".equals(s)){
// Exception in thread "main" java.util.ConcurrentModificationException
list.remove(s);
}
}
}
上面代码我们使用foreach方式遍历list集合,如果符合条件,将会从集合中删除该元素;
这个程序编译正常,但是运行时候,程序异常,日志如下
效果展示
Exception in thread “main” java.util.ConcurrentModificationException
at java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 901 ) a t j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList Itr.checkForComodification(ArrayList.java:901)atjava.util.ArrayListItr.next(ArrayList.java:851)
at com.geekmice.onetomany.list.ListTest.t4(ListTest.java:67)
at com.geekmice.onetomany.list.ListTest.main(ListTest.java:14)
分析
可以看到最终错误是由ArrayList$Itr.next处代码抛出,但是代码中我们并没有调用,为什么呢?
实际上这是foreach方式给java提供语法糖,编译后编程另外一种方式,反编译看一下
public static void t4() {
String[] array = new String[]{"1", "2", "3"};
ArrayList<String> list = new ArrayList(Arrays.asList(array));
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if ("1".equals(s)) {
list.remove(s);
}
}
}
可以看到foreach这种方式实际就是迭代器Iterator实现的,这也就是foreach被遍历的类需要实现Iterator接口的原因
看到modCount 和expectedModCount不同才错误,再看看这两个属性什么含义
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
modCount 来源于 ArrayList 的父类 AbstractList,可以用来记录 List 集合被修改的次数
modCount 计数操作将会交子类自己操作,ArrayList 每次修改操作(增、删)都会使 modCount 加 1
解决方案
使用迭代器删除
String[] array = {"1","2","3"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String str = iterator.next();
if(str.equals("1"){
iterator.remove();
}
}
使用removeIf删除
String[] array = {"1","2","3"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
list.removeIf(str->str.equals("1"));
问题五:普通for循环删除元素失败
问题复现
描述
目前有个业务,数据库返回list数据,年纪大于19岁删除,只需要小于等于19的数据
相关代码
package com.geekmice.onetomany.list;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@SpringBootTest
public class ListTest {
@Test
public void t6() {
List<Student> list = new ArrayList<Student>() {{
add(new Student("豹子头", 18));
add(new Student("鲁提辖", 20));
add(new Student("赖老三", 29));
}};
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getAge() > 19) {
list.remove(i);
}
}
System.out.println(list);
// [Student{userName='豹子头', age=18}, Student{userName='赖老三', age=29}]
}
}
问题分析
奇怪的是,明明条件是年龄大于19删除对应数据,'鲁提辖’这条数据却没有被清除;
删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
解决方案
普通for循环改善
顺序循环时,删除当前位置的值,下一个值就会补到当前位置,所以需要执行i–操作
List<Student> list = new ArrayList<Student>() {{
add(new Student("豹子头", 18));
add(new Student("鲁提辖", 20));
add(new Student("赖老三", 29));
}};
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getAge() > 19) {
list.remove(i);
i--; // 关键代码
}
}
System.out.println(list);
迭代器遍历删除
注意必须用迭代器的remove()方法,不要用list的remove,不然会发生java.util.ConcurrentModificationException 异常
System.out.println("迭代器遍历删除开始");
Iterator<Student> studentIterator = list.iterator();
while (studentIterator.hasNext()) {
if (studentIterator.next().getAge() > 19) {
studentIterator.remove();
}
}
System.out.println("迭代器遍历删除结束");
倒序遍历删除
因为list删除只会导致当前元素之后的元素位置发生改变,所以采用倒序可以保证前面的元素没有变化
List<Student> list = new ArrayList<Student>() {{
add(new Student("豹子头", 18));
add(new Student("鲁提辖", 20));
add(new Student("赖老三", 29));
}};
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).getAge() > 19) {
list.remove(i);
}
}
System.out.println(list);
stream流处理
List<Student> list = new ArrayList<Student>() {{
add(new Student("豹子头", 18));
add(new Student("鲁提辖", 20));
add(new Student("赖老三", 29));
}};
list.removeIf(item -> item.getAge() > 19);
总结
第一、Arrays.asList和List.subList就是一个普通独立的ArrayList
如果没有办法,使用了Arrays.asList和List.sublist,返回给其他方法时候,一定要嵌套真正的ArrayList
第二、jdk提供的不可变集合非常笨重,低效,不安全,推荐使用guava不可变集合
第三、不要随便在foreach增加/删除元素
第四、ArrayList是从新复制一个数组返回的,所以从后往前删除才删的干净。
参考博文
1、List如何在遍历时删除元素