是否有充分的理由将 OpenJDK 代码中的以下 ArrayList 构造函数分为两种不同的情况?

Jes*_*say 12 java reflection constructor arraylist java-17

例如,在 JDK17 中阅读 ArrayList 类的 OpenJDK 代码时

(https://github.com/openjdk/jdk17/blob/master/src/java.base/share/classes/java/util/ArrayList.java)

我偶然发现了以下构造函数:

public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}
Run Code Online (Sandbox Code Playgroud)

c.getClass()区分是或不是的原因是什么ArrayList.class?这个案子有必要分拆吗?

(我只是想理解 OpenJDK 类 ArrayList 发行版中的 Java 代码。)

Sla*_*law 6

免责声明:我没有参与编写 的代码java.util.ArrayList,也不知道有谁参与过。

\n
\n

您正在查看的构造函数是“复制构造函数”;它的目的是创建给定集合的浅表副本c。这意味着 的元素c必须复制到新集合中,但新集合的“结构”(在 的情况下为数组ArrayList)必须不同。否则,结构修改也c将通过新集合可见,反之亦然。

\n

这部分:

\n
\n
if (c.getClass() == ArrayList.class) {\n    elementData = a;\n} else {\n    elementData = Arrays.copyOf(a, size, Object[].class);\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n

确保新的ArrayList不c共享同一个阵列。但ArrayList 知道它自己的实现toArray()返回一个不同的数组。因此,当c是 an时ArrayList,它避免复制数组,从而避免不必要的工作。否则,如果c是任何其他集合类型的实例,包括(可以覆盖),\xc2\xa0的子类型 ,则不再依赖 的知识,因此将创建数组的防御性副本。ArrayListtoArray()toArray()

\n

也就是说,国家合同Collection#toArray():

\n
\n

返回的数组将是“安全的”,因为该集合不维护对它的引用。(换句话说,即使此集合由数组支持,此方法也必须分配一个新数组)。因此,调用者可以自由修改返回的数组。

\n
\n

因此,看起来复制数组是没有必要的,使得上面的代码显得多余。但我可以想到应该复制数组的一个原因:

\n
    \n
  • 安全。的恶意实现Collection可能会破坏数组的契约toArray()并保留对数组的引用,从而允许攻击者意外地查看和/或修改新的ArrayList. 由于ArrayList它是一个使用非常广泛的类,因此防范此类事情很重要。

    \n
  • \n
  • 额外原因(来自meron的回答):确保数组的组件类型是Object。的组件类型elementData必须能够Object与 的通用性质一起正确工作ArrayList。恶意或其他方式的错误实现可能会通过返回具有错误组件类型的数组来Collection强制执行。通过传递到ArrayStoreExceptiontoArray()Object[].classArrays.copyOf,上面的代码避免了这个问题。

    \n
  • \n
\n

然而,在这两种情况下,ArrayList仍然知道它自己的实现toArray()返回一个组件类型为的不同数组Object,因此在以下情况下仍然可以避免复制数组:c.getClass() == ArrayList.class。

\n
\n

示范

\n

下面是一些代码,演示了当数组未正确复制时上述两点所描述的问题。

\n

主要.java:

\n
import java.util.Arrays;\nimport java.util.Objects;\n\npublic class Main {\n\n    public static void main(String[] args) {\n        MaliciousStringCollection malicious = new MaliciousStringCollection();\n\n        MyArrayList<Object> objects = new MyArrayList<>(malicious);\n\n        System.out.println("Current elements of \'objects\':");\n        for (int i = 0; i < objects.size(); i++) {\n            System.out.printf("    objects[%d] = %s%n", i, objects.get(i));\n        }\n        System.out.println();\n\n        // Demonstrate how not copying the array can lead to the list\'s array being\n        // modified unexpectedly.\n        System.out.println("Changing elements of \'objects\' via \'malicious\'. New elements of \'objects\':");\n        malicious.changeData();\n        for (int i = 0; i < objects.size(); i++) {\n            System.out.printf("    objects[%d] = %s%n", i, objects.get(i));\n        }\n        System.out.println();\n\n        // Demonstrate how not ensuring the component type of the backing array is `Object` can\n        // lead to an ArrayStoreException.\n        System.out.println("Adding \'42\' to \'objects\'.");\n        objects.add(42); // throws ArrayStoreException\n    }\n\n    public interface MyCollection<E> {\n\n        /**\n         * @return a new array, with a component type of {@code Object}, that contains\n         *         the elements of this collection in encounter order\n         */\n        Object[] toArray();\n\n        /**\n         * @return the size of this collection\n         */\n        int size();\n    }\n\n    public static class MyArrayList<E> implements MyCollection<E> {\n\n        private Object[] data;\n        private int size;\n\n        public MyArrayList(MyCollection<? extends E> col) {\n            size = col.size();\n            /*\n             * Fails two things:\n             * \n             *     1. To ensure the component type of \'data\' is \'Object\'\n             *     2. To ensure the array is not shared with other code.\n             * \n             * Both things can be fixed by changing the code to:\n             * \n             *     data = Arrays.copyOf(col.toArray(), size, Object[].class);\n             */\n            data = col.toArray();\n        }\n\n        public void add(E e) {\n            if (data.length == size) {\n                int newLength = data.length + Math.max(data.length / 2, 1);\n                data = Arrays.copyOf(data, newLength);\n            }\n            data[size++] = e;\n        }\n\n        @SuppressWarnings("unchecked")\n        public E get(int index) {\n            Objects.checkIndex(index, size);\n            return (E) data[index];\n        }\n\n        @Override\n        public int size() {\n            return size;\n        }\n\n        @Override\n        public Object[] toArray() {\n            return Arrays.copyOf(data, size);\n        }\n    }\n\n    public static class MaliciousStringCollection implements MyCollection<String> {\n\n        private final String[] data = {"Hello", "World"};\n\n        @Override\n        public int size() {\n            return data.length;\n        }\n\n        public void changeData() {\n            data[0] = "Goodbye";\n        }\n        \n        @Override\n        public Object[] toArray() {\n            /*\n             * Malicious implementation:\n             * \n             *     1. Does not return a new, distinct array.\n             *     2. Returns an array whose component type is \'String\', not \'Object\'.\n             */\n            return data;\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

输出:

\n
Current elements of \'objects\':\n    objects[0] = Hello\n    objects[1] = World\n\nChanging elements of \'objects\' via \'malicious\'. New elements of \'objects\':\n    objects[0] = Goodbye\n    objects[1] = World\n\nAdding \'42\' to \'objects\'.\nException in thread "main" java.lang.ArrayStoreException: java.lang.Integer\n        at Main$MyArrayList.add(Main.java:71)\n        at Main.main(Main.java:29)\n
Run Code Online (Sandbox Code Playgroud)\n

  • 它的实施方式是最稳健的。它只需要传入的集合就能正确实现单个操作;它甚至适用于在执行构造函数时进行更新的并发集合,只要“toArray”操作返回有效结果(原子一致或弱一致)。 (2认同)

mer*_*ike 5

如果您对该源文件执行git Blame,您会发现此代码是作为提交343ecd806bb050的一部分引入的,其提交消息

8231800:更好的数组列表

在 JDK bugtracker 中引用了问题 JDK-8231800。唉,这个问题对公众来说是不可见的,但谷歌搜索显示它已经附带了当时支持的所有 Java 版本的安全更新,提供了强有力的证据表明这与安全问题有关(这也解释了为什么详细信息该问题尚未公开,以及为什么提交消息没有描述性)。

然而,谷歌搜索还发现了该代码的作者提交的增强请求,它确实描述了这段代码旨在解决的挑战:

考虑 new ArrayList(Collection arg)。这会调用 arg.toArray() 并为其创建一个防御性副本,以用作 ArrayList 的内部数组。如果 arg 的 toArray() 实现生成一个类不是 Object[].class 的数组,或者它保留对返回数组的引用,则此副本是必需的。(这两者都违反了规范,但它们是不可执行的。)

也就是说,ArrayList需要它的数组来

  • 承认类型元素E
  • 其他代码无法访问(因此无法修改)

指定的实现Collection.toArray是为了满足这一点:

Object[] toArray()

返回一个包含此集合中所有元素的数组。如果此集合对其迭代器返回其元素的顺序做出任何保证,则此方法必须以相同的顺序返回元素。返回的数组的运行时组件类型是Object。

返回的数组将是“安全的”,因为该集合不维护对它的引用。(换句话说,即使此集合由数组支持,此方法也必须分配一个新数组)。因此,调用者可以自由修改返回的数组。

但是,我们无法确定实现是否确实做到了这一点。例如,一个实现可能会执行以下操作:

class StringList implements Collection<String> {
    String[] elements = new String[10];
    int size = 0;
    
    // other methods omitted

    @Override 
    Object[] toArray() {
        return Arrays.copyOf(elements, size);
    }
}
Run Code Online (Sandbox Code Playgroud)

这看起来完全无辜,对吧?但这个实现实际上违反了规范,因为copyOf将返回 aString[]而不是 an Object[],所以如果有人这样做:

    var strings = new StringList("Hello", "World");
    var objects = new ArrayList<Object>(strings);
    objects.add(42); 
Run Code Online (Sandbox Code Playgroud)

就编译时类型系统而言,该代码是完全正确的,但ArrayStoreException如果ArrayList重用StringList.toArray().

第二个问题更加邪恶。假设你有这样的代码:

class AccessManager {
    final List<Permission> permissions;

    public AccessManager(List<Permission> requestedPermissions) {
        permissions = new ArrayList<>(requestedPermissions);
        verifyPermissions();
    }

    private verifyPermissions() { 
        for (var p : permissions) {
            if (!currentUser.has(p)) {
                throw new SecurityException();
            }
        }
    }

    boolean checkAccess(Permission p) {
        return permissions.contains(p);
    }
}
Run Code Online (Sandbox Code Playgroud)

一些邪恶的黑客宣称:

class NefariousList implements Collection<Permission> {
    Object[] permissions = { new InnocentPermission() };

    @Override
    public Object[] toArray() {
        return permissions;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后做了:

var nefarious = new NefariousList();
var accessManager = new AccessManager(nefarious);
var service = new Service(accessManager);

nefarious.permissions[0] = new AdminPermission();
service.deleteDatabase();
Run Code Online (Sandbox Code Playgroud)

他们可以成功删除数据库,因为在经过验证的权限列表中AccessManager找到了...AdminPermission

(如果您认为这种带有参考共享错误的手写安全代码是人为的,请从最近的 stackoverflow 问题中查看这个真实世界的示例)

由于 Java 语言的核心类需要正确工作,即使不受信任的代码在同一个 JVM 中执行,ArrayList因此通常不能依赖于Collection.toArray正确实现,除非已验证实现实际上是安全的,因为它来自值得信赖的 JDK 类。

  • 注意,在旧的实现中 `Arrays.toList("foo").toArray().getClass() == String[].class`。 (2认同)