洗牌 ObservableList 触发不正确的更改通知

Shuffling ObservableList fires incorrect change notification

方法 FXCollections.shuffle() 仅触发 wasRemoved 更改通知。我们可能知道,洗牌不仅仅是删除,而是删除和添加。

the documentation中我们可以看到:

Shuffles all elements in the observable list. Fires only one change notification on the list.

如果我没记错的话,一个更改可以同时包含 wasAddedwasRemoved。真遗憾 wasPermutated 没有被默认的 ObservableList FX api 触发(或者是吗?)。

测试代码:

public class SimpleMain {

    public static void main(String[] args) {
        ObservableList<Integer> integers = FXCollections.observableArrayList(1, 2, 3, 4);
        integers.addListener(initListener());
        FXCollections.shuffle(integers);
    }

    private static ListChangeListener<Integer> initListener() {
        return change -> {
            while (change.next()) {
                if (change.wasPermutated()) {
                    System.out.println("wasPermutated");
                } else if (change.wasRemoved()) {
                    System.out.println("wasRemoved");
                } else if (change.wasAdded()) {
                    System.out.println("wasAdded");
                }
            }
        };
    }

}

问题

您的代码假定 wasAdded()wasRemoved() 是互斥的,无论您是否有意如此。这个假设是错误的。如果一个或多个连续元素被 替换 那么这两种方法都将 return true.

请记住,Change 对象和“更改”是有区别的。单个 Change 实例可以进行多个更改。当文档说:

Fires only one change notification on the list.

并不是说只有一个 Change 对象会被发送到 ListChangeListener。它的意思是 Change 对象只会进行一次更改。换句话说,Change#next() 方法只会在第一次调用时 return true,因此你的 while 循环只会循环一次。


解决方案

您需要重写代码,因为 wasAdded()wasRemoved() 都可以是 true。例如,这是一个检查所有类型更改的侦听器:

private static ListChangeListener<Integer> initListener() {
  return change -> {
    while (change.next()) {
      if (change.wasPermutated()) {
        System.out.println("wasPermutated");
      } else if (change.wasUpdated()) {
        System.out.println("wasUpdated");
      } else if (change.wasReplaced()) {
        System.out.println("wasReplaced");
      } else if (change.wasRemoved()) {
        System.out.println("wasRemoved");
      } else { // only other change type is "added"
        System.out.println("wasAdded");
      }
    }
  };
}

上面使用wasReplaced(),与wasAdded() && wasRemoved()相同。请注意,如果您使用 if-else-if 结构,则必须在 wasRemoved()wasAdded() 之前检查 wasReplaced()。否则上面的代码将遇到与您的代码相同的问题。

如果您将上面的代码插入到您的代码中并 运行 您将看到以下输出:

wasReplaced

The documentation of ListChangeListener.Change 对如何实现 ListChangeListener.

给出了更一般的解释(和示例)

请注意,文档中的示例并未专门检查 wasReplaced()。相反,它处理在最终 else 块中删除的 添加的元素(在 wasPermutated()wasUpdated() return false).这是可能的,因为如果没有删除或添加元素,getRemoved()getAddedSubList() 将分别 return 空列表。如果您先处理任何已删除的元素,然后再处理任何添加的元素,则通常会产生与专门处理替换元素相同的效果。但是,根据您的用例,专门处理替换可能会有所帮助。


为什么不排列?

他们实施 shuffle 方法的方式是:

public static void shuffle(ObservableList list, Random rnd) {
    Object newContent[] = list.toArray();

    for (int i = list.size(); i > 1; i--) {
        swap(newContent, i - 1, rnd.nextInt(i));
    }

    list.setAll(newContent); 
}

来源:javafx.collections.FXCollections,JavaFX 15.

如你所见,元素被提取到一个数组中,数组被打乱,然后列表中的元素被替换 与数组。这导致单个“替换更改”被触发。

我在 while 循环中向您的 ListChangeListener 添加了一行:

            System.out.println("change: "+change);

我得到的输出是:

change: { [1, 2, 3, 4] replaced by [4, 2, 3, 1] at 0 }
wasRemoved

这暗示了你的错误。字符串表示中使用的动词是“替换”。 去掉监听器中的“其他”并添加对 wasReplaced() 的检查,您会发现这正是您得到的结果。