以这种方式复制ArrayList <E>是否安全?

时间:2020-03-04 05:03:45

标签: java 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()大于thresholdcurrThresh工作得相当好。要通过此测试:

    @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()的类型更改为List<Integer>,并使其在PRIMESthreshold < currThresh分支中返回threshold > 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的私有素数存储进行无意的修改,还是我的想象力失败了?

1 个答案:

答案 0 :(得分:1)

因此,您正在使用可变集合来保存不可变对象,但是那些不可变对象包装了基元,因此可能涉及装箱和拆箱。难怪你对此不确定。

但是请看一下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()中所看到的那样,通过将子列表通过此特定的ArrayList构造函数,调用者可以做与列表有关的任何事情,而不必担心引起PrimeLister有问题。

即使不对整数进行装箱和/或取消装箱,支持返回的ArrayList的数组现在也独立于PrimeLister的私有质数存储。

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

相关问题