6

Consider this class:

public class Handler
{
    private Supplier<Foo> foo;

    public void handle( Bar bar )
    {
        foo = () -> bar.getFoo();
    }
}

And consider this reflection snippet which wants to access the handle() method.

for( Method method : Handler.class.getDeclaredMethods() )
{
    if ( method.getParameterCount() == 1 && Bar.class.isAssignableFrom( method.getParameterTypes()[0] ) )
    {
        // This is the method you are looking for
    }
}

Instead of finding

  • public void Handler.handle(Bar)

It finds

  • private Foo Handler.lambda$3(Bar)

Which obviously then throws the Exception:

java.lang.IllegalAccessException: class HandlerService cannot access a member of class Handler with modifiers "private static"

Can someone explain what is going on here, please?

It looks like Java considers the lambda inside the method as a top-level declared method. Is this new (or even a bug) in Java 11 ?

Stewart
  • 17,616
  • 8
  • 52
  • 80
  • 2
    Java desuggars Lambdas by putting the code into their own private method (static if it doesn't reference `this` directly or indirectly) with the parameters of the bound variables, followed by the parameters of the interface in question. – Johannes Kuhn Apr 06 '20 at 09:56
  • 1
    For your question: Use `getMethods()`. And maybe check the return type too. – Johannes Kuhn Apr 06 '20 at 09:57
  • @JohannesKuhn OK, but is a lambda a DECLARED method ? Why is this getMethods() better? With enough supporting explanation, this could be an answer – Stewart Apr 06 '20 at 10:06
  • @user85421 Thank you. Didn't know this. – Stewart Apr 06 '20 at 10:08
  • @Stewart, can we see the Bar and HandlerService? –  Apr 06 '20 at 11:47
  • @AhmetOZKESEK HandlerService contains the code snippet which uses `getDeclaredMethods()` and Bar is just a pure data object – Stewart Apr 06 '20 at 11:55
  • 3
    No, this is not new to Java 11. Lambdas were always compiled that way since they are supported in Java, i.e. Java 8. The possibility to encounter synthetic methods when iterating non-public methods exists since Java 1.1. – Holger Apr 06 '20 at 12:44
  • 1
    @Stewart, right, I did some test with my own. In addition to Holger's point, when we use a lambda expression it compiles and puts a private static method for the lambda, but when we use a method reference it does not put. –  Apr 06 '20 at 14:11
  • @AhmetOZKESEK Wow?! For real?! Using `bar::getFoo` instead of `() -> bar.getFoo()` respond differently?! This needs to be an answer. – Stewart Apr 06 '20 at 14:17
  • 1
    @Stewart you can see yourself what is generating, by using a [special start-up flag](https://stackoverflow.com/a/47469864/1059372) or [this for example](https://stackoverflow.com/a/45393978/1059372) – Eugene Apr 06 '20 at 15:22
  • 1
    Method references do not use synthetic methods, in the typical case. In a few corner cases, e.g. involving intersection types, vararg invocations, or super calls, method references may still get compiled using a synthetic method like a lambda expression. – Holger Apr 07 '20 at 12:17

1 Answers1

3

You have to be careful with assumptions about the members of the compiled class.

There are even compiler-generated members which are part of the accessible API, like the default constructor or the values() and valueOf(String) methods of enum types. Further, compiled constructors of inner classes and enum types may have more parameters than visible in the source code and due to type erasure, the signatures in the compiled methods may differ from the source code.

Besides that, there can be different synthetic members. From Java 1.1 to Java 10, nested classes may access each others private members via synthetic helper methods (which became obsolete with Java 11). Also, overriding methods of generic classes or using covariant return types may cause the generation of a synthetic bridge method.

And that’s still not all.

The following program

import java.util.Arrays;
import java.util.stream.Stream;

public enum ShowSyntheticMembers {
    ;
    public static void main(String[] args) {
        Stream.of(ShowSyntheticMembers.class, Inner.class)
            .flatMap(cl -> Stream.concat(Arrays.stream(cl.getDeclaredFields()),
                                         Arrays.stream(cl.getDeclaredMethods())))
            .forEach(System.out::println);
    }
    private boolean x;
    class Inner {
        protected String clone() {
            assert x;
            return "";
        }
    }
}

prints when compiled with JDK 11:

private boolean ShowSyntheticMembers.x
private static final ShowSyntheticMembers[] ShowSyntheticMembers.$VALUES
public static void ShowSyntheticMembers.main(java.lang.String[])
public static ShowSyntheticMembers[] ShowSyntheticMembers.values()
public static ShowSyntheticMembers ShowSyntheticMembers.valueOf(java.lang.String)
private static void ShowSyntheticMembers.lambda$main$1(java.io.PrintStream,java.lang.Object)
private static java.util.stream.Stream ShowSyntheticMembers.lambda$main$0(java.lang.Class)
static final boolean ShowSyntheticMembers$Inner.$assertionsDisabled
final ShowSyntheticMembers ShowSyntheticMembers$Inner.this$0
protected java.lang.String ShowSyntheticMembers$Inner.clone()
protected java.lang.Object ShowSyntheticMembers$Inner.clone() throws java.lang.CloneNotSupportedException

while compiling and running with JDK 8 yields

private boolean ShowSyntheticMembers.x
private static final ShowSyntheticMembers[] ShowSyntheticMembers.$VALUES
public static void ShowSyntheticMembers.main(java.lang.String[])
public static ShowSyntheticMembers[] ShowSyntheticMembers.values()
public static ShowSyntheticMembers ShowSyntheticMembers.valueOf(java.lang.String)
static boolean ShowSyntheticMembers.access$000(ShowSyntheticMembers)
private static java.util.stream.Stream ShowSyntheticMembers.lambda$main$0(java.lang.Class)
static final boolean ShowSyntheticMembers$Inner.$assertionsDisabled
final ShowSyntheticMembers ShowSyntheticMembers$Inner.this$0
protected java.lang.String ShowSyntheticMembers$Inner.clone()
protected java.lang.Object ShowSyntheticMembers$Inner.clone() throws java.lang.CloneNotSupportedException
  • $VALUES is an artifact of the compiler generated values() implementation.
  • $assertionsDisabled part of the implementation of the assert statement.
  • this$0 is the inner class’ implicit reference to its outer this.
  • The clone() method with the return type Object, is a bridge method.
  • The access$000 method helps to access the private field of the outer class from the inner class, which is needed prior to JDK 11.
  • Interestingly, the synthetic method lambda$main$1, which only exists in the JDK 11 compiled version is part of the System.out::println method reference, but actually not needed here.
    It’s a side effect of a fix for certain intersection type related issues, hence, very compiler-specific. Changing .flatMap(…) to .<Object>flatMap(…) in the source code would make the method disappear even with this specific compiler version.

So, since a lot of factors determine the presence of synthetic members not visible in the source code, you should not search for a particular method by only using the parameter type as criteria.

When you want to access the public members, you better use Handler.class.getMethods() instead of Handler.class.getDeclaredMethods(). Or use Handler.class.getMethod("handle", Bar.class) to directly get the intended method.

If you don’t want to hardcode the method name as a string, a runtime visible annotation could help to identify the right method.

Holger
  • 285,553
  • 42
  • 434
  • 765