Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 361
jqno Avatar asked Sep 21 '20 14:09

jqno


People also ask

How to instantiate an object using reflection in Java?

As you can see, we are able to instantiate object using java.lang.reflect.Constructor at run time using reflection. In case, you have constructor parameters with primitive types, you can use .class suffix with that. If the parameter type is of int, then you can use int.class while getting constructor of that type.

What is reflective constructor in Java?

It also displays whether the constructor is synthetic (compiler-generated) or of variable arity. There are two reflective methods for creating instances of classes: java.lang.reflect.Constructor.newInstance () and Class.newInstance ().

Is it possible to use @jailbreak for reflection in Java?

For hidden class access, Java's annotation grammar requires the class to be annotated apart from its package. More generally you can use @Jailbreak for any type of reflection:

What is a public constructor in Laravel?

Using records, a public constructor – with an argument for each field – is generated for us. In the case of our Person record, the equivalent constructor is: This constructor can be used in the same way as a class to instantiate objects from the record:


Video Answer


1 Answers

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>
like image 143
Holger Avatar answered Nov 09 '22 06:11

Holger