Question about records and overriding their "magic methods"

forax at univ-mlv.fr forax at univ-mlv.fr
Sat Sep 23 06:53:18 UTC 2023


> From: "David Alayachew" <davidalayachew at gmail.com>
> To: "Remi Forax" <forax at univ-mlv.fr>
> Cc: "Brian Goetz" <brian.goetz at oracle.com>, "amber-dev" <amber-dev at openjdk.org>
> Sent: Thursday, September 21, 2023 8:40:05 PM
> Subject: Re: Question about records and overriding their "magic methods"

> Hello Rémi,

> Thank you for your response!

> > If you really really want to do it, you can use the
> > bootstrap method in java.lang.runtime.ObjectsMethods but
> > it's ugly.

> That's actually way better than I thought it would be. It's surprisingly direct
> too.

> > We also do not want to close the door to supporting
> > super.x() if at some point in the future if the VM spec
> > is changed to introduce a way to generate bridge methods
> > at runtime.

> > The record methods toString/equals/hashCode will be
> > declare as abstract bridge method in that case.

> I brushed up on bridge methods for this response - [
> https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html |
> https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html ]

> I get the concept, but I am curious what having that at runtime would give us
> that we don't already get at compile time. I don't quite see it.

The idea is to use a method handle to implement those dynamically. Anyway, thinking a little bit more, there is perhaps a simpler solution. 

Let state the issue, toString/equals/hashCode are abstract in Record, so you can not call super.toString(). 
If we want to support that, it means that either as Brian said we invent a way to ask for a default implementation provided by the compiler with a new syntax or we make toString/equals/hashCode non-abstract and we provide a code for them. 

As Brian said, the former is too adhoc and the latter run into the fact that while we can provide generic implementations inside java.lang.Record, those implementations will be slow because inside java.lang.Record we need to dynamically ask what is the actual record and then call a generic code with that record class. 

Another possible solution is to implement Record.equals/toString/hashCode to throw an exception saying they are magic methods and to re-route all access to use an invokedynamic instead. 
So 
- the compiler should trap the calls to super.toString()/equals()/hashCode() and generate an invokedynamic instead. 
- the VM should trap reflection calls and method handle creations to use a method handle produced by ObjectMethods.bootstrap() instead. 

For me, that's a lot of engineering for a use case that will be used once in a while. 

> Thank you for your time and help!
> David Alayachew

Regards, 
Rémi 

PS: we may be able in the future when specialized generics will be landed to re-visit that issue because we will get a way to have static method code specialized by a constant record class but this will still require a lot of engineering, i am afraid. 

> On Thu, Sep 21, 2023 at 1:49 PM Remi Forax < [ mailto:forax at univ-mlv.fr |
> forax at univ-mlv.fr ] > wrote:

>>> From: "Brian Goetz" < [ mailto:brian.goetz at oracle.com | brian.goetz at oracle.com ]
>>> >
>>> To: "David Alayachew" < [ mailto:davidalayachew at gmail.com |
>>> davidalayachew at gmail.com ] >, "amber-dev" < [ mailto:amber-dev at openjdk.org |
>>> amber-dev at openjdk.org ] >
>>> Sent: Thursday, September 21, 2023 7:18:13 PM
>>> Subject: Re: Question about records and overriding their "magic methods"

>>> We explored this topic somewhat when designing the feature.

>>> When you override a method x, you can still delegate (via super.x()) to the
>>> thing you override. We considered doing the same here (delegating to
>>> default.x()), but concluded that this failed the power-to-weight-ratio test,
>>> because usually you do not want to _refine_ an equals/toString/hashCode
>>> calculation, but instead broaden it (such as comparing arrays by contents
>>> rather than by ==.)

>>> If you pull on this "string" a bit, the API that you would want here is complex
>>> and enormous, and has as many tuning knobs as a Lombok. So refactoring records
>>> that customize Object methods becomes a higher-responsibility activity. Leave
>>> "future you" some comments for what you were thinking today.
>> If you really really want to do it, you can use the bootstrap method in
>> java.lang.runtime.ObjectsMethods but it's ugly.

>> public class CallingRecordToStringDemo {

>> record Foo ( String blah, int value) {
>> private static final MethodHandle TO_STRING ;
>> static {
>> var lookup = MethodHandles . lookup ();
>> var components = Foo . class .getRecordComponents();
>> CallSite callsite ;
>> try {
>> callsite = ( CallSite ) ObjectMethods . bootstrap ( lookup ,
>> "toString" ,
>> MethodType . methodType ( String . class , Foo . class ),
>> Foo . class ,
>> Arrays . stream ( components ).map( RecordComponent ::getName).collect(
>> Collectors . joining ( ";" )),
>> Arrays . stream ( components ).map(c -> {
>> try {
>> return lookup .unreflect(c.getAccessor());
>> } catch ( IllegalAccessException e) {
>> throw new AssertionError(e);
>> }
>> }).toArray( MethodHandle []:: new ));
>> } catch ( Throwable e) {
>> throw new AssertionError(e);
>> }
>> TO_STRING = callsite .dynamicInvoker();
>> }

>> @Override
>> public String toString () {
>> try {
>> return ( String ) TO_STRING .invokeExact( this );
>> } catch ( Error e) {
>> throw e;
>> } catch ( Throwable e) {
>> throw new AssertionError(e);
>> }
>> }
>> }

>> public static void main ( String [] args) {
>> System . out .println( new Foo( "blah" , 42 ));
>> }
>> }

>> We also do not want to close the door to supporting super.x() if at some point
>> in the future if the VM spec is changed to introduce a way to generate bridge
>> methods at runtime.
>> The record methods toString/equals/hashCode will be declare as abstract bridge
>> method in that case.

>> regards,
>> Rémi

>>> On 9/21/2023 1:00 PM, David Alayachew wrote:

>>>> Hello Amber Dev Team,

>>>> Let's say I have a record like the following.

>>>> record ComplexRecord(int blah /* and many more components */) {}

>>>> Next, let's say that I want to override my toString, to include some derived
>>>> information.

>>>> Obviously, putting the derived info into the toString is easy, but how do I go
>>>> about INCLUDING it with my original implementation of toString that was
>>>> magically created for me by Java?

>>>> If I try to fully recreate my toString, I run the risk of it becoming
>>>> out-of-date upon refactor. Best I can come up with is nesting another record
>>>> with the toString overloaded. Also not ideal.

>>>> Thank you for your time!
>>>> David Alayachew
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20230923/5a828b0c/attachment.htm>


More information about the amber-dev mailing list