Java11 中基于嵌套关系的访问控制优化问题
作者:看山
你好,我是看山。
Java 语言很强大,但是,有人的地方就有江湖,有猿的地方就有 bug,Java 的核心代码并非十全十美。比如在 JDK 中居然也有反模式接口常量 中介绍的反模式实现,以及本文说到的这个技术债务:嵌套关系(NestMate)调用方式。
在 Java 语言中,类和接口可以相互嵌套,这种组合之间可以不受限制的彼此访问,包括访问彼此的构造函数、字段、方法等。即使是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 之前的实现方式
我们使用 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
方法。这些新增的方法都是静态方法,作用域是默认作用域,即包内可用。这些方法最终被Inner
类中的print3
和print4
调用,相当于间接调用Outer
中的私有属性或方法。
我们称这些生成的方法为“桥”方法(Bridge Method),是一种实现嵌套关系内部互相访问的方式。
在编译的时候,Java 为了保持类的单一特性,会将嵌套类编译到多个 class 文件中,同时为了保证嵌套类能够彼此访问,自动创建了调用私有方法的“桥”方法,这样,在保持原有定义不变的情况下,又实现了嵌套语法。
技术债务
“桥”方法的实现是比较巧妙的,但是这会造成源码与编译结果访问控制权限不一致,比如,我们可以在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 改进,修复这个技术债务的同时,为后续的改进铺路。
Java11 中的实现
我们再来看看 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
这个方法是返回嵌套类的嵌套成员数组,下标是 0 的元素确定是 NestHost 对应的类,其他元素顺序没有给出排序规则。我们看下使用:
@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,彼此形成嵌套关系。判断依据还是嵌套主机,只要相同,两个就是 NestMate。我们看下使用:
@Test void checkIsNestmateOf() { assertTrue(Inner.class.isNestmateOf(Outer.class)); assertTrue(Outer.class.isNestmateOf(Inner.class)); }
后续的改进
嵌套关系是作为 Valhalla 项目的一部分,这个项目的主要目标之一是改进 JAVA 中的值类型和泛型。后续会有更多的改进:
- 在泛型特化(generic specialization)中,每个特化类型(specialized type)可被创建为泛型的一个 Nestmate。
- 支持对
Unsafe.defineAnonymousClass()
API 的安全替换,实现将新类创建为已有类的 Nestmate。 - 可能会影响“密封类”(sealed classes),仅允许 Nestmate 的子类作为密封类。
- 可能会影响私有嵌套类型。私有嵌套类型当前定义为包内可访问(package-access)。
到此这篇关于Java11 中基于嵌套关系的访问控制优化的文章就介绍到这了,更多相关Java访问控制优化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!