为什么 TableView 的更改侦听器会为 ObjectProperty<T> 与 JavaFX8 中的 TProperty 列提供不同的结果?

Why does a TableView's change listener give different results for ObjectProperty<T> vs TProperty columns in JavaFX8?

亲戚Java菜鸟问题

我有一个带提取器的 TableView 和一个添加到基础 ObservableList 的 ListChangeListener

如果我在数据模型中有一个 StringProperty 列,如果我双击该单元格然后在不进行任何更改的情况下按 ENTER 键,则更改侦听器不会检测到更改。不错。

但是,如果我将列定义为 ObjectProperty<String> 并双击然后按 ENTER,更改侦听器始终会检测到更改,即使 none 已经完成.

为什么会这样?从更改侦听器的角度来看,ObjectProperty<String>StringProperty 之间有什么区别?

我已阅读 and 并认为我理解其中的差异。但我不明白为什么更改侦听器会针对 TProperty/SimpleTPropertyObjectProperty<T> 给出不同的结果。

如果有帮助,这里是我有点荒谬的案例的 MVCE。我实际上正在尝试让一个更改侦听器为 BigDecimalLocalDate 列工作,并且已经坚持了 5 天。如果我能理解更改侦听器给出不同结果的原因,我也许能够让我的代码正常工作。

我正在使用 JavaFX8 (JDK1.8.0_181)、NetBeans 8.2 和 Scene Builder 8.3。

package test17;

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.converter.DefaultStringConverter;

public class Test17 extends Application {

    private Parent createContent() {

        ObservableList<TestModel> olTestModel = FXCollections.observableArrayList(testmodel -> new Observable[] {
                testmodel.strProperty(),
                testmodel.strObjectProperty()
        });

        olTestModel.add(new TestModel("A", "a"));
        olTestModel.add(new TestModel("B", "b"));

        olTestModel.addListener((ListChangeListener.Change<? extends TestModel > c) -> {
            while (c.next()) {
                if (c.wasUpdated()) {
                    System.out.println("===> wasUpdated() triggered");
                }
            }
        });

        TableView<TestModel> table = new TableView<>();

        TableColumn<TestModel, String> strCol = new TableColumn<>("strCol");
        strCol.setCellValueFactory(cellData -> cellData.getValue().strProperty());
        strCol.setCellFactory(TextFieldTableCell.forTableColumn(new DefaultStringConverter()));
        strCol.setEditable(true);
        strCol.setPrefWidth(100);
        strCol.setOnEditCommit((CellEditEvent<TestModel, String> t) -> {
                ((TestModel) t.getTableView().getItems().get(
                        t.getTablePosition().getRow())
                        ).setStr(t.getNewValue());
        });

        TableColumn<TestModel, String> strObjectCol = new TableColumn<>("strObjectCol");
        strObjectCol.setCellValueFactory(cellData -> cellData.getValue().strObjectProperty());
        strObjectCol.setCellFactory(TextFieldTableCell.forTableColumn(new DefaultStringConverter()));
        strObjectCol.setEditable(true);
        strObjectCol.setPrefWidth(100);
        strObjectCol.setOnEditCommit((CellEditEvent<TestModel, String> t) -> {
            ((TestModel) t.getTableView().getItems().get(
                    t.getTablePosition().getRow())
                    ).setStrObject(t.getNewValue());
        });

        table.getColumns().addAll(strCol, strObjectCol);
        table.setItems(olTestModel);
        table.getSelectionModel().setCellSelectionEnabled(true);
        table.setEditable(true);

        BorderPane content = new BorderPane(table);
        return content;
    }

    public class TestModel {

        private StringProperty str;
        private ObjectProperty<String> strObject;

        public TestModel(
            String str,
            String strObject
        ) {
            this.str = new SimpleStringProperty(str);
            this.strObject = new SimpleObjectProperty(strObject);
        }

        public String getStr() {
            return this.str.get();
        }

        public void setStr(String str) {
            this.str.set(str);
        }

        public StringProperty strProperty() {
            return this.str;
        }

        public String getStrObject() {
            return this.strObject.get();
        }

        public void setStrObject(String strObject) {
            this.strObject.set(strObject);
        }

        public ObjectProperty<String> strObjectProperty() {
            return this.strObject;
        }

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle("Test");
        stage.setWidth(350);
        stage.show();
    }

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

}

通过查看 StringPropertyBaseObjectPropertyBase 的源代码可以看出区别——特别是它们的 set 方法。

StringPropertyBase

@Override
public void set(String newValue) {
    if (isBound()) {
        throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
                getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
    }
    if ((value == null)? newValue != null : !value.equals(newValue)) {
        value = newValue;
        markInvalid();
    }
}

ObjectPropertyBase

@Override
public void set(T newValue) {
    if (isBound()) {
        throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
                getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
    }
    if (value != newValue) {
        value = newValue;
        markInvalid();
    }
}

注意到他们检查新值是否等于旧值的不同之处了吗? StringPropertyBase class 使用 Object.equals 检查,而 ObjectPropertyBase class 使用引用相等性 (==/!=)。

我无法肯定地回答为什么存在这种差异,但我可以大胆猜测:一个ObjectProperty可以容纳任何东西 因此 Object.equals 有可能变得昂贵;例如当使用 ListSet 时。在编码 StringPropertyBase 时,我猜他们认为潜力不存在,String 平等的语义更重要,或两者兼而有之。他们为什么这样做可能有 more/better 原因,但由于我没有参与开发,所以我不知道它们。


有趣的是,如果你看看他们如何处理听众 (com.sun.javafx.binding.ExpressionHelper) 您会看到他们使用 Object.equals 检查是否相等。只有在当前 ChangeListener 已注册时才会进行此相等性检查——可能是为了在没有 ChangeListener 时支持惰性求值。

如果新旧值是 equals,则不会通知 ChangeListeners。但是,这不会阻止 InvalidationListeners 收到通知。因此,您的 ObservableList 将触发更新更改,因为该机制基于 InvalidationListener 而不是 ChangeListener

相关源代码如下:

ExpressionHelper$Generic.fireValueChangedEvent

@Override
protected void fireValueChangedEvent() {
    final InvalidationListener[] curInvalidationList = invalidationListeners;
    final int curInvalidationSize = invalidationSize;
    final ChangeListener<? super T>[] curChangeList = changeListeners;
    final int curChangeSize = changeSize;

    try {
        locked = true;
        for (int i = 0; i < curInvalidationSize; i++) {
            try {
                curInvalidationList[i].invalidated(observable);
            } catch (Exception e) {
                Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
            }
        }
        if (curChangeSize > 0) {
            final T oldValue = currentValue;
            currentValue = observable.getValue();
            final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
            if (changed) {
                for (int i = 0; i < curChangeSize; i++) {
                    try {
                        curChangeList[i].changed(observable, oldValue, currentValue);
                    } catch (Exception e) {
                        Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
                    }
                }
            }
        }
    } finally {
        locked = false;
    }
}

您可以在以下代码中看到此行为:

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

public class Main {

  public static void main(String[] args) {
    ObjectProperty<String> property = new SimpleObjectProperty<>("Hello, World!");
    property.addListener(obs -> System.out.printf("Property invalidated: %s%n", property.get()));
    property.addListener((obs, ov, nv) -> System.out.printf("Property changed: %s -> %s%n", ov, nv));
    property.get(); // ensure valid

    property.set(new String("Hello, World!")); // must not use interned String
    property.set("Goodbye, World!");
  }

}

输出:

Property invalidated: Hello, World!
Property invalidated: Goodbye, World!
Property changed: Hello, World! -> Goodbye, World!