8

不要再踩 ArrayList 的这些坑了

 3 years ago
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.
neoserver,ios ssh client

不要再踩 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 行代码做个测试,运行起来确实抛了异常,异常如下:

v2-cc0e37663b4a117c291a2f1306e82842_720w.jpeg

直接看源码吧,定位到 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);
        }
}

里面定义了 setget等基本的方法,但是没有重写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 坑的程序员,不足以谈人生。所以,各位同学在使用一些看似简单、优雅的方法时,一定要清楚它的特性和原理,不然就离坑不远了。

公众号:古时的风筝


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK