Later this year I will work again on Objectos Code. It is a library for generating Java source code. At time of writing, it is still unreleased and alpha-quality. Using a code similar to the following planned one:
import static objectos.code.Java.*;
_var(id("foo"), _new(Foo.class, l(1), l("abc")));
Would generate the following Java statement:
var foo = new Foo(1, "abc");
Whenever I work on it, I have to study the Java Language Specification. In the previous example, I need to know what the actual formal definition of a new
expression is. Its formal name is Class Instance Creation Expression, defined in section 15.9. A relevant production is the following:
UnqualifiedClassInstanceCreationExpression:
new [TypeArguments] ClassOrInterfaceTypeToInstantiate ( [ArgumentList] ) [ClassBody]
As you can see, there is an optional list of type arguments immediately after the new
keyword. I did not know about that. The following java class compiles:
public class OptionalTypeArgumentsExample {
public static void main(String[] args) {
var t = new <String> Thread();
t.start();
}
}
The Eclipse JDT compiler issues a warning about an unused type argument. On the other hand, javac
(OpenJDK 18.0.1.1) compiles it without warnings:
$ javac -Xlint:all src/main/java/iter1/OptionalTypeArgumentsExample.java
[no warnings emitted]
So what is this? JLS section 15.9 states (emphasis mine):
If the constructor is generic (§8.8.4), the type arguments to the constructor may similarly either be inferred or passed explicitly.
Ah, so constructors can be generic. Well, I did not about that either. I think we should investigate that.
So, in this blog post I will give you a quick overview of generic constructors.
Generic constructors are rarely used (in the JDK)
As I have never seen generic constructors before I wanted to know how "real-world" code uses them. So I wrote a program that parses the Java files in the JDK source code. It uses the JavaParser open-source library. Since its README file mentions Java 15, I ran the program on tag jdk-15+36 of the JDK source code.
I found seven classes having generic constructors. They are all in the java.management
module. Four classes are exported (and therefore have Javadocs):
- javax.management.StandardEmitterMBean
- javax.management.StandardMBean
- javax.management.openmbean.OpenMBeanAttributeInfoSupport
- javax.management.openmbean.OpenMBeanParameterInfoSupport
While three of the classes are internal:
- com.sun.jmx.mbeanserver.MXBeanSupport
- com.sun.jmx.mbeanserver.MBeanSupport
- com.sun.jmx.mbeanserver.StandardMBeanSupport
Therefore one can safely say generic constructors are rarely used. At least in the JDK source code.
Still... it gives a glimpse on how to use them
Let's study the signature of one of those constructors.
For example, let's take this one from the OpenMBeanAttributeInfoSupport
class. Its signature is:
public <T> OpenMBeanParameterInfoSupport(
String name, String description,
OpenType<T> openType, T defaultValue)
throws OpenDataException
The Javadocs for the type parameter <T>
says:
T - allows the compiler to check that the defaultValue, if non-null, has the correct Java type for the given openType.
So the type parameter in the constructor prevents mixing incompatible types. In other words, the following code to compiles:
OpenType<Foo> openType = getOpenType();
Foo foo = getFoo();
var o = new OpenMBeanParameterInfoSupport(
"A Name", "Some description", openType, foo);
As OpenType<Foo>
is compatible with Foo
. However, the following code fails to compile:
OpenType<Foo> openType = getOpenType();
Bar bar = getBar();
var o = new OpenMBeanParameterInfoSupport(
"A Name", "Some description", openType, bar);
// compilation error ^^^
As OpenType<Foo>
is not compatible with Bar
.
Great, let's try to create an example using same idea. It should make things clearer.
A simple example
Suppose we have a Payload
class that represents arbitrary data to be sent over the wire. For example, It could be JSON data to be sent over HTTPS. To keep our example simple, we will model the data as a String
value. Additionally, since our data is immutable, we will use a Java record:
public record Payload(String data) {}
So, if we were to send a "Hello world!" message over the wire, we could invoke a hypothetical send
method like so:
send(new Payload("Hello world!"));
The actual JSON payload sent by our hypothetical service is not important for our example. But, for completeness sake, let's suppose the JSON data sent in our previous example was:
{
"data": "Hello world!"
}
That's great. Next, let's add a little complexity to our data.
Sending other data types
Suppose now we want to send data that is both structured and more complex than our previous "Hello world!" message. For example, we want to send a simplified log message represented by the following Java record:
public record Log(long millis, Level level, String msg) {}
This data is structured in the sense that its JSON format is defined by 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());
}
}
To send our log record we could just:
var converter = new LogConverter();
var log = new Log(12345L, Level.INFO, "A message");
var data = converter.convert(log);
send(new Payload(data));
But we expect more data types each with its own structure. That is, each data type will bring its own converter. So, let's refactor our Payload
record.
Enter the generic constructor
Since each data type will have its own converter there is a chance to use a generic constructor like so:
public record Payload(String data) {
public <T> Payload(Function<T, String> converter, T item) {
this(converter.apply(item));
}
}
The converter is represented by a Function
from a generic type T
to a String
. Our parameterized constructor ensures that the second argument's type is compatible with the converter.
So let's use our new constructor. The following test does just that:
@Test
public void data() {
var converter = new LogConverter();
var log = new Log(12345L, Level.INFO, "A message");
var p = new Payload(converter::convert, log);
assertEquals(p.data(), """
{
"millis": 12345,
"level": "INFO",
"msg": "A message"
}
""");
}
Good, our test passes. Granted, there is very little difference to the previous example using the canonical constructor. But it does its job as an example of generic constructors.
Invoking generic constructors
In our last example we invoked our generic constructor just like we do with a non-generic one. In other words, we did not provide explicit type arguments to our generic constructor. The actual type arguments were inferred by the compiler.
We can be explicit if we wanted. That is, we can provide a type argument list to the generic constructor.
Providing type arguments with the new
keyword
Taking again our last example, we can provide explicit type arguments. So the class instance creation becomes:
var p = new <Log> Payload(converter::convert, log);
Notice the <Log>
right after the new
keyword. Providing explicit type arguments means that the following code does not compile:
var converter = new LogConverter();
var log = new Log(12345L, Level.INFO, "A message");
var p = new <Category> Payload(converter::convert, log);
// compilation error ^^^ ^^^
The compiler tries to match the actual arguments to a "virtual" constructor having the following signature:
public Payload(Function<Category, String> converter, Category item);
As the types are not compatible, compilation fails.
Providing type arguments with the this
or super
keyword
Apart from the class instance creation expression (i.e., new
keyword), there are other ways to invoke constructors. In particular, constructors themselves can invoke other constructors:
- a constructor in the same class using
this
; and - a constructor from the superclass using
super
.
But what happens if the invoked constructor is generic? Let's investigate.
Here's the production from Section 8.8.7.1 of the JLS:
ExplicitConstructorInvocation:
[TypeArguments] this ( [ArgumentList] ) ;
[TypeArguments] super ( [ArgumentList] ) ;
ExpressionName . [TypeArguments] super ( [ArgumentList] ) ;
Primary . [TypeArguments] super ( [ArgumentList] ) ;
As suspected, both this
and super
can be invoked with a type arguments list.
So let's try it with our Payload
record. We can add a specialized constructor for a Log
instance like so:
public record Payload(String data) {
public <T> Payload(Function<T, String> converter, T item) {
this(converter.apply(item));
}
static final Function<Log, String> LOG_CONVERTER
= LogConverter.INSTANCE::convert;
public Payload(Log log) {
<Log> this(LOG_CONVERTER, log);
}
}
We added an invocation to the other constructor in the same class. It supplies a type argument to it:
public Payload(Log log) {
<Log> this(LOG_CONVERTER, log);
}
This means that the following code does not compile:
public Payload(Log log) {
<LocalDate> this(LOG_CONVERTER, log);
// error ^^^ ^^^
}
As the compiler tries to match the actual arguments to a "virtual" constructor having the following signature:
public Payload(Function<LocalDate, String> converter, LocalDate item);
As the types are not compatible, compilation fails.
Caveat with new
keyword and diamond form
Section 15.9 of the JLS has the following in bold:
It is a compile-time error if a class instance creation expression provides type arguments to a constructor but uses the diamond form for type arguments to the class.
Let's investigate. Here's a small Java program:
public class Caveat<T> {
public <E> Caveat(T t, E e) {}
public static void main(String[] args) {
var t = LocalDate.now();
var e = "abc";
new <String> Caveat<>(t, e);
}
}
The class Caveat
is generic on <T>
. It declares a single constructor which is generic on <E>
. In the main
method it tries to create a new instance of the Caveat
class.
Let's compile it:
$ javac src/main/java/iter3/Caveat.java
src/main/java/iter3/Caveat.java:17: error: cannot infer type arguments for Caveat<T>
new <String> Caveat<>(t, e);
^
reason: cannot use '<>' with explicit type parameters for constructor
where T is a type-variable:
T extends Object declared in class Caveat
1 error
Here's the explanation from the JLS:
This rule is introduced because inference of a generic class's type arguments may influence the constraints on a generic constructor's type arguments.
To be honest, I was not able to understand it. In any case, to fix the compilation error we replace the diamond form:
new <String> Caveat<LocalDate>(t, e);
With an explicit <LocalDate>
. The code now compiles.
Conclusion
In this blog post we discussed a few topics on generic constructors. A feature of the Java language I did not know until recently.
We have seen how it is rarely used in the JDK source code. Is it safe to extrapolate and say that it is rarely used in general? I personally believe it is. But don't take my word for it.
We then saw an example exercising a possible use-case.
Finally, we saw how to invoke generic constructors using:
-
new
keyword; and -
this
keyword (which can be equally applied to thesuper
keyword).
The source code of the examples in this post can be found in this GitHub repository.
Originally published at the Objectos Software Blog on July 18th, 2022.
Follow me on twitter.
Top comments (0)