Generic Constructors
本文最后更新于:2022年8月4日 中午
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:
1 |
|
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:
1 |
|
As OpenType<Foo>
is compatible with Foo
. However, the following code fails to compile:
1 |
|
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:
1 |
|
So, if we were to send a “Hello world!” message over the wire, we could invoke a hypothetical send
method like so:
1 |
|
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:
1 |
|
That’s great. Next, let’s add a little complexity to our data.j
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:
1 |
|
This data is structured in the sense that its JSON format is defined by the following converter:
1 |
|
To send our log record we could just:
1 |
|
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:
1 |
|
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:
1 |
|
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:
1 |
|
The compiler tries to match the actual arguments to a “virtual” constructor having the following signature:
1 |
|
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
; - 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:
1 |
|
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:
1 |
|
We added an invocation to the other constructor in the same class. It supplies a type argument to it:
1 |
|
This means that the following code does not compile:
1 |
|
As the compiler tries to match the actual arguments to a “virtual” constructor having the following signature:
1 |
|
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:
1 |
|
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:
1 |
|
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.