A relative Java newbie question.
I have a TableView with extractors and a ListChangeListener
added to the underlying ObservableList.
If I have a StringProperty
column in the data model, the change listener doesn't detect changes if I double-click the cell and then hit ENTER without making any changes. That's good.
However, if I define the column as ObjectProperty<String>
and double-click and then hit ENTER, the change listener always detects changes even when none have been made.
Why does that happen? What's the difference between ObjectProperty<String>
and StringProperty
from a change listener's point of view?
I've read Difference between SimpleStringProperty and StringProperty and JavaFX SimpleObjectProperty<T> vs SimpleTProperty and think I understand the differences. But I don't understand why the change listener is giving different results for TProperty
/SimpleTProperty
and ObjectProperty<T>
.
If it helps, here is a MVCE for my somewhat nonsensical case. I'm actually trying to get a change listener working for BigDecimal
and LocalDate
columns and have been stuck on it for 5 days. If I can understand why the change listener is giving different results, I might be able to get my code working.
I'm using JavaFX8 (JDK1.8.0_181), NetBeans 8.2 and 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);
}
}
The difference can be seen by looking at the source code of
StringPropertyBase
andObjectPropertyBase
—specfically, theirset
methods.StringPropertyBase
ObjectPropertyBase
Notice the difference in how they check if the new value is equal to the old value? The
StringPropertyBase
class checks by usingObject.equals
whereas theObjectPropertyBase
class uses reference equality (==
/!=
).I can't answer for certain why this difference exists, but I can hazard a guess: An
ObjectProperty
can hold anything and therefore there's the potential forObject.equals
to be expensive; such as when using aList
orSet
. When codingStringPropertyBase
I guess they decided that potential wasn't there, that the semantics ofString
equality was more important, or both. There may be more/better reasons for why they did what they did, but as I was not involved in development I'm not aware of them.Interestingly, if you look at how they handle listeners (
com.sun.javafx.binding.ExpressionHelper
) you'll see that they check for equality usingObject.equals
. This equality check only occurs if there are currentlyChangeListener
s registered—probably to support lazy evaluation when there are noChangeListener
s.If the new and old values are
equals
theChangeListener
s are not notified. This doesn't stop theInvalidationListener
s from being notified, however. Thus, yourObservableList
will fire an update change because that mechanism is based onInvalidationListener
s and notChangeListener
s.Here's the relevant source code:
ExpressionHelper$Generic.fireValueChangedEvent
And you can see this behavior in the following code:
Output: