Java11中基于嵌套關系的訪問控制優(yōu)化詳解
前言
Java 語言很強大,但是,有人的地方就有江湖,有猿的地方就有 bug,Java 的核心代碼并非十全十美。比如在 JDK 中居然也有反模式接口常量 中介紹的反模式實現(xiàn),以及本文說到的這個技術債務:嵌套關系(NestMate)調用方式。
在 Java 語言中,類和接口可以相互嵌套,這種組合之間可以不受限制的彼此訪問,包括訪問彼此的構造函數(shù)、字段、方法等。即使是private私有的,也可以彼此訪問。比如下面這樣定義:
public class Outer { private int i; public void print1() { print11(); print12(); } private void print11() { System.out.println(i); } private void print12() { System.out.println(i); } public void callInnerMethod() { final Inner inner = new Inner(); inner.print4(); inner.print5(); System.out.println(inner.j); } public class Inner { private int j; public void print3() { System.out.println(i); print1(); } public void print4() { System.out.println(i); print11(); print12(); } private void print5() { System.out.println(i); print11(); print12(); } } }
上例中,Outer類中的字段i、方法print11和print12都是私有的,但是可以在Inner類中直接訪問,Inner類的字段j、方法print5是私有的,也可以在Outer類中使用。這種設計是為了更好的封裝,在用戶看來,這幾個彼此嵌套的類/接口是一體的,分開定義是為了更好的封裝自己,隔離不同特性,但是有因為彼此是一體,所以私有元素也應該是共有的。
Java11 之前的實現(xiàn)方式
我們使用 Java8 編譯,然后借助javap -c命令分別查看Outer和Inner的結果。
$ javap -c Outer.class Compiled from "Outer.java" public class cn.howardliu.tutorials.java8.nest.Outer { public cn.howardliu.tutorials.java8.nest.Outer(); Code: 0: aload_0 1: invokespecial #4 // Method java/lang/Object."<init>":()V 4: return public void print1(); Code: 0: aload_0 1: invokespecial #2 // Method print11:()V 4: aload_0 5: invokespecial #1 // Method print12:()V 8: return public void callInnerMethod(); Code: 0: new #7 // class cn/howardliu/tutorials/java8/nest/Outer$Inner 3: dup 4: aload_0 5: invokespecial #8 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner."<init>":(Lcn/howardliu/tutorials/java8/nest/Outer;)V 8: astore_1 9: aload_1 10: invokevirtual #9 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.print4:()V 13: aload_1 14: invokestatic #10 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.access$000:(Lcn/howardliu/tutorials/java8/nest/Outer$Inner;)V 17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 20: aload_1 21: invokestatic #11 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.access$100:(Lcn/howardliu/tutorials/java8/nest/Outer$Inner;)I 24: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 27: return static int access$200(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: getfield #3 // Field i:I 4: ireturn static void access$300(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: invokespecial #2 // Method print11:()V 4: return static void access$400(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: invokespecial #1 // Method print12:()V 4: return }
再來看看Inner的編譯結果,這里需要注意的是,內部類會使用特殊的命名方式定義Inner類,最終會將編譯結果存儲在兩個文件中:
$ javap -c Outer\$Inner.class Compiled from "Outer.java" public class cn.howardliu.tutorials.java8.nest.Outer$Inner { final cn.howardliu.tutorials.java8.nest.Outer this$0; public cn.howardliu.tutorials.java8.nest.Outer$Inner(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: aload_1 2: putfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 5: aload_0 6: invokespecial #4 // Method java/lang/Object."<init>":()V 9: return public void print3(); Code: 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 7: invokestatic #6 // Method cn/howardliu/tutorials/java8/nest/Outer.access$200:(Lcn/howardliu/tutorials/java8/nest/Outer;)I 10: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 17: invokevirtual #8 // Method cn/howardliu/tutorials/java8/nest/Outer.print1:()V 20: return public void print4(); Code: 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 7: invokestatic #6 // Method cn/howardliu/tutorials/java8/nest/Outer.access$200:(Lcn/howardliu/tutorials/java8/nest/Outer;)I 10: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 17: invokestatic #9 // Method cn/howardliu/tutorials/java8/nest/Outer.access$300:(Lcn/howardliu/tutorials/java8/nest/Outer;)V 20: aload_0 21: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 24: invokestatic #10 // Method cn/howardliu/tutorials/java8/nest/Outer.access$400:(Lcn/howardliu/tutorials/java8/nest/Outer;)V 27: return static void access$000(cn.howardliu.tutorials.java8.nest.Outer$Inner); Code: 0: aload_0 1: invokespecial #2 // Method print5:()V 4: return static int access$100(cn.howardliu.tutorials.java8.nest.Outer$Inner); Code: 0: aload_0 1: getfield #1 // Field j:I 4: ireturn }
我們可以看到,Outer和Inner中多出了幾個方法,方法名格式是access$*00。
Outer中的access$200方法返回了屬性i,access$300和access$400分別調用了print11和print12方法。這些新增的方法都是靜態(tài)方法,作用域是默認作用域,即包內可用。這些方法最終被Inner類中的print3和print4調用,相當于間接調用Outer中的私有屬性或方法。
我們稱這些生成的方法為“橋”方法(Bridge Method),是一種實現(xiàn)嵌套關系內部互相訪問的方式。
在編譯的時候,Java 為了保持類的單一特性,會將嵌套類編譯到多個 class 文件中,同時為了保證嵌套類能夠彼此訪問,自動創(chuàng)建了調用私有方法的“橋”方法,這樣,在保持原有定義不變的情況下,又實現(xiàn)了嵌套語法。
技術債務
“橋”方法的實現(xiàn)是比較巧妙的,但是這會造成源碼與編譯結果訪問控制權限不一致,比如,我們可以在Inner中調用Outer中的私有方法,按照道理來說,我們可以在Inner中通過反射調用Outer的方法,但實際上不行,會拋出IllegalAccessException異常。我們驗證一下:
public class Outer { // 省略其他方法 public void callInnerReflectionMethod() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { final Inner inner = new Inner(); inner.callOuterPrivateMethod(this); } public class Inner { // 省略其他方法 public void callOuterPrivateMethod(Outer outer) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { final Method method = outer.getClass().getDeclaredMethod("print12"); method.invoke(outer); } } }
定義測試用例:
@Test void gotAnExceptionInJava8() { final Outer outer = new Outer(); final Exception e = assertThrows(IllegalAccessException.class, outer::callInnerReflectionMethod); e.printStackTrace(); assertDoesNotThrow(outer::callInnerMethod); }
打印的異常信息是:
java.lang.IllegalAccessException: class cn.howardliu.tutorials.java8.nest.Outer$Inner cannot access a member of class cn.howardliu.tutorials.java8.nest.Outer with modifiers "private"
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591)
at java.base/java.lang.reflect.Method.invoke(Method.java:558)
at cn.howardliu.tutorials.java8.nest.Outer$Inner.callOuterPrivateMethod(Outer.java:62)
at cn.howardliu.tutorials.java8.nest.Outer.callInnerReflectionMethod(Outer.java:36)
通過反射直接調用私有方法會失敗,但是可以直接的或者通過反射訪問這些“橋”方法,這樣就比較奇怪了。所以提出 JEP181 改進,修復這個技術債務的同時,為后續(xù)的改進鋪路。
Java11 中的實現(xiàn)
我們再來看看 Java11 編譯之后的結果:
$ javap -c Outer.class Compiled from "Outer.java" public class cn.howardliu.tutorials.java11.nest.Outer { public cn.howardliu.tutorials.java11.nest.Outer(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void print1(); Code: 0: aload_0 1: invokevirtual #2 // Method print11:()V 4: aload_0 5: invokevirtual #3 // Method print12:()V 8: return public void callInnerMethod(); Code: 0: new #7 // class cn/howardliu/tutorials/java11/nest/Outer$Inner 3: dup 4: aload_0 5: invokespecial #8 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner."<init>":(Lcn/howardliu/tutorials/java11/nest/Outer;)V 8: astore_1 9: aload_1 10: invokevirtual #9 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner.print4:()V 13: aload_1 14: invokevirtual #10 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner.print5:()V 17: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 20: aload_1 21: getfield #11 // Field cn/howardliu/tutorials/java11/nest/Outer$Inner.j:I 24: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 27: return }
是不是很干凈,與Outer類的源碼結構是一致的。我們再看看Inner有沒有什么變化:
$ javap -c Outer\$Inner.class Compiled from "Outer.java" public class cn.howardliu.tutorials.java11.nest.Outer$Inner { final cn.howardliu.tutorials.java11.nest.Outer this$0; public cn.howardliu.tutorials.java11.nest.Outer$Inner(cn.howardliu.tutorials.java11.nest.Outer); Code: 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return public void print3(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 7: getfield #4 // Field cn/howardliu/tutorials/java11/nest/Outer.i:I 10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 17: invokevirtual #6 // Method cn/howardliu/tutorials/java11/nest/Outer.print1:()V 20: return public void print4(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 7: getfield #4 // Field cn/howardliu/tutorials/java11/nest/Outer.i:I 10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 17: invokevirtual #7 // Method cn/howardliu/tutorials/java11/nest/Outer.print11:()V 20: aload_0 21: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 24: invokevirtual #8 // Method cn/howardliu/tutorials/java11/nest/Outer.print12:()V 27: return }
同樣干凈。
我們在通過測試用例驗證一下反射調用:
@Test void doesNotGotAnExceptionInJava11() { final Outer outer = new Outer(); assertDoesNotThrow(outer::callInnerReflectionMethod); assertDoesNotThrow(outer::callInnerMethod); }
結果是正常運行。
這就是 JEP181 期望的結果,源碼和編譯結果一致,訪問控制一致。
Nestmate 新增的 API
在 Java11 中還新增了幾個 API,用于嵌套關系的驗證:
getNestHost
這個方法是返回嵌套主機(NestHost),轉成普通話就是找到嵌套類的外層類。對于非嵌套類,直接返回自身(其實也算是返回外層類)。
我們看下用法:
@Test void checkNestHostName() { final String outerNestHostName = Outer.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.Outer", outerNestHostName); final String innerNestHostName = Inner.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.Outer", innerNestHostName); assertEquals(outerNestHostName, innerNestHostName); final String notNestClass = NotNestClass.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.NotNestClass", notNestClass); }
對于Outer和Inner都是返回了cn.howardliu.tutorials.java11.nest.Outer。
getNestMembers
這個方法是返回嵌套類的嵌套成員數(shù)組,下標是 0 的元素確定是 NestHost 對應的類,其他元素順序沒有給出排序規(guī)則。我們看下使用:
@Test void getNestMembers() { final List<String> outerNestMembers = Arrays.stream(Outer.class.getNestMembers()) .map(Class::getName) .collect(Collectors.toList()); assertEquals(2, outerNestMembers.size()); assertTrue(outerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer")); assertTrue(outerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer$Inner")); final List<String> innerNestMembers = Arrays.stream(Inner.class.getNestMembers()) .map(Class::getName) .collect(Collectors.toList()); assertEquals(2, innerNestMembers.size()); assertTrue(innerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer")); assertTrue(innerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer$Inner")); }
isNestmateOf
這個方法是用于判斷兩個類是否是彼此的 NestMate,彼此形成嵌套關系。判斷依據(jù)還是嵌套主機,只要相同,兩個就是 NestMate。我們看下使用:
@Test void checkIsNestmateOf() { assertTrue(Inner.class.isNestmateOf(Outer.class)); assertTrue(Outer.class.isNestmateOf(Inner.class)); }
后續(xù)的改進
嵌套關系是作為 Valhalla 項目的一部分,這個項目的主要目標之一是改進 JAVA 中的值類型和泛型。后續(xù)會有更多的改進:
- 在泛型特化(generic specialization)中,每個特化類型(specialized type)可被創(chuàng)建為泛型的一個 Nestmate。
- 支持對Unsafe.defineAnonymousClass() API 的安全替換,實現(xiàn)將新類創(chuàng)建為已有類的 Nestmate。
- 可能會影響“密封類”(sealed classes),僅允許 Nestmate 的子類作為密封類。
- 可能會影響私有嵌套類型。私有嵌套類型當前定義為包內可訪問(package-access)。
文末總結
本文闡述了基于嵌套關系的訪問控制優(yōu)化,其中涉及NestMate、NestHost、NestMember等概念。這次優(yōu)化是 Valhalla 項目中一部分,主要改進 Java 中的值類型和泛型等。
到此這篇關于Java11中基于嵌套關系的訪問控制優(yōu)化的文章就介紹到這了,更多相關Java11嵌套關系的訪問控制內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JDBC利用C3P0數(shù)據(jù)庫連接池連接數(shù)據(jù)庫
這篇文章主要為大家詳細介紹了JDBC利用C3P0數(shù)據(jù)庫連接池連接數(shù)據(jù)庫,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-08-08SpringBoot后端數(shù)據(jù)校驗實戰(zhàn)操作指南
在項?開發(fā)中,對于前端提交的表單,后臺接?接收到表單數(shù)據(jù)后,為了保證程序的嚴謹性,通常后端會加?業(yè)務參數(shù)的合法校驗操作來避免程序的?技術性?bug,這篇文章主要給大家介紹了關于SpringBoot后端數(shù)據(jù)校驗的相關資料,需要的朋友可以參考下2022-07-07java數(shù)字和中文算數(shù)驗證碼的實現(xiàn)
這篇文章主要介紹了java數(shù)字和中文算數(shù)驗證碼的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07Windows中在IDEA上安裝和使用JetBrains Mono字體的教程
這篇文章主要介紹了Windows IDEA上安裝和使用JetBrains Mono字體的教程,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03springBoot熱部署、請求轉發(fā)與重定向步驟詳解
這篇文章主要介紹了springBoot熱部署、請求轉發(fā)與重定向,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-06-06SpringBoot 整合 Shiro 密碼登錄與郵件驗證碼登錄功能(多 Realm 認證)
這篇文章主要介紹了SpringBoot 整合 Shiro 密碼登錄與郵件驗證碼登錄(多 Realm 認證),本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02解決java數(shù)值范圍以及float與double精度丟失的問題
下面小編就為大家?guī)硪黄鉀Qjava數(shù)值范圍以及float與double精度丟失的問題。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06