Is there a way to recognise a Java 16 record's canonical constructor via reflection?

3.1k views Asked by At

Assuming I have a record like this (or any other record):

record X(int i, int j) {
    X(int i) {
        this(i, 0);
    }
    X() {
        this(0, 0);
    }
    X(String i, String j) {
        this(Integer.parseInt(i), Integer.parseInt(j));
    }
}

Is there a way to find this record's canonical constructor via reflection, i.e. the one that is implicitly declared in the RecordHeader?

3

There are 3 answers

2
AudioBubble On BEST ANSWER

Try this

static <T extends Record> Constructor<T> canonicalConstructorOfRecord(Class<T> recordClass)
        throws NoSuchMethodException, SecurityException {
    Class<?>[] componentTypes = Arrays.stream(recordClass.getRecordComponents())
        .map(rc -> rc.getType())
        .toArray(Class<?>[]::new);
    return recordClass.getDeclaredConstructor(componentTypes);
}

And

Constructor<X> c = canonicalConstructorOfRecord(X.class);
X x = c.newInstance(1, 2);
System.out.println(x);

Output

X[i=1, j=2]
1
Lukas Eder On

This seems to work, though it's a bit lame:

List<?> componentTypes = Stream
    .of(X.class.getRecordComponents())
    .map(RecordComponent::getType)
    .toList();

for (Constructor<?> c : X.class.getDeclaredConstructors())
    if (Arrays.asList(c.getParameterTypes()).equals(componentTypes))
        System.out.println(c);

Printing

Test$1X(int,int)

I'm still open to better suggestions.

7
Michael On

The bytecode doesn't seem to have any indication of such.

Without anything in the bytecode to indicate this, the only other alternative would be something in the reflection API which was specifically added for this purpose, e.g. a getCanonicalConstructor method which works via inference, exactly like your solution of checking the argument types does. There wasn't anything like that added, though.

In my experiments, the primary constructor always occurs last so it would probably work if you just took the last element of getDeclaredConstructors(), but you can't rely on that since it's an implementation detail. (Maybe as a performance optimization you might decide to use that information to change your implementation to iterate through the list backwards though)

Javap output is below. For the purpose of brevity I just kept the X(String i, String j) and removed the other 2. I've removed some of the method implementations which should be plainly irrelevant even if you're not familiar with the format.

Classfile /tmp/5610502834030542116/classes/X.class
  Last modified Apr 16, 2021; size 1555 bytes
  SHA-256 checksum fe06254f15d68f71f0a576d1ce19c28c2d4b9479c3b16dadc8c0e69e6ab734c4
  Compiled from "Main.java"
final class X extends java.lang.Record
  minor version: 0
  major version: 60
  flags: (0x0030) ACC_FINAL, ACC_SUPER
  this_class: #8                          // X
  super_class: #2                         // java/lang/Record
  interfaces: 0, fields: 2, methods: 7, attributes: 4
Constant pool:
{
  private final int i;
    descriptor: I
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  private final int j;
    descriptor: I
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  X(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=3, locals=3, args_size=3
        start local 0 // X this
        start local 1 // java.lang.String i
        start local 2 // java.lang.String j
         0: aload_0
         1: aload_1
         2: invokestatic  #16                 // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I
         5: aload_2
         6: invokestatic  #16                 // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I
         9: invokespecial #22                 // Method "<init>":(II)V
        12: return
        end local 2 // java.lang.String j
        end local 1 // java.lang.String i
        end local 0 // X this
      LineNumberTable:
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   LX;
            0      13     1     i   Ljava/lang/String;
            0      13     2     j   Ljava/lang/String;

  X(int, int);
    descriptor: (II)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=3
        start local 0 // X this
        start local 1 // int i
        start local 2 // int j
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield      #7                  // Field i:I
         9: aload_0
        10: iload_2
        11: putfield      #13                 // Field j:I
        14: return
        end local 2 // int j
        end local 1 // int i
        end local 0 // X this
      LineNumberTable:
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LX;
            0      15     1     i   I
            0      15     2     j   I
    MethodParameters:
      Name                           Flags
      i
      j

  public final java.lang.String toString();
    ...toString

  public final int hashCode();
    ...hashCode

  public final boolean equals(java.lang.Object);
    ...equals

  public int i();
    ...getter

  public int j();
    ...getter
}
SourceFile: "Main.java"
Record:
  int i;
    descriptor: I

  int j;
    descriptor: I

BootstrapMethods:
  0: #54 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
    Method arguments:
      #8 X
      #61 i;j
      #63 REF_getField X.i:I
      #64 REF_getField X.j:I
InnerClasses:
  public static final #70= #66 of #68;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles