不要再踩 ArrayList 的这些坑了
source link: https://zhuanlan.zhihu.com/p/144520933
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
不要再踩 ArrayList 的这些坑了
请看下面的代码,谁能看出它有什么问题吗?
String a = "古时的";
String b = "风筝";
List<String> stringList = Arrays.asList(a,b);
stringList.add("!!!");
这是一个小白程序员问我的问题。
白:成哥,帮我看看这代码有什么问题吗,为什么报错呢,啥操作都没有啊?
我:看上去确实没什么问题,但是我确实没用过 Arrays.asList
这个方法,报什么错误?
白:异常信息是 java.lang.UnsupportedOperationException
,是调用 add
方法时抛出的。
恩,我大概明白了,这可能是 ArrayList
的又一个坑,和 subList
应该有异曲同工之妙。
Arrays.asList
Arrays.asList
方法接收一个变长泛型,最后返回 List,好像是个很好用的方法啊,有了它,我们总是说的 ArrayList
初始化方式是不是就能更优雅了,既不用{{
这种双括号方式,也不用先 new ArrayList
,然后再调用 add
方法一个个往里加了。但是,为啥没有提到这种方式呢?
虽然问题很简单,但还是有必要看一下原因的。于是,写了上面这 4 行代码做个测试,运行起来确实抛了异常,异常如下:
直接看源码吧,定位到 Arrays.asList
方法看一看。
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
咦,是 new 了一个 ArrayList
出来呀,怎么会不支持 add
操作呢,不仔细看还真容易被唬住,此ArrayList
非彼ArrayList
,这是一个内部类,但是类名也叫 ArrayList
,你说坑不坑。
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable {
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public Object[] toArray() {
return a.clone();
}
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@Override
public E get(int index) {
return a[index];
}
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
@Override
public int indexOf(Object o) {
E[] a = this.a;
if (o == null) {
for (int i = 0; i < a.length; i++)
if (a[i] == null)
return i;
} else {
for (int i = 0; i < a.length; i++)
if (o.equals(a[i]))
return i;
}
return -1;
}
@Override
public boolean contains(Object o) {
return indexOf(o) != -1;
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(a, Spliterator.ORDERED);
}
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E e : a) {
action.accept(e);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
E[] a = this.a;
for (int i = 0; i < a.length; i++) {
a[i] = operator.apply(a[i]);
}
}
@Override
public void sort(Comparator<? super E> c) {
Arrays.sort(a, c);
}
}
里面定义了 set
、get
等基本的方法,但是没有重写add
方法,这个类也是继承了 AbstractList
,但是 add
方法并没有具体的实现,而是抛了异常出来,具体的逻辑需要子类自己去实现的。
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
所以说,Arrays.asList
方法创建出来的 ArrayList
和真正我们平时用的 ArrayList
只是继承自同一抽象类的两个不同子类,而 Arrays.asList
创建的 ArrayList
只能做一些简单的视图使用,不能做过多操作,所以 ArrayList
的几种初始化方式里没有 Arrays.asList
这一说。
subList 方法
上面提到了那个问题和 subList
的坑有异曲同工之妙,都是由于返回的对象并不是真正的 ArrayList
类型,而是和 ArrayList
集成同一父类的不同子类而已。
所以会产生第一个坑,就是把当把 subList
返回的对象转换成 ArrayList
的时候
List<String> stringList = new ArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("风筝");
List<String> subList = (ArrayList) stringList.subList(0, 2);
会抛出下面的异常:
java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList
原因很明了,因为这俩根本不是一个对象,也不存在继承关系,如果真说有什么关系,顶多算是兄弟关系,因为都继承了 AbstractList
嘛 。
当你在 subList 中操作的时候,其实就是在操作原始的 ArrayList
,不明所以的同学以为这是一个副本列表,然后在 subList 上一顿操作猛如虎,最后回头一看原始 ArrayList
已然成了二百五。
例如下面这段代码,在 subList 上新增了一个元素,然后又删除了开头的一个元素,结果回头一看原始的 ArrayList,发现它的结果也发生了变化。
List<String> stringList = new ArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("风筝");
List<String> subList = stringList.subList(0, 3);
subList.add("!!!");
subList.remove(0);
System.out.println("------------------");
System.out.println("修改后的 subList");
System.out.println("------------------");
for (String s : subList) {
System.out.println(s);
}
System.out.println("------------------");
System.out.println("原始 ArrayList");
System.out.println("------------------");
for (String a : stringList) {
System.out.println(a);
}
以上代码的输出结果:
------------------
修改后的 subList
------------------
是
风筝
!!!
------------------
原始 ArrayList
------------------
是
风筝
!!!
为什么会发生这样的情况呢,因为 subList
的实现就是这样子啊,捂脸。我们可以看一下 subList 这个方法的源码。
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
看到它内部是 new 了一个 SubList 类,这个类就是上面提到的 ArrayList
的子类,看到第一个参数 this
了吗,this
就是当前的 ArrayList
原始列表,之后的增删改其实都是在 this
上操作,最终也就是在原始列表上进行的操作,所以你的一举一动最后都会诚实的反应到原始列表上,之后你再想用原始列表,对不起,已经找不到了。
如果你使用 subList 方法获取了一个子列表,这之后又在原始列表上进行了新增或删除的操作,这是,你之前获取到的 subList 就已经废掉了,不能用了,不能用的意思就是你在 subList 上进行遍历、增加、删除操作都会抛出异常,没错,连遍历都不行了。
例如下面这段代码
List<String> stringList = new ArrayList<>();
stringList.add("我");
stringList.add("是");
stringList.add("风筝");
List<String> subList = stringList.subList(0, 3);
// 原始列表元素个数改变
stringList.add("!!!");
// 遍历 subList
for (String s : subList) {
System.out.println(s);
}
// get 元素
subList.get(0);
// remove 元素
subList.remove(0);
//增加元素
subList.add("hello");
遍历、get、remove、add 都会抛出以下异常
其实与二坑的原因相同,subList 其实操作的是原始列表,当你在 subList 上进行操作时,会执行 checkForComodification
方法,此方法会检查原始列表的个数是否和最初的相同,如果不相同,直接抛出 ConcurrentModificationException
异常。
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
没有在项目中踩过 JDK 坑的程序员,不足以谈人生。所以,各位同学在使用一些看似简单、优雅的方法时,一定要清楚它的特性和原理,不然就离坑不远了。
公众号:古时的风筝。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK