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):

While three of the classes are internal:

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
2
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:

1
2
3
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:

1
2
3
4
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:

1
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:

1
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:

1
2
3
{
"data": "Hello world!"
}

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
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
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:

1
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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:

1
2
3
4
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:

1
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;
  • 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
2
3
4
5
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
public Payload(Log log) {
<Log> this(LOG_CONVERTER, log);
}

This means that the following code does not compile:

1
2
3
4
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:

1
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
$ 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 the super keyword).

The source code of the examples in this post can be found in this GitHub repository.

References


Generic Constructors
https://baymax55.github.io/2022/09/13/java/GenericConstructors/
作者
baymax55
发布于
2022年9月13日
许可协议