Search code examples
javajavafxtableview

How to resolve "Unchecked generics" warning when setting columns on a TableView in JavaFX


I encountered a compiler warning in this common situation of adding columns to a table view control in a JavaFX app.

To demonstrate, here is my modified version of the code example seen in the Javadoc for the JavaFX/OpenJFX class TableView. Note the getColumns line.

public class HelloApplication extends Application
{
    @Override
    public void start ( Stage stage )
    {
        TableView < Person > tableViewPersons = new TableView <> ( );
        ObservableList < Person > persons = FXCollections.observableArrayList ( this.fetchPersons ( ) );
        tableViewPersons.setItems ( persons );

        TableColumn < Person, String > firstNameCol = new TableColumn <> ( "First Name" );
        firstNameCol.setCellValueFactory ( new PropertyValueFactory <> ( persons.get ( 0 ).firstNameProperty ( ).getName ( ) ) );

        TableColumn < Person, String > lastNameCol = new TableColumn <> ( "Last Name" );
        lastNameCol.setCellValueFactory ( new PropertyValueFactory <> ( persons.get ( 0 ).lastNameProperty ( ).getName ( ) ) );

        tableViewPersons.getColumns ( ).setAll ( firstNameCol , lastNameCol );  // <---- Warning: Unchecked generics array creation for varargs parameter.

        Scene scene = new Scene ( tableViewPersons , 320 , 240 );
        stage.setTitle ( "JavaFX Example" );
        stage.setScene ( scene );
        stage.show ( );
    }
…

The line:

tableViewPersons.getColumns ( ).setAll ( firstNameCol , lastNameCol );

… generates a warning for:

Warning: Unchecked generics array creation for varargs parameter

I understand from this Answer on Java unchecked: unchecked generic array creation for varargs parameter, and from this post, that the issue involves ambiguity about the type of the arguments being passed.

If my goal is to resolve this compiler warning, then one solution is to make explicit the parameterized type of the collection of TableColumn objects be passed to ObservableList#setAll. So we can explicitly declare a List of the TableColumn objects.

        List < TableColumn < Person, ? > > columns = List.of ( firstNameCol , lastNameCol );  // <--- Adding this line to make explicit the parameterized type of `TableColumn` to resolve the "Unchecked generics" warning.
        tableViewPersons.getColumns ( ).setAll ( columns );

See that new code in the full context:

public class HelloApplication extends Application
{
    @Override
    public void start ( Stage stage )
    {
        TableView < Person > tableViewPersons = new TableView <> ( );
        ObservableList < Person > persons = FXCollections.observableArrayList ( this.fetchPersons ( ) );
        tableViewPersons.setItems ( persons );

        TableColumn < Person, String > firstNameCol = new TableColumn <> ( "First Name" );
        firstNameCol.setCellValueFactory ( new PropertyValueFactory <> ( persons.get ( 0 ).firstNameProperty ( ).getName ( ) ) );

        TableColumn < Person, String > lastNameCol = new TableColumn <> ( "Last Name" );
        lastNameCol.setCellValueFactory ( new PropertyValueFactory <> ( persons.get ( 0 ).lastNameProperty ( ).getName ( ) ) );

        List < TableColumn < Person, ? > > columns = List.of ( firstNameCol , lastNameCol );  // <--- Adding this line to make explicit the parameterized type of `TableColumn` to resolve the "Unchecked generics" warning.
        tableViewPersons.getColumns ( ).setAll ( columns );

        Scene scene = new Scene ( tableViewPersons , 320 , 240 );
        stage.setTitle ( "JavaFX Example" );
        stage.setScene ( scene );
        stage.show ( );
    }
…

My question:

  • Is this a valid, complete solution to resolving the the "Unchecked generics" warning?
  • Is there any other simpler way to resolve the "Unchecked generics" warning? (I would rather not suppress the warning.)

Solution

  • Using alternate API without varargs

