首页 > 解决方案 > JavaFX ListChangeListener 根据 ObservableList 中已删除项目的位置不一致地处理“removeAll(Collection)”

问题描述

我遇到了ListChangeListener处理批量删除的异常情况(即removeAll(Collection)。如果 中的项目Collection连续的,则侦听器中指定的处理操作工作正常。但是,如果Collection连续,则侦听器中指定的操作一旦连续性被破坏就停止。

这可以通过示例来最好地解释。假设ObservableList由以下项目组成:

还假设有一个单独ObservableList的跟踪hashCode颜色的值,并且ListChangeListener已经添加了hashCode一个,当第一个列表中的一个或多个项目被删除时,它会从第二个列表中删除。如果“删除”Collection由“红色”、“橙色”和“黄色”组成,则侦听器中的代码会hashCodes按预期从第二个列表中删除所有三个项目。但是,如果“删除”Collection由“红色”、“橙色”和“绿色”组成,则侦听器中的代码在删除hashCode“橙色”后停止,并且永远不会达到应有的“绿色”。

下面列出了一个说明问题的简短应用程序。侦听器代码位于一个名为的方法中,该方法buildListChangeListener()返回一个添加到“颜色”列表中的侦听器。要运行该应用程序,了解以下内容会有所帮助:

这是应用程序的代码:

    package test;

import static java.util.Objects.isNull;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Pair;

public class RemoveAllItemsBug extends Application {

     private StackPane stackPane;
     private HBox hbox;

     private VBox vbox1;
     private Label label1;
     private ListView<Pair<String, Color>> colors;

     private VBox vbox2;
     private Label label2;
     private ListView<Integer> hashCodes;

     private VBox vbox3;
     private Label label3;
     private ComboBox<String> actionModes;
     private Button btnRemove;
     private Button btnRefresh;

     final static private String CONSECUTIVE = "consecutive", BROKEN = "broken";

     private final EventHandler<WindowEvent> onCloseRequestListener = evt -> {
          Platform.exit();
          System.exit(0);
     };

     @Override
     public void start(Stage primaryStage) throws Exception {
          primaryStage.setTitle("DUMMY  APP");

          // Necessary to ensure stage closes completely and javaw.exe stops running
          primaryStage.setOnCloseRequest(onCloseRequestListener);

          primaryStage.setWidth(550);
          primaryStage.setHeight(310);

          //          primaryStage.setMinWidth(550);
          //          primaryStage.setMinHeight(310);

          /*
           * Code block below for width/height property printouts is used to
           * test for an optimal size for the app. Once the size is determined
           * they may (and should be) commented out as here.
           */
          primaryStage
                      .widthProperty()
                      .addListener((width, oldWidth, newWidth) -> {
                           System.out.println("width: " + newWidth);
                      });

          primaryStage
                      .heightProperty()
                      .addListener((height, oldHeight, newHeight) -> {
                           System.out.println("height: " + newHeight);
                      });

          initializeUI();
          installSimpleBehavior();
          installListChangeListener();

          primaryStage.setScene(new Scene(stackPane));
          primaryStage.show();
     }

     private void installListChangeListener() {
          /*
           * The 'listChangeListenerUsingIf()' method returns a listener that
           * uses an 'if (c.next()) ...' statement to access the first change in
           * the Change variable (c). For purposes of accessing the first change
           * this is functionally equivalent to a 'while (c.next()) ...'
           * statement. However, because the Change variable may contain
           * multiple 'remove' changes where each change is represented by a
           * separate 'getRemoved()' list, the 'if (c.next())' statement will
           * catch only the first change while the 'while (c.next())' statement
           * (which is used in the 'listChangeListenerUsingWhile()' method)
           * catches them all.
           * 
           * The code below should be commented out as appropriate before
           * running the app in order to see the difference.
           * 
           * This case illustrates a serious flaw in the ListChangeListener API
           * documentation because it fails to indicate that the Change variable
           * may include multiple 'remove' changes and that each such change
           * must be accessed in a separate iteration (e.g. the 'while
           * (c.next()...').
           * 
           * In contrast, 'add' changes (i.e. changes resulting from the
           * addition of one or more items to the source list), the name of the
           * method that returns the change(s) is 'getAddSublist()'. This
           * clearly indicates that there may be more than one list of items
           * that have been added, or similarly that the total items that have
           * been 'added' by the change(s) represented by the Change variable
           * may be included in more than one list; thus the use of the term
           * 'sublist'.
           * 
           * The flaw is illustrated further in the cautionary note in the API
           * that reads as follows:
           * 
           * "[I]n case the change contains multiple changes of different type,
           * these changes must be in the following order: <em> permutation
           * change(s), add or remove changes, update changes </em> This is
           * because permutation changes cannot go after add/remove changes as
           * they would change the position of added elements. And on the other
           * hand, update changes must go after add/remove changes because they
           * refer with their indexes to the current state of the list, which
           * means with all add/remove changes applied."
           * 
           * This is certainly useful information. However, the problems
           * illustrated by the case at hand (i.e. different treatment based on
           * whether the changed items are continguous in the source list) are
           * just as significant as the situation addressed by the note, yet
           * they are not mentioned.
           * 
           * A better understanding as to how the process works can be gained by
           * running a system printout for the Change variable class
           * (System.out.println("Change variable class: " +
           * c.getClass().getSimpleName())) and compare the results yielded from
           * changing the choice in the 'Action modes' combo box from
           * 'consecutive' to 'broken'. For 'consecutive' (i.e. continguous),
           * the class for the Change variable is
           * ListChangeBuilder$SingleChange, for 'broken' (i.e. non-continguous)
           * the class is ListChangeBuilder$IterableChange. These classes aren't
           * well documented, which while regrettable is understandable inasmuch
           * as they're private inner classes for restricted API. Interestingly,
           * however, there is a public class MultipleAdditionAndRemovedChange
           * (also restricted API) that appears to fit this case perfectly and
           * is a bit more informative.
           */

          //          colors.getItems().addListener(listChangeListenerUsingIf());
          colors.getItems().addListener(listChangeListenerUsingWhile());
     }

     private void initializeUI() {

          //- Controls for colors
          label1 = new Label("Colors");
          colors = new ListView<Pair<String, Color>>();
          colors.setPrefSize(150, 200);
          colors.setItems(FXCollections.observableList(new ArrayList<>(colorsList())));
          vbox1 = new VBox(label1, colors);

          //- Controls for colors
          label2 = new Label("Hash codes");
          hashCodes = new ListView<Integer>();
          hashCodes.setPrefSize(150, 200);
          hashCodes.setItems(FXCollections.observableList(new ArrayList<>(
                                                                          colorsList().stream()
                                                                                      .map(e -> e.hashCode())
                                                                                      .collect(Collectors.toCollection(ArrayList::new)))));
          vbox2 = new VBox(label2, hashCodes);

          //- 'Action mode' controls
          label3 = new Label("Action mode");
          actionModes = new ComboBox<>(
                                       FXCollections.observableList(List.of(CONSECUTIVE, BROKEN)));
          actionModes.setPrefWidth(150);
          actionModes.getSelectionModel().select(0);
          btnRemove = new Button("Remove");
          btnRefresh = new Button("Refresh");
          List.of(btnRemove, btnRefresh).forEach(b -> {
               b.setMaxWidth(Double.MAX_VALUE);
               VBox.setMargin(b, new Insets(5, 0, 0, 0));
          });
          vbox3 = new VBox(label3, actionModes, btnRemove, btnRefresh);

          hbox = new HBox(vbox1, vbox2, vbox3);
          hbox.setPadding(new Insets(10));
          hbox.setSpacing(15);
          hbox.setBackground(new Background(
                                            new BackgroundFill(Color.DARKGRAY, CornerRadii.EMPTY, Insets.EMPTY),
                                            new BackgroundFill(Color.WHITESMOKE, CornerRadii.EMPTY, new Insets(1))));

          stackPane = new StackPane(hbox);
          stackPane.setPadding(new Insets(15));
     }

     private void installSimpleBehavior() {

          //- 'Colors' cell factory
          colors.setCellFactory(listView -> {
               return new ListCell<Pair<String, Color>>() {

                    @Override
                    protected void updateItem(Pair<String, Color> item, boolean empty) {
                         super.updateItem(item, empty);
                         if (isNull(item) || empty) {
                              setGraphic(null);
                              setText(null);
                         }
                         else {
                              HBox graphic = new HBox();
                              graphic.setPrefSize(15, 15);
                              graphic.setBackground(new Background(new BackgroundFill(
                                                                                      item.getValue(),
                                                                                      CornerRadii.EMPTY,
                                                                                      Insets.EMPTY)));
                              setGraphic(graphic);
                              setText(item.getKey());
                              setContentDisplay(ContentDisplay.LEFT);
                         }
                    }
               };
          });

          //- 'Colors' cell factory
          hashCodes.setCellFactory(listView -> {
               return new ListCell<Integer>() {

                    @Override
                    protected void updateItem(Integer item, boolean empty) {
                         super.updateItem(item, empty);
                         if (isNull(item) || empty) {
                              setGraphic(null);
                              setText(null);
                         }
                         else {
                              HBox graphic = new HBox();
                              graphic.setPrefSize(15, 15);
                              graphic.setBackground(new Background(new BackgroundFill(
                                                                                      colorForHashCode(item),
                                                                                      CornerRadii.EMPTY,
                                                                                      Insets.EMPTY)));
                              Canvas c = new Canvas(15, 15);
                              GraphicsContext graphics = c.getGraphicsContext2D();
                              graphics.setFill(colorForHashCode(item));
                              graphics.fillRect(0, 0, c.getWidth(), c.getHeight());
                              setGraphic(c);
                              setText("" + item);
                              setContentDisplay(ContentDisplay.LEFT);
                         }
                    }

                    private Color colorForHashCode(int hash) {
                         return colorsList().stream()
                                            .filter(e -> e.hashCode() == hash)
                                            .map(e -> e.getValue())
                                            .findFirst()
                                            .orElseThrow();
                    }
               };
          });

          //- 'Remove' button action
          btnRemove.setOnAction(e -> {
               String actionMode = actionModes.getValue();
               if (CONSECUTIVE.equals(actionMode)) {
                    colors.getItems().removeAll(consecutiveColors());
               }
               else if (BROKEN.equals(actionMode)) {
                    colors.getItems().removeAll(brokenColors());
               }
          });

          //- 'Refresh' button action
          btnRefresh.setOnAction(e -> {
               colors.getItems().setAll(colorsList());
               hashCodes.getItems().setAll(colorsList()
                                                       .stream()
                                                       .map(ee -> ee.hashCode())
                                                       .collect(Collectors.toCollection(ArrayList::new)));
          });
     }

     private ListChangeListener<Pair<String, Color>> listChangeListenerUsingIf() {
          return c -> {
               if (c.next()) {
                    System.out.println("Change variable class: " + c.getClass().getName());
                    if (c.wasRemoved()) {
                         System.out.println("Removing " + c.getRemovedSize() + " items");
                         c.getRemoved().forEach(e -> {
                              Integer hash = Integer.valueOf(e.hashCode());
                              hashCodes.getItems().remove(hash);
                         });
                         System.out.println("number of 'hash codes' after removal: " + hashCodes.getItems().size());
                         System.out.println();
                    }
                    if (c.wasAdded()) {
                         c.getAddedSubList().forEach(e -> {
                              if (hashCodes.getItems().stream().noneMatch(ee -> ee == e.hashCode()))
                                   hashCodes.getItems().add(e.hashCode());
                         });
                    }
               }
          };
     }

     private ListChangeListener<Pair<String, Color>> listChangeListenerUsingWhile() {
          return c -> {
               while (c.next()) {
                    System.out.println("Change variable class: " + c.getClass().getName());
                    if (c.wasRemoved()) {
                         System.out.println("Removing " + c.getRemovedSize() + " items");
                         c.getRemoved().forEach(e -> {
                              Integer hash = Integer.valueOf(e.hashCode());
                              hashCodes.getItems().remove(hash);
                         });
                         System.out.println("number of 'hash codes' after removal: " + hashCodes.getItems().size());
                         System.out.println();
                    }
                    if (c.wasAdded()) {
                         c.getAddedSubList().forEach(e -> {
                              if (hashCodes.getItems().stream().noneMatch(ee -> ee == e.hashCode()))
                                   hashCodes.getItems().add(e.hashCode());
                         });
                    }
               }
          };
     }

     private List<Pair<String, Color>> colorsList() {
          return List.of(
                         new Pair<>("rot", Color.RED),
                         new Pair<>("orange", Color.ORANGE),
                         new Pair<>("gelb", Color.YELLOW),
                         new Pair<>("grün", Color.GREEN),
                         new Pair<>("blau", Color.BLUE),
                         new Pair<>("violett", Color.PURPLE),
                         new Pair<>("grau", Color.GRAY),
                         new Pair<>("schwarz", Color.BLACK));
     }

     private List<Pair<String, Color>> consecutiveColors() {
          return List.of(
                         new Pair<>("gelb", Color.YELLOW),
                         new Pair<>("grün", Color.GREEN),
                         new Pair<>("blau", Color.BLUE));
     }

     private List<Pair<String, Color>> brokenColors() {
          return List.of(
                         new Pair<>("rot", Color.RED),
                         new Pair<>("grün", Color.GREEN),
                         new Pair<>("blau", Color.BLUE));
     }

     public static void main(String[] args) {
          launch(args);
     }
}

提前感谢您的任何反馈。

[根据@Slaw 的第一条评论进行编辑]

这个案例提出了几个问题。@Slaw 的第一条评论让我以不同的方式看待这个问题。@Slaw 正确地指出使用while (c.next()) ...子句可以解决使用子句时引起的问题if (c.next())...

然而,如果从整体上来看,还有一个更根本的问题,与其说是使用该if (c.next())从句,不如说是掩盖了该错误并使其难以发现。这个问题是ListChangeListener该类的糟糕文档。

我已经修改了示例应用程序的代码以包含第二个正常工作的侦听器方法(将名称更改为生成错误的那个),以及关于为什么它是必要的以及如何ListChangeListener以及更特别是它的Change伴侣的评论,似乎工作。该评论的相关部分重复如下:

listChangeListenerUsingIf()方法返回一个侦听器,该侦听器使用if (c.next()) ...语句访问Change变量 (c) 中的第一个更改。出于访问第一个更改的目的,这在功能上等同于while (c.next()) ...语句。但是,由于Change变量可能包含多个“删除”更改,其中每个更改都由一个单独的getRemoved()列表表示,因此该if (c.next())语句将仅捕获第一个更改,而该while (c.next())语句(在listChangeListenerUsingWhile()方法中使用)将所有更改都捕获。

这个案例说明了ListChangeListenerAPI 文档中的一个严重缺陷,因为它没有表明Change变量可能包含多个“删除”更改,并且每个此类更改都必须在单独的迭代中访问(例如while (c.next()...)。

相反,对于“添加”更改(即,由于向源列表中添加一个或多个项目而导致的更改),返回更改的方法的名称是getAddedSublist()。这清楚地表明可能存在不止一个已添加的项目列表,或者类似地,由Change变量表示的更改“添加”的总项目可能包含在多个列表中;因此使用了这个词sublist

API 中的警告说明进一步说明了该缺陷,内容如下:

“[I]如果更改包含不同类型的多个更改,这些更改必须按以下顺序排列:排列更改、添加或删除更改、更新更改这是因为排列更改不能在添加/删除更改之后进行“

这当然是有用的信息。然而,手头案例所说明的问题(即基于更改的项目在源列表中是否连续的不同处理)与注释所解决的情况一样重要。但他们没有被提及。

通过运行Change变量类System.out.println("Change variable class: " + c.getClass().getSimpleName())(对于“连续”(即连续Change变量的类是ListChangeBuilder$SingleChange,对于“破碎”(即连续),类是ListChangeBuilder$IterableChange。这些类没有很好的文档记录,虽然令人遗憾的是可以理解,因为它们是受限 API 的私有内部类。然而,有趣的是,有一个公共类MultipleAdditionAndRemovedChange(也是受限 API)似乎非常适合这种情况,并且信息量更大。

我希望这会有所帮助,并感谢@Slaw 的有用输入。

标签: javafxobservablelistchangelistener

解决方案


从以下文档ListChangeListener.Change

表示对ObservableList. 更改可能包含一个或多个实际更改,并且必须通过调用next()方法进行迭代 [强调添加]

在您的实施中,ListChangeListener您有:

if (c.next()) {
  // handle change...
}

这只会处理一次更改。如果有多个,您需要循环(即迭代)更改:

while (c.next()) {
 // handle change...
}

只需在您的示例中将其更改if为 a 即可while解决您描述的问题。

下面是一个示例,展示了批量删除非连续元素如何导致多个更改合并到一个ListChangeListener.Change对象中:

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;

public class Main {

  public static void main(String[] args) {
    var list = FXCollections.observableArrayList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    list.addListener(
        (ListChangeListener<Integer>)
            c -> {
              System.out.println("----------BEGIN_CHANGE----------");
              while (c.next()) {
                // for example, assume c.wasRemoved() returns true
                System.out.printf(
                    "Removed %d element(s): %s%n", c.getRemovedSize(), c.getRemoved());
              }
              System.out.println("-----------END_CHANGE-----------");
            });
    list.removeAll(1, 7, 3, 8, 2, 10);
  }
}

和输出:

----------BEGIN_CHANGE----------
Removed 3 element(s): [1, 2, 3]
Removed 2 element(s): [7, 8]
Removed 1 element(s): [10]
-----------END_CHANGE-----------

如果您熟悉 JDBC,您会注意到迭代 a 的 APIListChangeListener.Change类似于迭代 a ResultSet


推荐阅读