2017-02-02 79 views
2

我对以下情况感到困惑。Lambda可访问私有方法

考虑两个包ab有以下类:

1)MethodInvoker只是调用call()给定对象上:

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

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??? 

      } 
     }); 
    } 
} 

我明确提出method()私人来测试我如何t可以在Test类范围外调用。我通常对所有3例都感到困惑,因为我觉得它们有争议。 我通常会期望他们都应该以相同的方式工作。至少我期望如果测试3抛出IllegalAccessException,那么测试2应该做同样的事情。但测试2工作正常!

有人可以根据JLS给出严格的解释为什么每个这样的情况工作,因为它的工作原理?

回答

3

TEST1和TEST3之间的区别归结为如何实现lambda和匿名类之间的区别。

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

TEST1拉姆达:

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

TEST3匿名类:

一个匿名类被转换为一个类。 method.invoke()在该类中被调用,该类不应该访问私有方法。由于反射,合成方法的工作周转不起作用。

TEST2: 为了允许嵌套类访问其外部类的私有成员,引入了合成方法。如果你看看字节码,你会看到与调用转发给Void method()

+0

好,但请,我强调的是,我根据JLS – Andremoniy

+0

但你期待字符串解释在你的(可能的)假设中是正确的,这个问题是由我们以前的问题的答案引起的:) – Andremoniy

+1

对不起,搞砸了数字。现在应该修复。对不起,但我不会提供所有内容的jls链接,因为这涉及太多话题。 –

3

关于对语言水平的辅助签名static java.lang.Void access$000();的方法,有直接的声明JLS §6.6.1, Determining Accessibility

...

  • 否则,该成员或构造函数声明为private,并且只有当它出现在包含声明的顶层类(§7.6)成员或构造函数。

由于所有嵌套类和lambda表达式驻留在同一个“顶层阶级的身体”内,这已经足以说明该访问的有效性。

但是lambda表达式是根本不同的内部类反正:

JLS §15.27.2, Lambda Body

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

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

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


虽然反射是不同的事情。它甚至不出现在语言规范中。这是一个图书馆功能。当涉及到内部类的可访问性时,这个库已经存在已知的问题。这些问题与内部类自身一样古老(自Java 1.1以来)。有JDK-8010319, JVM support for Java access rules in nested classes当前状态为目标的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); } 
      } 
     }); 
    } 
} 

当然,由于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); 
    } 
} 
+0

感谢您的详细解释 – Andremoniy