【问题标题】:Unexpected IndexOutOfBoundsException意外的 IndexOutOfBoundsException
【发布时间】:2014-10-18 04:58:22
【问题描述】:

最近我完全被我的代码中的一个奇怪的错误弄糊涂了,到了极度沮丧的地步。最后,我在我的代码中输入了System.out.println();,直到我缩小范围,以揭示最奇怪的结果。代码如下:

for (Iterator<Integer> it = list.iterator(); it.hasNext();) {
  i = it.next();
  System.out.println(DeathWish.getInstanceListReference()
                     .get(DeathWish.getInstanceListReference().size() -1) );
  System.out.println("Golden number: " + 
                     (DeathWish.getInstanceListReference().size() -1) );
  System.out.println((DeathWish.getInstanceListReference().size() -1) == i);
  System.out.println("CurrentInstance List: " + 
                     Arrays.toString(DeathWish.getInstanceListReference().toArray()));
  System.out.println("Iteration: " + i);
  try {
    System.out.println( DeathWish.getInstanceListReference()
      .remove((int)list.get(i))); //remove unwanted objects from game paint list
  } catch(IndexOutOfBoundsException ex) {
    System.err.println(ex + " Error with multiple GameObj objects");
  } finally {
    System.out.println("\n");
  }
}

和输出:

DeathWish.Bullet@ddd5de
Golden number: 2
true
CurrentInstance List: [DeathWish.Player@3a5cf7, DeathWish.Bullet@6cca54, DeathWish.Bullet@ddd5de]
Iteration: 2
java.lang.IndexOutOfBoundsException: Index: 2, Size: 2 Error with multiple GameObj objects

getInstanceListReference() 返回一个ArrayList&lt;GameObj&gt;GameObj 是我项目中代表游戏对象的超超类之一。此外,getInstanceListReference() 返回的ArrayList 在运行时不会被内部源代码修改。

在for循环之前执行了一些代码:

System.out.println("\n------------------------\n" + "Read Instance List: " + Arrays.toString(tmpLock.toArray()) + "|" + Arrays.toString(tmpLock1.toArray()));
System.out.println("Recycle Bin: " + recycleBin.get(0) + "|" + recycleBin.get(1) + "\n------------------------\n");

源代码中最顶部的这个列表指的是recycleBin。它保存计划删除的对象的索引。带有IndexOutOfBoundsException 的for 循环循环遍历recycleBin 中的整数,并利用它们删除实例列表中的索引。

我是否在做一些我不知道导致问题的不良做法?

【问题讨论】:

  • 发帖SSCCE
  • 您确定您没有从其他线程访问该列表吗?尝试围绕对 listReference 的所有访问添加 synchronized(),它是在其中写入和读取的。这有强烈的线程问题的味道。
  • 是的,我确定我没有从另一个线程访问。为了更好地衡量,我将同步修饰符添加到我的 get 方法中。我无法将同步修饰符添加到 ArrayList 引用本身,因为我正在使用封装(对于新手:引用有一个私有修饰符和一个 get 方法来将值传递给其他类)。
  • 你们肯定还没有“发现 Java 的错误”,这不应该是您跳到的第一个解释。您在 您的代码 中发现错误的可能性。如果您按照建议发布 SSCCE,我们可以帮助您调试和理解它。
  • 你没有抓住重点——你的代码的其余部分(大概)不是问题的原因。尝试通过删除理论上不相关的代码来复制问题。例如,就像 sscce.org 建议的那样,如果这不是问题的一部分,请删除任何图形界面。在这里,我认为你可以去掉匿名类,然后迭代,比如说,一个整数列表。如果您的代码如此复杂且相互关联,以至于无法提取较小的组件,那么这是您的错误做法;将您的代码构造成可以单独测试的独立部分。

标签: java debugging runtime-error iteration indexoutofboundsexception


【解决方案1】:

如果没有我们可以自己运行的 SSCCE,我们无法确定地给您答案。但是,根据您发布的描述和 cmets,我想到了一种可能的解释:


您正在以无法触发ConcurrentModificationException 的方式同时修改您正在迭代的列表。尽管有这个名字,但即使在单个线程中也完全有可能遇到这种问题。这是一个简单的例子(SSCCE,如果你愿意的话):

int len = ls.size();
for(int i = 0; i < len; i++) {
  if(i %2 == 0) {
    ls.remove(i);
  }
}

当它在迭代列表时修改列表,i 与列表的元素断开连接。这不是删除偶数元素(可能是预期的行为),而是删除第 0 个、第 3 个、第 6 个等,直到它 IndexOutOfBoundExceptions 在循环的中途。

通常,避免此问题的正确方法是遍历一个集合并修改副本,或者在删除操作期间使用Iterator。例如,这是删除所有其他元素的安全方法 - 请注意,我们不使用索引,而是仅使用迭代器,它在内部保持其在列表中的位置,而不管删除了什么:

Iterator<?> iter = ls.iterator();
while(iter.hasNext()) {
  iter.next();
  iter.remove();
  if(iter.hasNext()) {
    iter.next();
  }
}

好的,让我们尝试另一个示例,更直接地基于循环中的代码。

我想从另一个列表中删除一个索引列表:

List<Integer> toRemove = Arrays.asList(0,2,4,6);
List<String> myList = new ArrayList<>(
                        Arrays.asList("A","B","C","D","E","F","G","H"));

现在,如果我尝试从 myList 中删除 idexes 0、2、4 和 6,我会得到一个 IndexOutOfBoundsException,因为当我调用 .remove() 时,列表会缩小一个,并且后面的所有元素列表下移一位。

