【问题标题】:Updating a PriorityQueue when iterating it迭代时更新 PriorityQueue
【发布时间】:2011-08-18 10:25:24
【问题描述】:

我需要根据它们的 ID 更新 PriorityQueue 中的一些固定优先级元素。我认为这是一个很常见的场景,这里有一个示例 sn-p (Android 2.2):

for (Entry e : mEntries) {
    if (e.getId().equals(someId)) {
        e.setData(newData);
    }
}

然后我将Entry 设置为“不可变”(没有setter 方法),以便创建一个新的Entry 实例并由setData() 返回。我将我的方法修改为:

for (Entry e : mEntries) {
    if (e.getId().equals(someId)) {
        Entry newEntry = e.setData(newData);
        mEntries.remove(e);
        mEntries.add(newEntry);
     }
}

代码似乎工作正常,但有人指出,在迭代队列时修改队列是个坏主意:它可能会引发 ConcurrentModificationException,我需要将要删除的元素添加到 ArrayList 并删除稍后。他没有解释原因,对我来说这似乎是一个很大的开销,但我在互联网上找不到任何具体的解释。

This post 类似,但优先级可以改变,这不是我的情况)

谁能澄清我的代码有什么问题,我应该如何更改它以及 - 最重要的是 - 为什么?

谢谢, 涟漪


PS:一些实现细节...

PriorityQueue<Entry> mEntries = new PriorityQueue<Entry>(1, Entry.EntryComparator());

与:

public static class EntryComparator implements Comparator<Entry> {
    public int compare(Entry my, Entry their) {
        if (my.mPriority < their.mPriority) {
            return 1;
        }
        else if (my.mPriority > their.mPriority) {
            return -1;
        }
        return 0;
    }
}

【问题讨论】:

  • 可能创建一个堆栈,然后在完成后将所需的元素添加到那里。

标签: java android priority-queue


【解决方案1】:

这段代码在 Java 6 的 PriorityQueue 实现中:

private class Itr implements Iterator<E> {
  /**
   * The modCount value that the iterator believes that the backing
   * Queue should have.  If this expectation is violated, the iterator
   * has detected concurrent modification.
   */
  private int expectedModCount = modCount;

  public E next() {
    if(expectedModCount != modCount) {
      throw new ConcurrentModificationException();
    }


  }

}

现在,为什么这里有这段代码?如果您查看Javadoc for ConcurrentModificationException,您会发现如果在迭代完成之前对底层集合进行了修改,则迭代器的行为是未定义的。因此,许多集合都实现了这种modCount 机制。

修复您的代码

您需要确保不要在循环中修改代码。如果您的代码是单线程的(看起来是),那么您只需按照同事的建议进行操作,然后将其复制到列表中以供以后包含。此外,还记录了 Iterator.remove() 方法的使用以防止 ConcurrentModificationExceptions。一个例子:

List<Entry> toAdd = new ArrayList<Entry>();
Iterator it = mEntries.iterator();
while(it.hasNext()) {
  Entry e = it.next();

  if(e.getId().equals(someId)) {
    Entry newEntry = e.setData(newData);
    it.remove();
    toAdd.add(newEntry);
  }
}
mEntries.addAll(toAdd);

【讨论】:

  • 很好的解释和解决方案:)
【解决方案2】:

PriorityQueue 的 Javadoc 明确指出:

“请注意,此实现不是同步的。如果任何线程在结构上修改列表,则多个线程不应同时访问 PriorityQueue 实例。相反,请使用线程安全的 PriorityBlockingQueue 类。”

这似乎是你的情况。

【讨论】:

    【解决方案3】:

    您的代码中的问题已经解释过了——实现迭代器,它可以通过交叉修改一致地迭代集合,这是一项相当艰巨的任务。您需要指定如何处理已删除的项目(将通过迭代器看到吗?),添加的项目,修改的项目......即使您可以始终如一地做到这一点,它也将是相当复杂和低效的实现——而且,大多数情况下,不是非常有用,因为用例“无需修改即可迭代”更为常见。因此,Java 架构师选择在迭代时拒绝修改,Java 集合 API 中的大多数集合都遵循这一点,如果检测到此类修改,则抛出 ConcurrentModificationException。

    至于你的代码——对我来说,你不应该让项目不可变。不变性是件好事,但不应过度使用。如果您在此处使用的 Entry 对象是某种领域对象,并且您真的希望它们是不可变的——您可以创建某种临时数据持有者 (MutableEntry) 对象,在您的算法中使用它,然后将数据复制到 Entry返回。从我的角度来看,这将是最好的解决方案。

    【讨论】:

      【解决方案4】:

      一个稍微好一点的实现是

      List<Entry> toAdd = new ArrayList<Entry>();
      for (Iterator<Entry> it= mEntries.iterator();it.hasNext();) {
          Entry e = it.next();
          if (e.getId().equals(someId)) {
              Entry newEntry = e.setData(newData);
              it.remove();
              toAdd.add(newEntry);
           }
      }
      mEntries.addAll(toAdd);
      

      这使用了迭代器的删除和之后的批量添加

      【讨论】:

        猜你喜欢
        • 2011-12-29
        • 2010-10-17
        • 1970-01-01
        • 1970-01-01
        • 2010-12-24
        • 2020-04-12
        • 1970-01-01
        相关资源
        最近更新 更多