Java11中基于嵌套关系的访问控制优化详解

作者:看山 时间:2021-12-28 18:54:20 

前言

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中的私有属性或方法。

我们称这些生成的方法为&ldquo;桥&rdquo;方法(Bridge Method),是一种实现嵌套关系内部互相访问的方式。

在编译的时候,Java 为了保持类的单一特性,会将嵌套类编译到多个 class 文件中,同时为了保证嵌套类能够彼此访问,自动创建了调用私有方法的&ldquo;桥&rdquo;方法,这样,在保持原有定义不变的情况下,又实现了嵌套语法。

技术债务

&ldquo;桥&rdquo;方法的实现是比较巧妙的,但是这会造成源码与编译结果访问控制权限不一致,比如,我们可以在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)

通过反射直接调用私有方法会失败,但是可以直接的或者通过反射访问这些&ldquo;桥&rdquo;方法,这样就比较奇怪了。所以提出 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。

  • 可能会影响&ldquo;密封类&rdquo;(sealed classes),仅允许 Nestmate 的子类作为密封类。

  • 可能会影响私有嵌套类型。私有嵌套类型当前定义为包内可访问(package-access)。

文末总结

本文阐述了基于嵌套关系的访问控制优化,其中涉及NestMate、NestHost、NestMember等概念。这次优化是 Valhalla 项目中一部分,主要改进 Java 中的值类型和泛型等。

来源:https://blog.csdn.net/liuxinghao/article/details/122334212

标签:java11,嵌套,控制
0
投稿

猜你喜欢

  • 解决MyEclipse出现the user operation is waiting的问题

    2022-05-02 21:44:07
  • 分享15款Java程序员必备的开发工具

    2021-12-07 19:09:07
  • Java计算文本MD5加密值的方法示例

    2023-11-15 13:18:48
  • IntelliJ IDEA2019实现Web项目创建示例

    2023-06-05 00:29:33
  • java中常见的死锁以及解决方法代码

    2023-04-07 19:47:30
  • java 流与 byte[] 的互转操作

    2023-06-26 11:25:46
  • SpringCloud Zuul网关功能实现解析

    2022-09-12 14:30:31
  • ImportBeanDefinitionRegistrar手动控制BeanDefinition创建注册详解

    2021-11-11 18:49:18
  • 替换so文件来动态替换Flutter代码实现详解

    2023-06-23 16:24:06
  • 基于JAVA文件中获取路径及WEB应用程序获取路径的方法

    2022-08-21 01:26:57
  • Unity使用鼠标旋转物体效果

    2021-10-17 05:08:12
  • springboot项目如何防止XSS攻击

    2021-10-17 10:03:02
  • Java实现带GUI的气泡诗词效果

    2022-09-12 18:04:48
  • SpringBoot配置Email发送功能实例

    2022-03-07 20:52:20
  • C#实现观察者模式(Observer Pattern)的两种方式

    2023-06-20 21:05:18
  • spring boot ${}占位符不起作用的解决方案

    2022-06-28 20:49:29
  • Spring Security认证机制源码层探究

    2022-07-27 19:05:26
  • Java线程安全的计数器简单实现代码示例

    2023-11-09 15:41:57
  • 基于Mybatis映射的一点心得(分享)

    2023-08-08 13:15:53
  • java实现PPT转化为PDF

    2021-06-17 05:02:24
  • asp之家 软件编程 m.aspxhome.com