21

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?

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • 5
    It's called a _canonical_ constructor – ZhekaKozlov Apr 16 '21 at 14:13
  • 2
    Working on support for records in jOOQ, eh? :) – Lii Apr 26 '21 at 11:36
  • 2
    @Lii: Yes :) It's already done and ready for jOOQ 3.15 due in Q2 2021: https://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 -parameters` – Lukas Eder Apr 27 '21 at 07:10

3 Answers3

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]
Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
  • 3
    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. – Michael Apr 16 '21 at 14:33
  • 5
    This corresponds to what is currently being considered [as an addition to the Javadoc](https://github.com/openjdk/jdk/pull/3556), following [a discussion on twitter](https://twitter.com/tagir_valeev/status/1383343671043522564), so since this will be the recommended way going forward, I'll accept this answer. – Lukas Eder Apr 17 '21 at 09:12
11

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.

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • 6
    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. – Brian Goetz Apr 18 '21 at 15:55
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

Michael
  • 41,989
  • 11
  • 82
  • 128
  • Regarding the last constructor: [*"The elements in the arrayreturned are not sorted and are not in any particular order"*](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/Class.html#getDeclaredConstructors()). 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. – Lukas Eder Apr 16 '21 at 14:05
  • @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. – Michael Apr 16 '21 at 14:10
  • And by "clearly" you mean, [OMG they might just do it??](https://twitter.com/stuartmarks/status/1365473376727261185) – Lukas Eder Apr 16 '21 at 14:18
  • @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. – Michael Apr 16 '21 at 14:26
  • 1
    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. – Holger Apr 16 '21 at 17:22
  • 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)](https://twitter.com/stuartmarks/status/1383089478315642893) for reasons other than shuffling things on purpose, so as with many such micro optimisations, the question is: Is it really worth it? – Lukas Eder Apr 17 '21 at 09:10
  • 4
    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://stackoverflow.com/a/67126110/3553087. – Brian Goetz Apr 18 '21 at 15:54