    One way to handle the unchecked generics warnings (which won't work in all cases) is to rewrite your code to use a different API which is less susceptible to such issues.

    For example, instead of calling setAll, you can call clear on the list and then call the add method individually for each item. The varargs typed setAll is mostly a convenience method for doing this.

    There is a slight difference in meaning between the two options. Because the list is observable, changes are fired after each atomic operation. setAll makes all of the changes for the call atomic, resulting in fewer changes fired, but I don't think that will be an issue in this case.

    It is more wordy to call add for each column individually if you have many columns, but will avoid any potential for type errors and warnings. (I'm not advocating that approach, just letting you know the possibility).

    You can replace the call:

    tableViewPersons.getColumns().setAll(firstNameCol, lastNameCol);  // <---- Warning: Unchecked generics array creation for varargs parameter.
    

    With alternate API calls which do not use a varargs parameter:

    tableViewPersons.getColumns().clear();
    tableViewPersons.getColumns().add(firstNameCol);
    tableViewPersons.getColumns().add(lastNameCol);
    

    Your solution is OK (and also uses alternate API without varargs)

    Your solution to this issue where you create a List of a given type up front and provide that to the TableView is really the same approach as described in the above section, you are using a different API instead of the varargs API.

    List < TableColumn < Person, ? > > columns = List.of ( firstNameCol , lastNameCol );  // <--- Adding this line to make explicit the parameterized type of `TableColumn` to resolve the "Unchecked generics" warning.
    tableViewPersons.getColumns ( ).setAll ( columns );
    

    ObservableList has overloaded setAll methods:

    You are replacing the varargs API call with the single argument variant. It is a perfectly valid solution. It won't work in the general case with different APIs that don't provide an overloaded interface like this, but it will work in this instance.

    In your case, where all columns are mapping String properties, I would not use the wildcard type ?, instead I would use:

    List < TableColumn < Person, String > > columns = List.of ( firstNameCol , lastNameCol );
    

    and only use the wild card type if each column mapped to a different type.

    Using SafeVarargs

    Another way to handle this is to create a helper method, annotated SafeVarargs to assert you are using the variable arguments safely.

    //...
    addColumns(tableViewPersons, firstNameCol, lastNameCol);
    //...
    
    @SafeVarargs
    private static void addColumns(
        TableView<Person> tableViewPersons, 
        TableColumn<Person, String>... columns) 
    {
        tableViewPersons.getColumns().setAll(columns);
    }
    

    Unsolicited opinion on suppressing warnings

    I know you don't want to suppress the warnings as mentioned in the question (which is why I have provided other options). But I think, in this case, suppression is OK.

    My opinion is that the unchecked stuff is just an annoyance in many cases. Generics were added to Java in what I think even the designers would say was a compromise design, because the language was not built for generics originally. The stuff that shows up around some of the unchecked generics warnings in cases like this is a part of the compromise design.

    Having the main type on the TableView definition, backing list, etc, is good (and important). But when you start having to do stuff like use wildcard types or method level type specifiers when using classes and methods that are already typed, or trying to hunt around to remove a warning on a method usage that you already know is safe, it becomes more distracting than beneficial. In such cases, I advise suppressing the warnings on a selective basis.

    Potentially you can use your IDE to assist in suppressing unnecessary warnings, for example suppressing in Idea. Before doing so, check that it isn't really a significant potential for error, but if it is not (like a lot of the unchecked generics warnings IMO), then I think it is OK to suppress them.

    For example to suppress the warning in the Idea IDE for a specific line, the following comment can be used (this is what I usually do):

    //noinspection unchecked
    tableViewPersons.getColumns().setAll(firstNameCol, lastNameCol);
    

    Unfortunately, in a general case Java only allows suppressing the warnings on a class, field or method level.

    So a portable way to do this at the method level is using the SupressWarnings annotation where limited, relevant code is placed in a small method:

    @SuppressWarnings("unchecked")
    private TableView<Person> createPersonTableView(ObservableList<Person> people) {
        TableView<Person> tableViewPersons = new TableView<>(people);
    
        TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
        firstNameCol.setCellValueFactory(data -> data.getValue().firstNameProperty());
    
        TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
        firstNameCol.setCellValueFactory(data -> data.getValue().lastNameProperty());
        
        tableViewPersons.getColumns().setAll(firstNameCol, lastNameCol);
        
        return tableViewPersons;
    }
    

    Using lambda's instead of PropertyValueFactory

    Incidentally, but not related to the generics issue, the example snippet above also illustrates the "correct" use of lambdas to set the cell value factories rather than relying on PropertyValueFactory, as discussed in:

    In my opinion, using lambdas instead of PropertyValueFactory is more important than the issues around suppression of the varargs type checks on the TableView method calls, because it is far easier to create runtime errors when using a PropertyValueFactory and more difficult to debug them if they do arise.