2017-02-02 118 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