JDK中的Bug: Array和List的互相转化

9/19/2020 BUG

# JDK中的Bug: Array和List的互相转化

​众所周知,JDK提供了一对方法来进行Array和List的互相转换:

  • Arrays.asList() Array->List

  • arr.toArray() List->Array

但是上述方法内部含有一些已知bug,会导致编译器正常但运行时报错

# 报错

// TODO 放入链接

Bug示例代码:

public class ToArrayBugExperiment {
    public static void main(String[] args) {
        Child[] childArray = {new Child(), new Child()};
//        使用List<Object>接响应
        List<Object> arr = Arrays.asList(childArray);  
        try {
            // 此处报错1
            arr.set(0, new Object());
        } catch (Exception e) {
            e.printStackTrace();
        }
//        正确的方法
        List<Child> childArrayList = Arrays.asList(childArray);
//        调用带参的toArray方法
        Object[] withParam = childArrayList.toArray(new Object[0]);
        System.out.println("带参方法的返回数组类型:"+withParam.getClass());
        withParam[0] = new Object();
//        调用无参的toArray方法
        Object[] withoutParam = childArrayList.toArray();
        System.out.println("无参方法的返回数组类型:"+withoutParam.getClass());
        try {
            // 此处报错2
            withoutParam[0] = new Object();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static class Child extends Parent{}

    private static class Parent{}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

此代码可以正常编译,但是运行时抛出异常: toArrayException.png

  • 报错1:将一个Child类型的array,通过Arrays.asList转化成了一个List<Object>,理论上此类型的List可以存储任何Object类型(及其子类型)的对象,下面的set方法理应可以正常执行,但是报错了
  • 报错2:Arrays.asList转换而成的List,再次执行toArray()后的Object[]数组,放入Object对象报错

# 原因(JDK8)

如果仔细查看java.util.Arrays#asList方法,会发现其只是将入参数组传入了ArrayList的构造器,并返回构造出的实例,此处有几个关键点:

  1. 这里的类是java.util.Arrays.ArrayList,是Arrays的内部类,而非常用的java.util.ArrayList
  2. 看下述的源码,此类只是简单的将传入的array的引用赋值给内部的数组
  3. 后面的getset方法,都是直接对包裹的数组进行操作。因此在上述测试代码set的时候,实质上是想赋值一个Child类型的数组的位置为Object,根据数组的约定,是不允许的,因此报错
  4. toArray()方法调用clone(),此方法会直接复制原数组,因此返回值仍是Child[]类型,发生了协变,即实际类型为Child[]的数组,被一个Object[]类型的变量引用
  5. toArray(T... a)方法调用Arrays.copyOf或者System.arraycopy方法,两种方法均保证返回值数组为Object[]类型
private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private final E[] a;

    ArrayList(E[] array) {
        // 这里是构造方法,直接引用赋值
        a = Objects.requireNonNull(array);
    }

    // 下面两个方法实现不同
    @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;
    }

    // 省略其他方法实现
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 解决

  1. 使用Arrays.asList(T... a)方法时,注意返回值的List的泛型应该与传入的类型T一致,编译器不会报错

    List<Child> arr = Arrays.asList(childArray);  // <== 此处List的泛型应为Child,不能随便修改
    
    1
  2. 使用toArray(T[] a)来保证结果数组与预期类型一致,避免使用无参的toArray()方法

# JDK11的变化

JDK11中,java.util.Arrays.ArrayList类的实现有变化, toArray()方法同时会修改返回值的类型,强制转为Object[],具体代码如下:

    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        // 这是JDK8的实现
        @Override
        public Object[] toArray() {
            return a.clone();
        }
        // 这是JDK11的实现,数组的类型有变化
        @Override
        public Object[] toArray() {
            return Arrays.copyOf(a, a.length, Object[].class);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

对于JDK11来说,返回的数组永远是Object[],不会因为协变导致问题。因此第二个问题不会报错。

Last Updated: 1/22/2024, 8:56:53 AM