What is the purpose of @NamedArg annotation in javaFX 8?

The @NamedArg annotation allows an FXMLLoader to instantiate a class that does not have a zero-argument constructor.

Technical Background:

The FXMLLoader creates objects using reflection. Typically, if you use a tag corresponding to a class with a constructor taking no arguments, an object is created from that class by calling Class.newInstance(), which invokes the no-argument constructor.

If a class is defined only with constructors that take parameters, then this is problematic. The main issue is that the Java Language Specification does not require the names of parameters (to methods or constructors) to be retained at runtime. This means there’s no direct, guaranteed, way for the FXMLLoader to determine which parameter has a given name.

To make this concrete, suppose we define a Person class as follows:

package application;

import javafx.beans.NamedArg;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Person {
    private final StringProperty firstName ;
    private final StringProperty lastName ;

    public Person(String firstName, String lastName) {
        this.firstName = new SimpleStringProperty(this, "firstName", firstName);
        this.lastName = new SimpleStringProperty(this, "lastName", lastName);
    }

    // methods....

}

In FXML we might try to create a Person as follows:

<Person firstName="Jacob" lastName="Smith"/>

This won’t work, because the FXML loader has no guarantee that the runtime representation of the Person class retains the information as to which constructor parameter is firstName and which is lastName.

Historical background

Java 2.2 defined “Builder” classes corresponding to each control. These builder classes follow the standard builder pattern. When the FXMLLoader encounters a tag referencing a class with no zero-argument constructor, it would use the corresponding builder to create the instance.

Unfortunately, the implementation of the builder classes was flawed, and they were deprecated in JavaFX 8, and will be removed in a later version (probably JavaFX 9). This left a problem for the FXMLLoader, which would no longer have builder classes to rely on for instantiating classes with no zero-argument constructor. A real example is the Color class, which has no zero-argument constructor and will have its builder class removed.

@NamedArgs

The fix for this was to introduce an annotation that is used to retain a name of a method (or constructor) argument at runtime. By reflection, we can query the parameter list of a constructor/method, and get the type (but not the name) of each parameter. It is also possible to query each parameter for any annotations, and get the value of those annotations. So the @NamedArg annotation was introduced specifically for the purpose of retaining a name of a parameter at runtime.

Example

For an example, use the Person class we introduced above:

package application;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Person {
    private final StringProperty firstName ;
    private final StringProperty lastName ;

    public Person(String firstName, String lastName) {
        this.firstName = new SimpleStringProperty(this, "firstName", firstName);
        this.lastName = new SimpleStringProperty(this, "lastName", lastName);
    }

    public final StringProperty firstNameProperty() { return firstName; }
    public final String getFirstName() { return firstNameProperty().get(); }
    public final void setFirstName(final String firstName) { firstNameProperty().set(firstName); }
    public final StringProperty lastNameProperty() { return lastName; }
    public final String getLastName() { return lastNameProperty().get(); }
    public final void setLastName(final String lastName) { lastNameProperty().set(lastName); }
}

If you try to load this using FXML:

Person.fxml:

<?xml version="1.0" encoding="UTF-8"?>
<?import application.Person?>
<Person firstName="Jacob" lastName="Smith" xmlns:fx="http://javafx.com/fxml/1" />

Main.java:

package application;

import java.io.IOException;
import javafx.fxml.FXMLLoader;

public class Main  {

    public static void main(String[] args) throws IOException {     
        Person person = FXMLLoader.load(Main.class.getResource("Person.fxml"));
        System.out.println(person.getFirstName()+" "+person.getLastName());
    }
}

then you see an error at runtime:

Caused by: java.lang.NoSuchMethodException: application.Person.<init>()

indicating the FXMLLoader is looking for a constructor taking no arguments (Person.<init>()).

In JavaFX 8, you can fix the problem by specifying the name of the parameters with the @NamedArg annotation:

package application;

import javafx.beans.NamedArg;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Person {
    private final StringProperty firstName ;
    private final StringProperty lastName ;

    public Person(@NamedArg("firstName") String firstName, @NamedArg("lastName") String lastName) {
        this.firstName = new SimpleStringProperty(this, "firstName", firstName);
        this.lastName = new SimpleStringProperty(this, "lastName", lastName);
    }

    public final StringProperty firstNameProperty() { return firstName; }
    public final String getFirstName() { return firstNameProperty().get(); }
    public final void setFirstName(final String firstName) { firstNameProperty().set(firstName); }
    public final StringProperty lastNameProperty() { return lastName; }
    public final String getLastName() { return lastNameProperty().get(); }
    public final void setLastName(final String lastName) { lastNameProperty().set(lastName); }
}

This will allow the FXMLLoader to load the class as required.

Note that you can also fix the issue by defining a builder class, and that this also works in JavaFX 2.0 and later. The JavaFX team decided (probably correctly) that using this approach in a way that didn’t suffer from the bugs that existed in the initial implementation of the builders would add too much bloat to the codebase of the framework.

package application;

public class PersonBuilder {

    private String firstName ;
    private String lastName ;

    private PersonBuilder() {   }

    public static PersonBuilder create() {
        return new PersonBuilder();
    }

    public PersonBuilder firstName(String firstName) {
        this.firstName = firstName ;
        return this ;
    }

    public PersonBuilder lastName(String lastName) {
        this.lastName = lastName ;
        return this ;
    }

    public Person build() {
        return new Person(firstName, lastName);
    }
}

Clearly if you are using JavaFX 8, the constructor annotation approach is much less work.

References:

Leave a Comment