Objectos Weekly #006: Using method references to invoke generic constructors

Marcio Endo
December 18, 2022

Welcome to Objectos Weekly issue #006.

In Java, constructors can declare type parameters. Also in Java, you can represent a deferred class instance creation with a method reference expression. How do you supply type arguments to a generic constructor when using method references? This is what we will discuss in this issue.

On a personal note, last Thursday, Monica and I celebrated 6 years together! We had our first date on December 15th, 2016.

Let's begin.

Generic constructors

I wrote about generic constructors before. I recommend you read the article if you are not familiar with them.

One takeaway from that article is:

If constructors are allowed to declare type parameters;
then language constructs which invoke constructors must be allowed to supply type arguments.

To illustrate, suppose a class called NonEmptySet. The class is non-generic and declares the following generic constructor:

public <T> NonEmptySet(T defaultValue, Set<T> set) {
  this.defaultValue = Objects.requireNonNull(defaultValue);
  this.set = Objects.requireNonNull(set);
}

Notice how the constructor declares the <T> type parameter. We can invoke this constructor from a different constructor of the same class. The following is valid Java code:

public NonEmptySet(String defaultValue) {
  <String> this(defaultValue, Set.of());
}

Note the explicit <String> type argument before the this keyword. Supplying the type argument is not required as the compiler can infer the type. On the other hand, you can be explicit if you prefer, like we were in the example.

When creating instances using the new keyword the constructor is invoked. As the constructor is generic, we can supply type arguments. The following is also valid Java code:

Set<String> values = getValues(); 
var set = new <String> NonEmptySet("N/A", values);

Note the <String> type argument after the new keyword. If we were to change its value, to say Integer, compilation would now fail:

Set<String> values = getValues(); 
var set = new <Integer> NonEmptySet("N/A", values);
// compilation error                ^^^^^  ^^^^^^

Method references

Since Java 8, constructors can be lazily invoked using method reference expressions.

Suppose the following Java record that represents a City:

public record City(String name) {}

We can cause the City constructor to be lazily invoked by using a method reference like so:

import objectos.util.GrowableList;

var cityNames = List.of(
  "São Paulo", "Belém", "Rio de Janeiro");

var cities = cityNames.stream()
    .map(City::new) // <-- method ref
    .collect(Collectors.toCollection(GrowableList::new));

System.out.println(cities);

Running the program prints:

GrowableList [
  0 = City[name=São Paulo]
  1 = City[name=Belém]
  2 = City[name=Rio de Janeiro]
]

Method reference as a lambda expression

The method reference in our previous example City::new does not invoke the constructor right away. Instead, it produces an instance of a functional interface. In our example, an instance of Function<String, City> is created. The constructor is invoked at a later time when the function is evaluated.

We can rewrite the method reference as a lambda expression. It makes it easier to "see" the constructor invocation. In our previous example, the following two forms produce the same results:

Function<String, City> f;
f = City::new;

Function<String, State> f;
f = (cityName) -> new City(cityName);

We know that, as constructors can be generic, we can supply type arguments to the second form.

And what about the first form?

Method references and type arguments

If we are allowed to invoke a generic constructor via a method reference, then we must also be allowed to supply type arguments to the constructor via the method reference.

We can, in fact.

Method reference expressions are defined in Section 15.13 of the Java Language Specification. Here is the production:

MethodReference:
  ExpressionName :: [TypeArguments] Identifier
  Primary :: [TypeArguments] Identifier
  ReferenceType :: [TypeArguments] Identifier
  super :: [TypeArguments] Identifier
  TypeName . super :: [TypeArguments] Identifier
  ClassType :: [TypeArguments] new
  ArrayType :: new

Notice how all forms, except for the last one, includes an optional TypeArguments.

For the rest of this article, we will focus on the form:

ClassType :: [TypeArguments] new

Let's now illustrate it with an example.

Our running example

To keep things simple, we will use the same example from the generic constructors post.

Suppose we want to send a simplified log message represented by the following Java record:

public record Log(long millis, Level level, String msg) {}

We have to send this data in the JSON format. So we will use the following converter:

public class LogConverter {
  public String convert(Log log) {
    return """
    {
      "millis": %d,
      "level": "%s",
      "msg": "%s"
    }""".formatted(log.millis(), log.level(), log.msg());
  }
}

The payload

The actual data is sent via a Payload class. It represents some arbitrary data to be sent over the wire:

public record Payload(String data) {
  public <T> Payload(Function<T, String> converter, T item) {
    this(converter.apply(item));
  }
}

Notice that the class declares a generic constructor. It accepts an instance of a converter along with an instance of the data to be sent.

The Payload generic constructor can be represented by the following BiFunction when <T> is Log:

BiFunction<Function<Log, String>, Log, Payload> constructor;
constructor = Payload::new;

We used a method reference to obtain the BiFunction instance. Additionally, we relied on the compiler's type inference.

Let's supply explicit type arguments instead. The following compiles without errors:

BiFunction<Function<Log, String>, Log, Payload> constructor;
constructor = Payload::<Log> new;

Note the <Log> type argument immediately before the new keyword. Let's rewrite the method reference as a lambda expression:

BiFunction<Function<Log, String>, Log, Payload> constructor;
constructor = (converter, item) -> new <Log> Payload(converter, item);

Once again the explicit type argument is not strictly required; the compiler can infer it.

It is just that, until recently, I didn't know code like that was valid Java code!

Putting it all together

Let's see those forms in action. Consider the following program:

public class Example {
  public static void main(String[] args) {
    BiFunction<Function<Log, String>, Log, Payload> constructor;
    constructor = Payload::<Log> new;

    Function<Log, String> converter;
    converter = new LogConverter()::convert;

    Consumer<Log> emitter;
    emitter = (log) -> {
      var payload = constructor.apply(converter, log);

      System.out.println(payload);
    };

    emitter.accept(new Log(111L, Level.INFO, "Hello world!"));
    emitter.accept(new Log(222L, Level.ERROR, "Uh-oh"));
  }
}

The emitter consumer combines the Payload constructor and the LogConverter. It acts as a serializer of sorts.

Running this program prints:

Payload[data={
  "millis": 111,
  "level": "INFO",
  "msg": "Hello world!"
}]
Payload[data={
  "millis": 222,
  "level": "ERROR",
  "msg": "Uh-oh"
}]

Great! It works.

Let's work together

Please know that I will be open for freelance consulting work in 2023. Do you have a legacy Java application that needs some looking at? Perhaps I can help. Let's get in touch.

You can find my contacts on this page. All work will be provided via Objectos Software LTDA based in São Paulo, Brazil.

Until the next issue of Objectos Weekly

So that's it for today. I hope you enjoyed reading.

The source code of all of the examples are in this GitHub repository.

Please send me an e-mail if you have comments, questions or corrections regarding this post.