for(int i : toRemove) {
  myList.remove(i);
}

虽然我不能确切地说什么适合您的用例,但一种选择是保持原始列表不变,修改一个副本,然后在完成所有更改后存储该修改后的副本。这是一种方法:

Set<Integer> toRemoveSet = new HashSet<>(toRemove);
List<String> cleanList = new ArrayList<>();
for(int i = 0; i < myList.size(); i++) {
  if(!toRemoveSet.contains(i)) {
    cleanList.add(myList.get(i));
  }
}
System.out.println(cleanList);

我们不是一个一个地删除项目,而是检查每个索引是否应该保留,并跳过任何不应该的。这避免了尝试从列表中删除项目的不一致状态,当您处理它时,该列表正在缩小。


一般来说,随着项目规模的扩大,可变集合的使用可能会很棘手。优秀的 Guava 库提供了一组 immutable collections 专门帮助人们避免此类问题。它需要一种稍微不同的思维方式来解决问题,但它是一种更强大的方式。即使您不使用 Guava,也请尽可能考虑您的集合是不可变的,并在尝试进行结构修改时制作防御性副本。

【讨论】:

  • 你的例子也与我的“不一致”不同,因为我有这个列表。 recycleBin 首先加载最大的索引,以便在删除过程发生时不会影响其后面的数据。我在示例中引用的 instanceList 是 DeathWish.getInstanceListReference(),它返回 GameObj 的 ArrayList
  • 我要试试你的 iter.remove() 技术
  • 请注意,您不能在 iter.remove() 中使用索引 - 您不应该在迭代器循环内调用 get(i) 或任何类似内容。
  • 如果我是正确的,不可变不是指随着时间的推移不变或无法改变吗?您是说参考中的不可变集合不能更改(最终)还是列表中的项目本身不能删除、添加或重新排列?
  • 一个不可变的集合不能被重组或调整大小——Arrays.asList() 返回的List 就是这样一个集合的一个例子。使用明确不可变的集合有很多好处,但也可以简单地将集合视为不可变的,即使它们可能不是。
【解决方案2】:

我会重写accept() 以使用Java 库方法,您可以使用这些方法来处理集合。

  • 您的方法 accept() 传递了一个 Collection&lt;Integer&gt;,它表示您要从 DeathWith.getInstanceListReference() 中删除的索引。
  • 您当前正在处理在对其进行迭代时修改集合。

考虑使用Collections.removeAll() 来完成(我相信)你需要的东西。

public void accept(Set<Integer> deadIndices) {
    for (int i : deadIndices) {
        DeathWish.getInstanceListReference().set(i, null);
    }
    List<GameObj> nullList = new ArrayList<GameObj>();
    nullList.add(null);
    DeathWish.getInstanceListReference().removeAll(nullList);
}

假设:

  • nullDeathWish.getInstanceListReference() 中的无效值。

【讨论】:

  • 稍微安全一些,但使用null 或任何标记值同样有可能发生故障——如果列表中已经包含null,你会遇到问题。归根结底,使用.removeAll() 作为删除特定索引的代理就像使用螺丝刀作为锤子一样——它会很好地工作,直到你注意到它为止。
  • 我之前想过这个方法:将完成的游戏对象清空,然后检查它们是否为空,但我从未尝试过。另外,如果已经有空值,为什么会遇到问题? removeAll 不会简单地删除它们吗?
  • @KiroYakuza 如果您的程序从不将null 添加到DeathWish.getInstanceListReference(),那么这是好的(但不是理想的解决方案)
  • @dimo414,是的 - 但是螺丝刀设计精良,是一种优质工具,而锤子是您以前从未尝试过的品牌 - 一定要注意这些问题
【解决方案3】:

IndexOutOfBoundsException 来自您的 list.get(i),我假设它只有 2 个元素,而不是来自 DeathWish.getInstanceListReference() 列表!

【讨论】:

  • 哦,抱歉忘了提这个列表实际上是我的“recycleBin”。它检查instanceList ArrayList 中的对象,并添加满足一定条件的对象的索引。这里有更多输出:读取实例列表:[DeathWish.Player@a8409d, DeathWish.Bullet@1fcd8fd, DeathWish.Bullet@24f455] 回收站:[2, 1]|[]。 *是的,它是用我的同步编辑重新编译的,但我仍然得到相同的其他输出,包括错误。
  • 试图从回收站中获取第三个对象会导致问题。第 291 行的 list.get(i) 失败了,因为其中只有两个项目 (2, 1) 但您正在尝试阅读第三个项目。这不是 java 的错误,这是您的代码的错误。
  • 回收站中的instanceList索引只有两个整数:[2,1]。没有第三个对象。 instanceList 包含 3 个对象:[player@...,bullet@...,bullet@...]。 recycleBin 坚持认为子弹在它们各自的索引 2 和 1 处需要被删除。播放器引用永远无法删除播放器实例。
  • 这正是问题所在!您从 [2,1] 中取出 2 并尝试获取 list.get(2) 并且那里只有 2 个元素 [2,1]
  • 那么 it.next() 返回第一个实际元素之前的元素?但是如果它的for循环初始化器是列表的size(),那不就意味着它正在初始化size() + 1吗?接下来是获取下一个值,本质上是list.size()?
猜你喜欢
  • 2013-08-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-21
  • 2015-01-12
  • 2021-08-01
  • 2023-03-27
相关资源
最近更新 更多