Why should I avoid using PropertyValueFactory in JavaFX?

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:

  1. Relying on reflection, and
  2. 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.

  1. The PropertyValueFactory won’t be able to find the “name” property and will throw an exception at run-time. This is because the methods of Person do not follow the property naming conventions. In this case, they failed to follow the camelCase pattern. The methods should be:

    • getnamegetName
    • setnamesetName
    • namepropertynameProperty

    Fixing this problem will fix this error, but then you run into the second issue.

  2. The call to updateItem(Integer item, boolean empty) will throw a ClassCastException, saying a String cannot be cast to an Integer. We’ve “accidentally” (in this contrived example) created a TableColumn<Person, Integer> when we should have created a TableColumn<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:

  1. 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.

  2. Or use e.g., IntegerProperty#asObject(), which returns an ObjectProperty<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.

Records

If the data is your TableView is read-only then you can use a record, which is a special kind of class.

For a record, you cannot use a PropertyValueFactory and must use a custom cell value factory (e.g. a lambda).

The naming strategy for record accessor methods differs from the standard java beans naming strategy. For example, for a member called name the standard java beans accessor name used by PropertyValueFactory would be getName(), but for a record, the accessor for the name member is just name(). Because records don’t follow the naming conventions required by a PropertyValueFactory, a PropertyValueFactory cannot be used to access data stored in records.

However, the lambda approach detailed in this answer will be able to access the data in the record just fine.

Further information and an example of using a record with a cell value factory for a TableView can be found at:

Leave a Comment