这使我对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()
的类型更改为List<Integer>
,并使其在PRIMES
和threshold < 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
的私有素数存储进行无意的修改,还是我的想象力失败了?
答案 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的一般评论,而不是对程序的评论。