首页 > 解决方案 > 复制`ArrayList是否安全`以这种方式?

问题描述

这让我更加欣赏 Scala 中的不可变集合。

假设我们有一个 Java 静态类,它返回一个ArrayList<Integer>介于 1 和某个指定截止值之间的素数。我从这个开始:

package org.oeis.primes;

import java.util.ArrayList;

public class PrimeLister {

    private static final ArrayList<Integer> PRIMES = new ArrayList<>();
    private static int currThresh;

    static {
        PRIMES.add(2);
        PRIMES.add(3);
        PRIMES.add(5);
        PRIMES.add(7);
        currThresh = 10;
    }

    // STUB TO FAIL THE FIRST TEST
    public static ArrayList<Integer> listPrimes(int threshold) {
        ArrayList<Integer> selPrimes = new ArrayList<>(PRIMES);
        return selPrimes;
    }

}

然后,经过几个 TDD 周期,listPrimes()threshold大于currThresh. 要通过此测试:

    @Test
    public void testPrimeListerCanTrim() {
        int threshold = 80;
        ArrayList<Integer> result = PrimeLister.listPrimes(threshold);
        System.out.println("PrimeLister reports " + result.size() 
                + " primes between 1 and " + threshold);
        threshold = 20;
        Integer[] smallPrimes = {2, 3, 5, 7, 11, 13, 17, 19};
        ArrayList<Integer> expResult = new ArrayList<>(Arrays.asList(smallPrimes));
        result = PrimeLister.listPrimes(threshold);
        assertEquals(expResult, result);
    }

listPrimes()需要取 的一个子集PRIMES

        // Not yet worried that threshold could be negative
        if (threshold < currThresh) {
            int trimIndex = PRIMES.size();
            int p;
            do {
                trimIndex--;
                p = PRIMES.get(trimIndex);
            } while (p > threshold);
            return new ArrayList<>(PRIMES.subList(0, trimIndex + 1));
        }

使用素数定理肯定有一种更快的方法来找到正确trimIndex的,但我目前并不担心。我担心的是subList()Javadoc 中的这个花絮:

返回的列表由该列表支持,因此返回列表中的非结构性更改会反映在该列表中,反之亦然。...如果后备列表(即此列表)以除通过返回列表之外的任何方式进行结构修改,则此方法返回的列表的语义变得未定义。

我不太明白这意味着什么。我尝试更改listPrimes()to的类型List<Integer>并使其PRIMESthreshold < currThreshthreshold > currThresh分支中都返回。然后我写了这个测试:

    @Test
    public void testModifyPrimeSubset() {
        int threshold = 20;
        List<Integer> subset = PrimeLister.listPrimes(threshold);
        for (int i = 0; i < subset.size(); i++) {
            int p = -subset.get(i);
            subset.set(i, p);
        }
        threshold = 40;
        Integer[] smallPrimes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
        ArrayList<Integer> expResult = new ArrayList<>(Arrays.asList(smallPrimes));
        List<Integer> result = PrimeLister.listPrimes(threshold);
        assertEquals(expResult, result);
    }

正如预期PRIMES的那样,直接返回允许调用者修改PrimeLister私有质数存储。我的一个测试由于得到 -2、-3、-5 等而失败。其他测试导致IndexOutOfBoundsException. 将副本恢复到新实例ArrayList<Integer>会使所有测试再次通过。

我的问题:这是否足以防止对PrimeLister的私有质数存储进行无意修改,还是我的想象力失败了?

标签: javaarraylist

解决方案


因此,您使用可变集合来保存不可变对象,但这些不可变对象包装原语,因此可能涉及一些装箱和拆箱。难怪你对此不确定。

但是看一下JDK源代码。您可以在 IntelliJ IDEA Ultimate Edition 中执行此操作,大概也可以在其他 IDE 中执行此操作。这是相关的构造函数:

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

由于多态性,有几种不同的可能性toArray()被调用。但是,我们可能可以指望 JDK 的作者来维护某些保证。

public abstract Object[] toArray()

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

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

testModifyPrimeSubset()因此,正如您已经ArrayListPrimeLister.

即使整数没有被装箱和/或拆箱,支持返回的数组ArrayList现在也独立于PrimeLister的私有质数存储。

最后,我想有趣地指出,PrimeLister如此嫉妒地保护本质上是公共领域的信息是多么荒谬。这是对 Java 的一般评论,而不是对您的程序的评论。


推荐阅读