Java - Parameterized Generic Record Examples

This tutorial explains how to create a generic record with parameterized types in Java.

You may already know that Java supports generic parameterized classes and methods. In Java 14, there is a new type of class called record. Just like conventional classes, records also support parameterized types. While you can use Object as a field type, it's not a good practice since you will need to cast the Object based on its type which requires more effort. In addition, generic types also help to make sure that the passed values are correct according to what you write when defining a variable or a return type. Another advantage of creating a generic record is you can reuse it for various data types. The concept is basically the same as a generic class. Below are some usage examples.

Create Generic Record

To create a generic record, add <> after the record name. Inside the <>, add the list of parameterized types. Then, you can use each defined type parameter as the type of one or more fields.

  record Pair<T>(T x, T y) {}

To create variables with the above record, pass the actual types inside <>. The arguments passed to the constructor must comply with the corresponding types defined in <>. Below are correct examples.

  Pair<Integer> intPair = new Pair<>(1, 2);
  Pair<Double> doublePair = new Pair<>(1.5, 2.5);

Below is another example where a value passed as the argument doesn't match the type.

  Pair<Integer> intPair = new Pair<>(1, 2.5);

As a result, the error below will be thrown at compile time.

  error: incompatible types: cannot infer type arguments for Pair<>
Pair<Integer> intPair = new Pair<>(1.5, 2.5);
^ reason: inference variable T has incompatible bounds equality constraints: Integer lower bounds: Double where T is a type-variable: T extends Object declared in record Pair

You can also have more than one parameterized type.

  record PairWithScore<T, U>(T x, T y, U score) {}    

  PairWithScore<Integer, Double> pairWithScore = new PairWithScore<>(1, 2, 100.0);

Bounded Type Parameters

It's also possible to only allow certain types to be passed as parameterized types. For example, we have a Point record and we want to restrict that the type must be a Number. The solution is to add extends Number after the parameterized type.

  record Point<T extends Number>(T x, T y) {}

  Point<Integer> intPoint = new Point<>(1, 2);
  Point<Double> doublePoint = new Point<>(1.5, 2.5);

Below is an example that doesn't work because the String type is not a sub-type of Number.

  Point<String> stringPoint = new Point<>("a", "b");

It will throw the following error at compile time.

  error: type argument String is not within bounds of type-variable T
      Point<String> stringPoint = new Point<>("a", "b");
            ^
    where T is a type-variable:
      T extends Number declared in record Point
  /home/ivan/IdeaProjects/java/src/main/java/com/woolha/example/record/GenericRecordExample.java:22: error: cannot infer type arguments for Point<>
      Point<String> stringPoint = new Point<>("a", "b");
                                  ^
    reason: inference variable T has incompatible bounds
      upper bounds: Number
      lower bounds: String
    where T is a type-variable:
      T extends Number declared in record Point

Get Generic Type

If the parameterized type is only used as the field type, you can obtain the type by getting the class of the field. Access the field's accessor method, then call the getClass method.

  System.out.println(doublePoint.x().getClass());

If the parameterized type is not for the field type, you may need a little workaround. Look at the example below where the parameterized type is used on a method argument.

  record Printable<T>(int value) {

    public void printValue(T prefix) {
      System.out.println(prefix.toString() + value);
    }
  }

  Printable<String> printable = new Printable<>(1);

The easiest workaround is by adding a Class field to the record. Then, access the Class and call getTypeName. You need to pass the parameterized type as the Class's type to make sure that the type is always the same.

  record Printable<T>(int value, Class<T> clazz) {

    public void printValue(T prefix) {
      System.out.println(prefix.toString() + value);
    }
  }

  Printable<String> printable = new Printable<>(1, String.class);
  System.out.println(printable.clazz().getTypeName());

Summary

In this tutorial, we have seen the examples of how to make a generic record. In general, it's very similar to a generic class. You can define multiple parameterized types and use bounded-type parameters.

You can also read about: