Issue
Many answers (and comments) to questions relating to PropertyValueFactory
recommend avoiding that class and others like it. What is wrong with using this class, and what should be used in its place?
Solution
TL;DR:
You should avoid
PropertyValueFactory
and similar classes because they rely on reflection and, more importantly, cause you to lose helpful compile-time validations (such as if the property actually exists).Replace uses of
PropertyValueFactory
with lambda expressions. For example, replace:nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
With:
nameColumn.setCellValueFactory(data -> data.getValue().nameProperty());
(assumes you're using Java 8+ and you've defined the model class to expose JavaFX properties)
PropertyValueFactory
This class, and others like it, is a convenience class. JavaFX was released during the era of Java 7 (if not earlier). At that time, lambda expressions were not part of the language. This meant JavaFX application developers had to create an anonymous class whenever they wanted to set the cellValueFactory
of a TableColumn
. It would look something like this:
// Where 'nameColumn' is a TableColumn<Person, String> and Person has a "name" property
nameColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person>, ObservableValue<String>>() {
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<Person> data) {
return data.getValue().nameProperty();
}
});
As you can see, this is pretty verbose. Imagine doing the same thing for 5 columns, 10 columns, or more. So, the developers of JavaFX added convenience classes such as PropertyValueFactory
, allowing the above to be replaced with:
nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
Disadvantages of PropertyValueFactory
However, using PropertyValueFactory
and similar classes has its own disadvantages. Those disadvantages being:
- Relying on reflection, and
- Losing compile-time validations.
Reflection
This is the more minor of the two disadvantages, though it directly leads to the second one.
The PropertyValueFactory
takes the name of the property as a String
. The only way it can then invoke the methods of the model class is via reflection. You should avoid relying on reflection when you can, as it adds a layer of indirection and slows things down (though in this case, the performance hit is likely negligible).
The use of reflection also means you have to rely on conventions not enforceable by the compiler. In this case, if you do not follow the naming conventions for JavaFX properties exactly, then the implementation will fail to find the needed methods, even when you think they exist.
No Compile-time Validations
Since PropertyValueFactory
relies on reflection, Java can only validate certain things at run-time. More specifically, the compiler cannot validate that the property exists, or if the property is the right type, during compilation. This makes developing the code harder.
Say you had the following model class:
/*
* NOTE: This class is *structurally* correct, but the method names
* are purposefully incorrect in order to demonstrate the
* disadvantages of PropertyValueFactory. For the correct
* method names, see the code comments above the methods.
*/
public class Person {
private final StringProperty name = new SimpleStringProperty(this, "name");
// Should be named "setName" to follow JavaFX property naming conventions
public final void setname(String name) {
this.name.set(name);
}
// Should be named "getName" to follow JavaFX property naming conventions
public final String getname() {
return name.get();
}
// Should be named "nameProperty" to follow JavaFX property naming conventions
public final StringProperty nameproperty() {
return name;
}
}
Having something like this would compile just fine:
TableColumn<Person, Integer> nameColumn = new TableColumn<>("Name");
nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
nameColumn.setCellFactory(tc -> new TableCell<>() {
@Override
public void updateItem(Integer item, boolean empty) {
if (empty || item == null) {
setText(null);
} else {
setText(item.toString());
}
}
});
But there will be two issues at run-time.
The
PropertyValueFactory
won't be able to find the "name" property and will throw an exception at run-time. This is because the methods ofPerson
do not follow the property naming conventions. In this case, they failed to follow thecamelCase
pattern. The methods should be:getname
→getName
setname
→setName
nameproperty
→nameProperty
Fixing this problem will fix this error, but then you run into the second issue.
The call to
updateItem(Integer item, boolean empty)
will throw aClassCastException
, saying aString
cannot be cast to anInteger
. We've "accidentally" (in this contrived example) created aTableColumn<Person, Integer>
when we should have created aTableColumn<Person, String>
.
What Should You Use Instead?
You should replace uses of PropertyValueFactory
with lambda expressions, which were added to the Java language in version 8.
Since Callback
is a functional interface, it can be used as the target of a lambda expression. This allows you to write this:
// Where 'nameColumn' is a TableColumn<Person, String> and Person has a "name" property
nameColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person>, ObservableValue<String>>() {
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<Person> data) {
return data.getValue().nameProperty();
}
});
As this:
nameColumn.setCellValueFactory(data -> data.getValue().nameProperty());
Which is basically as concise as the PropertyValueFactory
approach, but with neither of the disadvantages discussed above. For instance, if you forgot to define Person#nameProperty()
, or if it did not return an ObservableValue<String>
, then the error would be detected at compile-time. This forces you to fix the problem before your application can run.
The lambda expression even gives you more freedom, such as being able to use expression bindings.
Disadvantage
There is one disadvantage, though it's small.
The "number properties", such as IntegerProperty
and DoubleProperty
, all implement ObservableValue<Number>
. This means you either have to:
Use
Number
instead of e.g.,Integer
as the column's value type. This is not too bad, since you can call e.g.,Number#intValue()
if and as needed.Or use e.g.,
IntegerProperty#asObject()
, which returns anObjectProperty<Integer>
. The other "number properties" have a similar method.column.setCellValueFactory(data -> data.getValue().someIntegerProperty().asObject());
Kotlin
If you're using Kotlin, then the lambda may look something like this:
nameColumn.setCellValueFactory { it.value.nameProperty }
Assuming you defined the appropriate Kotlin properties in the model class. See this Stack Overflow answer for details.
Answered By - Slaw
Answer Checked By - Katrina (JavaFixing Volunteer)