我正在研究轮廓扫描仪。对于每个轮廓,我想保存角落/边缘协调。 而不是在轮廓中有一个数组,我有一个大的数组,我分享。这样做的原因是它必须处理动画,因此这是为了优化。
这个想法是每个轮廓得到一个大数组的子列表视图。
我在课堂上工作了很长时间才能轻松完成,现在我遇到了一个问题:
ArrayList<PVector> vecs = new ArrayList<PVector>();
for (int i = 0; i < 10; i++) {
vecs.add(new PVector());
}
List<PVector> subList = vecs.subList(0, 5);
for (int i = 0; i < 10; i++) {
vecs.add(new PVector());
}
// ConcurrentModificationException
for (int i = 0; i < subList.size (); i++) {
}
首先,如果我使用add(Object)
,那么它的java设计是否会引发并发修改?这应该对我已经拥有的任何子列表没有影响,因为它添加到右边? (逻辑来说)。我的观点是,add(Object)
永远不会影响已经存在的子列表,只有add(index, Object)
可以做到(以及删除,交换和排序等其他内容)。
二。处理这个问题的好方法是什么?我可以让大数组真的很大,所以在我已经创建了一个子列表之后我不太可能需要添加元素,但我想知道是否还有其他好的方法来处理它。
这是我制作的课程,这对我来说很容易让我的生活变得艰难:)
public class ListDivisor<T> {
List<T> list;
int subListStartIndex = 0;
int currentGetIndex = 0;
InstanceHelper instanceHelper;
public ListDivisor(List<T> list, InstanceHelper<T> instanceHelper) {
this.list = list;
this.instanceHelper = instanceHelper;
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public void reset() {
subListStartIndex = 0;
currentGetIndex = 0;
if (instanceHelper.doResetInstances()) {
for (T obj : list) {
instanceHelper.resetInstance(obj);
}
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public List<T> getSubList(int size) {
int fromIndex = subListStartIndex; // inclusive
int toIndex = fromIndex + size; // exclusive
for (int i = list.size(); i < toIndex; i++) {
list.add((T) instanceHelper.createInstance());
}
subListStartIndex = toIndex;
currentGetIndex = toIndex;
return list.subList(fromIndex, toIndex);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Returns a subList starting where the previous subList ended till
* the latest object added till then.
*
* @return
*/
public List<T> getSubList() {
return getSubList(currentGetIndex-subListStartIndex);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public T getNext() {
if (currentGetIndex >= list.size()) {
list.add((T)instanceHelper.createInstance());
}
return list.get(currentGetIndex++);
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public void clear() {
list.clear();
reset();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
public interface InstanceHelper<T> {
public T createInstance();
public boolean doResetInstances();
public void resetInstance(T obj);
}
}
这是如何使用该类的一个小例子:
ListDivisor<PVector> vectorsDivisor = new ListDivisor<PVector>(
new ArrayList<PVector>(),
new InstanceHelper<PVector>() {
//@Override
public PVector createInstance() {
return new PVector();
}
//@Override
public boolean doResetInstances() {
return true;
}
//@Override
public void resetInstance(PVector v) {
v.set(0,0,0);
}
});
PVector v = vectorsDivisor.getNext();
v.set(1, 1, 1);
v = vectorsDivisor.getNext();
v.set(2, 2, 2);
v = vectorsDivisor.getNext();
v.set(3, 3, 3);
subList = vectorsDivisor.getSubList();
println("subList size: "+subList.size());
答案 0 :(得分:2)
根据ArrayList.sublist
的javadoc:
“如果支持列表(即此列表)在结构上以除返回列表之外的任何方式进行修改,则此方法返回的列表的语义将变为未定义。(结构修改是那些更改此列表大小的修改,或以其他方式扰乱它,使得正在进行的迭代可能产生不正确的结果。)“
在您正在使用的JVM中的ArrayList
实现中,“未指定”的语义似乎是抛出ConcurrentModificationException
。
首先,如果我使用add(Object),那么它的java设计是否会引发并发修改?
不。这不是“糟糕的设计”。
他们是故意设计的,并且有充分的理由。此外,他们记录,如果你做了你正在做的事情,你会得到未指明的行为。事实上,如果检测到并发修改,他们已经将子列表类实现为快速失败。这是一件好事......并且完全符合API规范。
这应该对我已经拥有的任何子列表没有影响,因为它添加到右边?
问题在于,虽然你的例子(可能)是安全的,但其他人却不是。如果不添加大量额外的基础架构,很可能会使安全和不安全案例区分开来,这些基础架构最有可能使ArrayList
效率降低,而且对其他用例的内存密集程度更高。
(因此我的“有充分理由”的评论如上。)
二。处理这个问题的好方法是什么?
如果不了解您的代码实际需要做什么,很难就此提出建议。但是我想到了一些通用的替代方案:
更改算法,以便不使用List.sublist方法;例如将您的名义子列表的第一个和最后一个索引作为参数传递。
每次更新支持列表时重新创建子列表对象。 (假设您对支持列表的更新非常安全,那么这很简单,而且非常正确......假设您没有使用子列表的迭代器。)
将您的代码更改为使用Queue
或Deque
代替List
。
实现您自己的List类(基于ArrayList
),在此安全方案中不会抛出CME。
答案 1 :(得分:1)
ArrayList的subList方法的documentation表示只应在后备列表的结构不变的情况下使用子列表:
如果支持列表(即此列表)在结构上以除返回列表之外的任何方式进行修改,则此方法返回的列表的语义将变为未定义。 (结构修改是那些改变了这个列表的大小,或以其他方式扰乱它的方式,正在进行的迭代可能会产生不正确的结果。)
虽然你是对的,但是添加到支持列表的末尾不应该导致子列表中的任何意外行为,在这种情况下Java是悲观的,如果你使用的话,抛出ConcurrentModificationException
支持列表更改后以任何方式的子列表。这类似于列表迭代器的快速失败行为,这也在the ArrayList docs中进行了解释,它认为最好是干净地失败而不是猜测哪些并发修改是&# 34;安全&#34;并最终出现意外行为。
当然,解决这个问题的最简单方法是在创建子列表之前完成对大型ArrayList的所有结构修改。您是否有任何理由需要交换创建子列表和扩展ArrayList?
如果你在使用它的子部分时绝对需要修改大型ArrayList,我建议不要使用子列表,只是直接使用索引到ArrayList中。每个轮廓任务都会获得它正在处理的ArrayList的子部分的开始和结束索引,并且它将通过在大型ArrayList上重复使用get()
来完成它们。使用显式get()
调用进行迭代非常重要,因为当您添加到大型ArrayList的末尾时,使用迭代器或for-each循环进行迭代将遇到相同的ConcurrentModificationException
问题。
或者,如果您确实需要每个轮廓任务的子列表的List
- 兼容视图,您可以实现自己的SubList类,如果其后备列表得到add()
,则不会抛出异常to,然后编写一个覆盖ArrayList
的{{1}}的小子类来返回它。 Java subList()
和ArrayList
的源代码是available online,因此如果您想确保您的SubList完成Java所做的一切,您可以参考它。