Objectos Weekly #006: Using method references to invoke generic constructors
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.