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);
}
}
setCellValueFactory. Sets the value of the property cellValueFactory.
The TableView class provides built-in capabilities to sort data in columns. Users can alter the order of data by clicking column headers. The first click enables the ascending sorting order, the second click enables descending sorting order, and the third click disables sorting. By default, no sorting is applied.
public PropertyValueFactory(String property) Creates a default PropertyValueFactory to extract the value from a given TableView row item reflectively, using the given property name. Parameters: property - The name of the property with which to attempt to reflectively extract a corresponding value for in a given object.
The difference can be seen by looking at the source code of StringPropertyBase
and ObjectPropertyBase
—specfically, their set
methods.
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();
}
}
Notice the difference in how they check if the new value is equal to the old value? The StringPropertyBase
class checks by using Object.equals
whereas the ObjectPropertyBase
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 for Object.equals
to be expensive; such as when using a List
or Set
. When coding StringPropertyBase
I guess they decided that potential wasn't there, that the semantics of String
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 using Object.equals
. This equality check only occurs if there are currently ChangeListener
s registered—probably to support lazy evaluation when there are no ChangeListener
s.
If the new and old values are equals
the ChangeListener
s are not notified. This doesn't stop the InvalidationListener
s from being notified, however. Thus, your ObservableList
will fire an update change because that mechanism is based on InvalidationListener
s and not ChangeListener
s.
Here's the relevant source code:
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;
}
}
And you can see this behavior in the following code:
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!");
}
}
Output:
Property invalidated: Hello, World!
Property invalidated: Goodbye, World!
Property changed: Hello, World! -> Goodbye, World!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With