Search code examples
javareflectionjava-15

Java record constructor invisible through reflection


I'm playing with Java 15's new records feature, and how it interacts with reflection. I've run into some strange behaviour, where I can sometimes access a record's constructor via reflection, and sometimes not. For example, given the following Java file:

Recording.java:

public class Recording {
    public static void main(String[] args) {
        System.out.println("Constructors: " + MainRecord.class.getConstructors().length);
        System.out.println("Methods: " + MainRecord.class.getDeclaredMethods().length);
    }

    record MainRecord(int i, String s) {}
}

This behaves as follows:

❯ javac --enable-preview --release 15 Recording.java
Note: Recording.java uses preview language features.
Note: Recompile with -Xlint:preview for details.
❯ java --enable-preview Recording
Constructors: 0
Methods: 5

In other words, the call to getConstructors() does not find any constructors (while the call to `getDeclaredMethods() does find methods). I don't understand why not, because the constructor does exist:

❯ javap Recording\$MainRecord
Compiled from "Recording.java"
final class Recording$MainRecord extends java.lang.Record {
  Recording$MainRecord(int, java.lang.String);
  public final java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public int i();
  public java.lang.String s();
}

(Putting the record in a separate Java file gives the same results.)

However, if I do the same from JShell:

❯ jshell --enable-preview
|  Welcome to JShell -- Version 15
|  For an introduction type: /help intro

jshell> record JShellRecord(int i, String s) {}
|  created record JShellRecord

jshell> JShellRecord.class.getConstructors().length
$2 ==> 1

So, now it does find the constructor.

Here's the Java version I'm using:

❯ java -version
openjdk version "15" 2020-09-15
OpenJDK Runtime Environment AdoptOpenJDK (build 15+36)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 15+36, mixed mode, sharing)

Compiling and running the same program from Java 14 does work:

❯ java -version
openjdk version "14.0.2" 2020-07-14
OpenJDK Runtime Environment AdoptOpenJDK (build 14.0.2+12)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 14.0.2+12, mixed mode, sharing)
❯ javac --enable-preview --release 14 Recording.java
Note: Recording.java uses preview language features.
Note: Recompile with -Xlint:preview for details.
❯ java --enable-preview Recording
Constructors: 1
Methods: 5

I know that in Java 15, compared to Java 14, a number of restrictions have been put in place regarding reflection on records, but if I read the JEP correctly, those only apply to modification. Finding (and perhaps invoking) a constructor does not seem to apply.

Can anyone tell my what's going on here? What do I need to do to see a record's constructor in Java 15 through reflection?


Solution

  • getConstructors() returns public constructors only. Use getDeclaredConstructors() to get all constructors.

    Your declaration record MainRecord(int i, String s) {} lacks the public modifier, so it creates a non-public class and also a non-public constructor. See JLS15-preview, §8.10.4

    The implicitly declared canonical constructor has the same access modifier as the record class R, unless the record class lacks an access modifier, in which case the canonical constructor has package access

    This does indeed differ from the JDK 14 preview. The beginning of the JDK 15 preview document says:

    The changes are the same as those in the first preview of Records in Java SE 14, except for the following:

    • 8.10.4 Removed requirement that canonical constructor must be public. Any access modifier must provide at least as much access as the record class. If a canonical constructor is implicitly declared, then its access modifier is the same as the record class.

    It seems, top level classes created in JShell are implicitly public.

    > jdk-15\bin\jshell --enable-preview
    |  Welcome to JShell -- Version 15
    |  For an introduction type: /help intro
    
    jshell> record JShellRecord(int i, String s) {}
    |  created record JShellRecord
    
    jshell> JShellRecord.class.getConstructors()[0]
    $2 ==> public JShellRecord(int,java.lang.String)
    
    jshell> java.lang.reflect.Modifier.isPublic(JShellRecord.class.getModifiers())
    $3 ==> true
    
    jshell>