JavaFX ListChangeListener 根据已删除项目在 ObservableList 中的位置不一致地处理 'removeAll(Collection)'
JavaFX ListChangeListener handles 'removeAll(Collection)' inconsistently based on position of removed items in ObservableList
我遇到了 ListChangeListener
如何处理批量删除的异常情况(即 removeAll(Collection)
。如果 Collection
中的项目 是连续的 然后监听器中指定的处理操作工作正常。但是,如果 Collection
是 不 连续然后监听器中指定的操作停止一次 连续性被打破。
这最好通过示例来解释。假设 ObservableList
包含以下项目:
- "red"
- "orange"
- "yellow"
- "green"
- "blue"
还假设有一个单独的 ObservableList
跟踪颜色的 hashCode
值,并且添加了一个 ListChangeListener
从中删除了 hashCode
每当第一个列表中的一个或多个项目被删除时,第二个列表。如果 'removal' Collection
由 "red"、"orange" 和 "yellow" 组成,则侦听器中的代码将从符合预期的第二个列表。但是,如果 'removal' Collection
由 "red"、"orange" 和“green”组成,则侦听器中的代码在之后停止删除 "orange" 的 hashCode
并且永远不会达到应有的 "green"。
下面列出了一个说明问题的简短应用程序。侦听器代码位于名为 buildListChangeListener()
的方法中,该方法 returns 添加到 'Colors' 列表的侦听器。对于 运行 应用程序,它有助于了解:
ComboBox
中的 - 'consecutive' 指定三种颜色 连续 如上所述;单击 'Remove' 按钮将使他们从 'Colors' 列表中删除,并且他们的
hashCodes
来自另一个列表。
- 'broken'指定三种颜色不连续,这样
单击 'Remove' 按钮只会删除其中一种颜色
- 单击 'Refresh' 将两个列表恢复到其原始状态
这是应用程序的代码:
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
class.
的糟糕文档
我已经修改了示例应用程序的代码以包含第二个正常工作的侦听器方法(名称更改为生成错误的方法),以及关于为什么需要它以及如何ListChangeListener
,尤其是它的 Change
伴侣,似乎有效。该评论的相关部分重复如下:
listChangeListenerUsingIf()
方法 returns 使用 if (c.next()) ...
语句访问 Change
变量 (c) 中的第一个更改的侦听器。为了访问第一个更改,这在功能上等同于 while (c.next()) ...
语句。但是,由于 Change
变量可能包含多个 'remove' 更改,其中每个更改都由单独的 getRemoved()
列表表示,因此 if (c.next())
语句将仅捕获第一个更改,而 while (c.next())
语句(在 listChangeListenerUsingWhile()
方法中使用)将它们全部捕获。
这种情况说明了 ListChangeListener
API 文档中的一个严重缺陷,因为它未能表明 Change
变量可能包含多个 'remove' 更改,并且每个这样的更改必须在单独的迭代中访问更改(例如 while (c.next()...
)。
相比之下,对于 'add' 更改(即由于将一个或多个项目添加到源列表而导致的更改)returns 更改的方法的名称是 getAddedSublist()
。这清楚地表明可能有多个已添加的项目列表,或者类似地,可能包含 'added' 由 Change
变量表示的更改的总项目在多个列表中;因此使用术语 sublist
.
API 中的警告说明进一步说明了该缺陷,内容如下:
"[I]如果更改包含多个不同类型的更改,则这些更改必须按以下顺序排列: 排列更改、添加或删除更改、更新更改 这是因为排列更改不能在 add/remove 更改之后进行,因为它们会更改添加元素的位置。另一方面,更新更改必须在 add/remove 更改之后进行,因为它们使用索引引用列表的当前状态,这意味着应用了所有 add/remove 更改。"
这当然是有用的信息。然而,手头的案例所说明的问题(即根据更改的项目是否在源列表中是连续的进行不同的处理)与注释所解决的情况一样重要;但他们没有被提及。
通过 运行 打印 Change
变量 class (System.out.println("Change variable class: " + c.getClass().getSimpleName())
) 的系统打印输出并比较将 'Action modes' 组合框中的选项从 'consecutive' 更改为 'broken' 产生的结果。对于 'consecutive'(即 continguous),Change
变量的 class 是 ListChangeBuilder$SingleChange
,对于 'broken'(即非连续) class 是 ListChangeBuilder$IterableChange
。这些 classes 没有很好的记录,虽然令人遗憾但这是可以理解的,因为它们是受限 API 的私有内部 classes。然而,有趣的是,有一个 public class MultipleAdditionAndRemovedChange
(也受限制 API)似乎非常适合这种情况并且提供更多信息。
希望对您有所帮助,感谢@Slaw 提供的有用信息。
来自 ListChangeListener.Change
的文档:
Represents a report of changes done to an ObservableList
. The change may consist of one or more actual changes and must be iterated by calling the next()
method [emphasis added].
在 ListChangeListener
的实施中,您有:
if (c.next()) {
// handle change...
}
这只会处理一个更改。您需要 循环 遍历(即迭代)更改,以防有多个更改:
while (c.next()) {
// handle change...
}
只需将示例中的 if
更改为 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,您会注意到迭代 ListChangeListener.Change
的 API 类似于迭代 ResultSet
.
我遇到了 ListChangeListener
如何处理批量删除的异常情况(即 removeAll(Collection)
。如果 Collection
中的项目 是连续的 然后监听器中指定的处理操作工作正常。但是,如果 Collection
是 不 连续然后监听器中指定的操作停止一次 连续性被打破。
这最好通过示例来解释。假设 ObservableList
包含以下项目:
- "red"
- "orange"
- "yellow"
- "green"
- "blue"
还假设有一个单独的 ObservableList
跟踪颜色的 hashCode
值,并且添加了一个 ListChangeListener
从中删除了 hashCode
每当第一个列表中的一个或多个项目被删除时,第二个列表。如果 'removal' Collection
由 "red"、"orange" 和 "yellow" 组成,则侦听器中的代码将从符合预期的第二个列表。但是,如果 'removal' Collection
由 "red"、"orange" 和“green”组成,则侦听器中的代码在之后停止删除 "orange" 的 hashCode
并且永远不会达到应有的 "green"。
下面列出了一个说明问题的简短应用程序。侦听器代码位于名为 buildListChangeListener()
的方法中,该方法 returns 添加到 'Colors' 列表的侦听器。对于 运行 应用程序,它有助于了解:
- 'consecutive' 指定三种颜色 连续 如上所述;单击 'Remove' 按钮将使他们从 'Colors' 列表中删除,并且他们的
hashCodes
来自另一个列表。 - 'broken'指定三种颜色不连续,这样 单击 'Remove' 按钮只会删除其中一种颜色
- 单击 'Refresh' 将两个列表恢复到其原始状态
ComboBox
中的 这是应用程序的代码:
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
class.
我已经修改了示例应用程序的代码以包含第二个正常工作的侦听器方法(名称更改为生成错误的方法),以及关于为什么需要它以及如何ListChangeListener
,尤其是它的 Change
伴侣,似乎有效。该评论的相关部分重复如下:
listChangeListenerUsingIf()
方法 returns 使用 if (c.next()) ...
语句访问 Change
变量 (c) 中的第一个更改的侦听器。为了访问第一个更改,这在功能上等同于 while (c.next()) ...
语句。但是,由于 Change
变量可能包含多个 'remove' 更改,其中每个更改都由单独的 getRemoved()
列表表示,因此 if (c.next())
语句将仅捕获第一个更改,而 while (c.next())
语句(在 listChangeListenerUsingWhile()
方法中使用)将它们全部捕获。
这种情况说明了 ListChangeListener
API 文档中的一个严重缺陷,因为它未能表明 Change
变量可能包含多个 'remove' 更改,并且每个这样的更改必须在单独的迭代中访问更改(例如 while (c.next()...
)。
相比之下,对于 'add' 更改(即由于将一个或多个项目添加到源列表而导致的更改)returns 更改的方法的名称是 getAddedSublist()
。这清楚地表明可能有多个已添加的项目列表,或者类似地,可能包含 'added' 由 Change
变量表示的更改的总项目在多个列表中;因此使用术语 sublist
.
API 中的警告说明进一步说明了该缺陷,内容如下:
"[I]如果更改包含多个不同类型的更改,则这些更改必须按以下顺序排列: 排列更改、添加或删除更改、更新更改 这是因为排列更改不能在 add/remove 更改之后进行,因为它们会更改添加元素的位置。另一方面,更新更改必须在 add/remove 更改之后进行,因为它们使用索引引用列表的当前状态,这意味着应用了所有 add/remove 更改。"
这当然是有用的信息。然而,手头的案例所说明的问题(即根据更改的项目是否在源列表中是连续的进行不同的处理)与注释所解决的情况一样重要;但他们没有被提及。
通过 运行 打印 Change
变量 class (System.out.println("Change variable class: " + c.getClass().getSimpleName())
) 的系统打印输出并比较将 'Action modes' 组合框中的选项从 'consecutive' 更改为 'broken' 产生的结果。对于 'consecutive'(即 continguous),Change
变量的 class 是 ListChangeBuilder$SingleChange
,对于 'broken'(即非连续) class 是 ListChangeBuilder$IterableChange
。这些 classes 没有很好的记录,虽然令人遗憾但这是可以理解的,因为它们是受限 API 的私有内部 classes。然而,有趣的是,有一个 public class MultipleAdditionAndRemovedChange
(也受限制 API)似乎非常适合这种情况并且提供更多信息。
希望对您有所帮助,感谢@Slaw 提供的有用信息。
来自 ListChangeListener.Change
的文档:
Represents a report of changes done to an
ObservableList
. The change may consist of one or more actual changes and must be iterated by calling thenext()
method [emphasis added].
在 ListChangeListener
的实施中,您有:
if (c.next()) {
// handle change...
}
这只会处理一个更改。您需要 循环 遍历(即迭代)更改,以防有多个更改:
while (c.next()) {
// handle change...
}
只需将示例中的 if
更改为 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,您会注意到迭代 ListChangeListener.Change
的 API 类似于迭代 ResultSet
.