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

3

20

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?

Katar answered 16/4, 2021 at 13:25 Comment(3)
It's called a canonical constructorSoilure
Working on support for records in jOOQ, eh? :)Mezuzah
@Lii: Yes :) It's already done and ready for jOOQ 3.15 due in Q2 2021: github.com/jOOQ/jOOQ/issues/11778. jOOQ always supported records, but only when passing arguments by index, not by name. Passing by name will now be possible as well, without having to resort to @ConstructorProperties or javac -parametersKatar
V
22

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]
Victoir answered 16/4, 2021 at 14:26 Comment(2)
Nice! 2 improvements I can think of: All records extend Record, so you could prevent some runtime errors by adding a constraint to the generic type. If you do that, then NoSuchMethodException is effectively impossible, since all records must have a canonical constructor. So for the purpose of a convenient library method, I'd catch NoSuchMethodException and rethrow as a runtime exception.Allonge
This corresponds to what is currently being considered as an addition to the Javadoc, following a discussion on twitter, so since this will be the recommended way going forward, I'll accept this answer.Katar
K
10

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.

Katar answered 16/4, 2021 at 13:25 Comment(1)
Not lame at all. Because getRecordComponents is guaranteed to return the components in the canonical order, and the canonical constructor is guaranteed to be there (unless an off-label compiler generates classfiles that don't adhere to the language spec), this gives you exactly what you want. The primary purpose of the reflection API is to reflect what is in the classfile; the canonical constructor of a record has no marking in the classfile, but the record components do.Newfoundland
A
1

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

Allonge answered 16/4, 2021 at 14:3 Comment(7)
Regarding the last constructor: "The elements in the arrayreturned are not sorted and are not in any particular order". In particular, even if you find the constructors in some order in byte code, reflection tends to change things again, and this is even JVM specific.Katar
@LukasEder An even stronger reason not to rely on it then. But just because it says that there is no particular order doesn't mean it's not deterministic. They are clearly not going to be shuffling the array every time you call it to keep you on your toes. So that line does not preclude optimizing based on the observation of patterns. It's basically just making it explicit that they reserve the right to alter the order for whatever reason. It doesn't mean they will exercise that right.Allonge
And by "clearly" you mean, OMG they might just do it??Katar
@LukasEder Nice. I forgot about that. But they're not about to go through every method in the JDK and retrospectively shuffle everything without a guaranteed iteration order, so I think my point still stands. The optimization I suggested would currently be better, and even if they introduced a shuffle would be no worse than yours.Allonge
Note that the canonical constructor is the only one whose MethodParameters are available, which is a strong hint. Of course, the hint is gone when compiling when -parameters to make them available for all constructors. But when processing bytecode, you could concatenate all descriptors of the Record attribute and have the descriptor of the canonical constructor.Consecrate
Sure, that could be an micro optimisation for current JDKs. In my case, it wouldn't be too important, because all the reflection lookup logic is cached. And these things do change (I had to ask) for reasons other than shuffling things on purpose, so as with many such micro optimisations, the question is: Is it really worth it?Katar
It is definitely a bad idea to rely on the order in which members are returned from getDeclaredConstructors and friends. On the other hand, the order in which record components are returned in getRecordComponents is guaranteed, so you can rely on that, as in https://mcmap.net/q/618532/-is-there-a-way-to-recognise-a-java-16-record-39-s-canonical-constructor-via-reflection.Newfoundland

© 2022 - 2024 — McMap. All rights reserved.