Lambda私有方法的可访问性

And*_*niy 4 java reflection lambda jls java-8

我对以下情况感到困惑。

考虑两个包ab具有以下类:

1)MethodInvokercall()在给定对象上调用:

package b;
import java.util.concurrent.Callable;
public class MethodInvoker {
    public static void invoke(Callable r) throws Exception {
        r.call();
    }
}
Run Code Online (Sandbox Code Playgroud)

2)

package a;
import b.MethodInvoker;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class Test {

    private static Void method() {
        System.out.println("OK");
        return null;
    }

    public static void main(String[] args) throws Exception {
        Method method = Test.class.getDeclaredMethod("method");
        method.invoke(null);        // ok

        // TEST 1
        MethodInvoker.invoke(() -> {
            return method.invoke(null);  // ok (hmm....
        });

        // TEST 2
        MethodInvoker.invoke(new Callable() {
            @Override
            public Object call() {
                return method();        // ok (hm...???
            }
        });

        // TEST 3
        MethodInvoker.invoke(new Callable() {
            @Override
            public Object call() throws Exception {
                return method.invoke(null); // throws IllegalAccessException, why???

            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

我明确地将其method() 私有化,以测试如何在Test类范围之外调用它。我通常对这三个案例感到困惑,因为我发现它们是矛盾的。我通常希望他们所有人都应该以相同的方式工作。至少,我希望如果TEST 3抛出IllegalAccessException,那么TEST 2应该也这样做。但是TEST 2工作正常!

有人可以根据JLS给出严格的解释,为什么每种情况都起作用呢?

k5_*_*k5_ 5

TEST1和TEST3之间的差异归结为lambda和匿名类的实现方式之间的差异。

查看这些特殊情况的实际字节码总是很有趣。 https://javap.yawk.at/#jXcoec

TEST1 lambda:

Lambda表达式将转换为定义的类中的方法。传递了对该方法的方法引用。由于lambda方法是该类的一部分,因此可以直接访问该类的私有方法。method.invoke()作品。

TEST3匿名类:

匿名类将转换为类。在该类中调用method.invoke(),该类不应访问私有方法。由于反射,使用合成方法的工作环境无法正常工作。

TEST2:为了允许嵌套类访问其外部类的私有成员,引入了综合方法。如果您查看字节码,将会看到带有签名的方法,该方法static java.lang.Void access$000();将调用转发给Void method()


Hol*_*ger 5

关于语言层面的可访问性,在JLS §6.6.1, Determining Accessibility 中有一个直接的声明:

  • 否则,声明成员或构造函数private,当且仅当它发生在包含成员或构造函数声明的顶级类(第7.6 节)的主体内时,才允许访问。

由于所有嵌套类和 lambda 表达式都驻留在同一个“顶级类的主体”中,这已经足以解释访问的有效性。

但是无论如何,lambda 表达式与内部类有着根本的不同:

JLS §15.27.2,Lambda 主体

出现在匿名类的声明不同的代码,名称的含义和thissuper出现在拉姆达身体,用引用声明的可访问性以及关键字,都一样在周围环境(除了拉姆达参数引入新的名称)。

这使得 lambda 表达式很明显可以访问private其类的成员,即定义它的类,而不是函数接口。lambda 表达式没有实现函数式接口,也没有从中继承成员。它将与目标类型类型兼容,并且将有一个函数接口的实例,当函数方法在运行时调用时执行 lambda 表达式的主体。

此实例的生成方式是有意未指定的。作为技术细节的说明,参考实现中生成的类可以访问private另一个类的方法,这是必要的,因为为 lambda 表达式生成的合成方法private也是如此。这可以通过添加MethodInvoker.invoke(Test::method);到您的测试用例来说明。此方法引用允许method直接调用,而无需在类中使用任何合成方法Test


不过,反射是另一回事。它甚至没有出现在语言规范中。这是图书馆的特色。这个库在内部类可访问性方面存在已知问题。这些问题与内部类特性本身一样古老(自 Java 1.1 起)。还有JDK-8010319,在嵌套类Java访问规则JVM支持其当前状态的目标是Java的10 ...

如果你真的需要内部类中的反射访问,你可以使用这个java.lang.invoke包:

public class Test {
    private static Void method() {
        System.out.println("OK");
        return null;
    }
    public static void main(String[] args) throws Exception {
        // captures the context including accessibility,
        // stored in a local variable, thus only available to inner classes of this method
        MethodHandles.Lookup lookup = MethodHandles.lookup();

        MethodHandle method = lookup.findStatic(Test.class, "method",
                                  MethodType.methodType(Void.class));
        // TEST 2
        MethodInvoker.invoke(new Callable() {
            public Object call() throws Exception {
                // invoking a method handle performs no access checks
                try { return (Void)method.invokeExact(); }
                catch(Exception|Error e) { throw e; }
                catch(Throwable t) { throw new AssertionError(t); }
            }
        });
        // TEST 3
        MethodInvoker.invoke(new Callable() {
            // since lookup captured the access context, we can search for Test's
            // private members even from within the inner class
            MethodHandle method = lookup.findStatic(Test.class, "method",
                                      MethodType.methodType(Void.class));
            public Object call() throws Exception {
                // again, invoking a method handle performs no access checks
                try { return (Void)method.invokeExact(); }
                catch(Exception|Error e) { throw e; }
                catch(Throwable t) { throw new AssertionError(t); }
            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,由于MethodHandles.Lookup对象和MethodHandle包含private无需进一步检查即可访问其创建者成员的能力,因此必须小心不要将它们分发给无意的人。但是为此,您可以确定现有的语言级别可访问性。如果在private字段中存储查找对象或句柄,则只有同一顶级类中的代码才能访问它,如果使用局部变量,则只有同一局部范围内的类才能访问它。


由于只有直接调用者java.lang.reflect.Method,另一种解决方案是使用蹦床:

public class Test {
    private static Void method() {
        System.out.println("OK");
        return null;
    }
    public static void main(String[] args) throws Exception {
        Method method = Test.class.getDeclaredMethod("method");

        // TEST 3
        MethodInvoker.invoke(new Callable() {
            @Override
            public Object call() throws Exception {
                return invoke(method, null); // works

            }
        });
    }
    private static Object invoke(Method m, Object obj, Object... arg)
    throws ReflectiveOperationException {
        return m.invoke(obj, arg);
    }
}
Run Code Online (Sandbox Code Playgroud)