DEV Community

Marcio Endo
Marcio Endo

Posted on • Originally published at objectos.com.br

Things I didn't know about Java: Generic Constructors

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")));
Enter fullscreen mode Exit fullscreen mode

Would generate the following Java statement:

var foo = new Foo(1, "abc");
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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

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:

public <T> OpenMBeanParameterInfoSupport(
  String name, String description,
  OpenType<T> openType, T defaultValue)
  throws OpenDataException
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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                      ^^^
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

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!"));
Enter fullscreen mode Exit fullscreen mode

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!"
}
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

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());
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
  """);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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           ^^^                 ^^^
Enter fullscreen mode Exit fullscreen mode

The compiler tries to match the actual arguments to a "virtual" constructor having the following signature:

public Payload(Function<Category, String> converter, Category item);
Enter fullscreen mode Exit fullscreen mode

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] ) ;
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

This means that the following code does not compile:

public Payload(Log log) {
  <LocalDate> this(LOG_CONVERTER, log);
  // error         ^^^            ^^^
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

Originally published at the Objectos Software Blog on July 18th, 2022.

Follow me on twitter.

Top comments (0)