问题复现
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方法,可以正常使用
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.guavagroupId>
<artifactId>guavaartifactId>
<version>22.0version>
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 这种方式生成不可变集合。
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"));
第一、Arrays.asList和List.subList就是一个普通独立的ArrayList
如果没有办法,使用了Arrays.asList和List.sublist,返回给其他方法时候,一定要嵌套真正的ArrayList
第二、jdk提供的不可变集合非常笨重,低效,不安全,推荐使用guava不可变集合
第三、不要随便在foreach增加/删除元素