How do you use a JavaFX TableView with java records?

Solution

Use a lambda cell value factory instead of a PropertyValueFactory.

For some explanation of the difference between the two, see:

Why this works

The issue, as you note, is that record accessors don’t follow standard java bean property naming conventions, which is what the PropertyValueFactory expects. For example, a record uses first() rather than getFirst() as an accessor, which makes it incompatible with the PropertyValueFactory.

Should you apply a workaround of “doing this horrible thing” of adding additional get methods to the record, just so you can make use of a PropertyValueFactory to interface with a TableView? -> Absolutely not, there is a better way 🙂

What is needed to fix it is to define your own custom cell factory instead of using a PropertyValueFactory.

This is best done using a lambda (or a custom class for really complicated cell value factories). Using a lambda cell factory has advantages of type safety and compile-time checks that a PropertyValueFactory does not have (see the prior referenced answer for more information).

Examples for defining lambdas instead of PropertyValueFactories

An example usage of a lambda cell factory definition for a record String field:

TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
lastColumn.setCellValueFactory(
        p -> new SimpleStringProperty(p.getValue().last())
);

It is necessary to wrap the record field in a property or binding as the cell value factory implementation expects an observable value as input.

You may be able to use a ReadOnlyStringWrapper instead of SimpleStringProperty, like this:

lastColumn.setCellValueFactory(
        p -> new ReadOnlyStringWrapper(p.getValue().last()).getReadOnlyProperty()
);

In a quick test that worked. For immutable records, it might be a better approach, but I haven’t thoroughly tested it to be sure, so, to be safe, I have used simple read-write properties throughout the rest of this example.

Similarly, for an int field:

TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
ageColumn.setCellValueFactory(
        p -> new SimpleIntegerProperty(p.getValue().age()).asObject()
);

The need to put asObject() on the end of the lambda is explained here, in case you are curious (but it is just a weird aspect of the usage of java generics by the JavaFX framework, which isn’t worth spending a lot of time investigating, just add the asObject() call and move on IMO):

Similarly, if your record contains other objects (or other records), then you can define a cell value factory for SimpleObjectProperty<MyType>.

Note: This approach for lambda cell factory definition and the patterns defined above also works for standard (non-record) classes. There is nothing special here for records. The only thing to be aware of is to take care to use the correct accessor name after the getValue() call in the lambda. For example, use first() rather than the standard getFirst() call which you would usually define on a class to support the standard Java Bean naming pattern. The really great thing about this is that, if you define the accessor name wrong, you will get a compiler error and know the exact issue and location before you even attempt to run the code.

Example Code

Full executable example based on the code in the question.

example

Person.java

public record Person(String last, String first, int age) {}

RecordTableViewer.java

import javafx.application.Application;
import javafx.beans.property.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class RecordTableViewer extends Application {
    @Override
    public void start(Stage stage) {
        TableView<Person> table = new TableView<>();

        TableColumn<Person, String> lastColumn = new TableColumn<>("Last");
        lastColumn.setCellValueFactory(
                p -> new SimpleStringProperty(p.getValue().last())
        );

        TableColumn<Person, String> firstColumn = new TableColumn<>("First");
        firstColumn.setCellValueFactory(
                p -> new SimpleStringProperty(p.getValue().first())
        );

        TableColumn<Person, Integer> ageColumn = new TableColumn<>("Age");
        ageColumn.setCellValueFactory(
                p -> new SimpleIntegerProperty(p.getValue().age()).asObject()
        );

        //noinspection unchecked
        table.getColumns().addAll(lastColumn, firstColumn, ageColumn);

        table.getItems().addAll(
                new Person("Smith", "Justin", 41),
                new Person("Smith", "Sheila", 42),
                new Person("Morrison", "Paul", 58),
                new Person("Tyx", "Kylee", 40),
                new Person("Lincoln", "Abraham", 200)
        );

        stage.setScene(new Scene(table, 200, 200));
        stage.show();
    }
}

Should PropertyValueFactory be “fixed” for records?

Record field accessors follow their own access naming convention, fieldname(), just like Java Beans do, getFieldname().

Potentially an enhancement request could be raised for PropertyValueFactory to change its implementation in the core framework so that it can also recognize the record accessor naming standard.

However, I do not believe that updating PropertyValueFactory to recognize record field accessors would be a good idea.

A better solution is not to update PropertyValueFactory for record support and to only allow the typesafe custom cell value approach which is outlined in this answer.

I believe this because of the explanation provided by kleopatra in comments:

a custom valueFactory is definitely the way to go 🙂 Even if it might appear attractive to some to implement an equivalent to PropertyValueFactory – but: that would be a bad idea, looking at the sheer number of questions of type “data not showing in table” due to typos ..

Leave a Comment