当您在另一个类中有一个类时,他们可以看到彼此的private
方法。 在Java开发人员中并不为人所知。 面试中的许多候选人说, private
是一种可见性,它使代码可以查看成员是否属于同一班级。 这实际上是对的,但是更准确地说,代码和成员都在一个类中。当我们嵌套了内部类时, private
成员和使用它的代码可能会出现在同一班级,同时他们也处于不同的班级。
例如,如果我在一个顶级类中有两个嵌套类,则其中一个嵌套类中的代码可以看到另一个嵌套类的private
成员。
当我们查看生成的代码时,它开始变得很有趣。 JVM不在乎其他类中的类。 它处理JVM“顶级”类。 当您在类A
有一个名为B
的类时,编译器将创建.class
文件,其名称将类似于A$B.class
。 B
有一个可从A
调用的private
方法,然后JVM看到A.class
中的代码调用A$B.class
的方法。 JVM检查访问控制。 当我们与初级用户讨论此问题时,有人建议JVM可能不在乎修饰符。 那是不对的。 尝试编译A.java
和B.java
,两个顶级班,在一些代码A
调用一个public
的方法B
。 当你拥有A.class
和B.class
修改方法B.java
被public
是private
,并重新编译B
た新B.class
。 启动应用程序,您将看到JVM非常关心访问修饰符。 尽管如此,您仍可以在上面的示例中从A.class
调用A$B.class
的方法。
为了解决此冲突,Java生成了额外的合成方法,这些合成方法本来就是公共的,可以在同一类内调用原始的私有方法,并且在考虑JVM访问控制的情况下可以调用。 另一方面,如果您找出生成的方法的名称并尝试直接从Java源代码中调用,则Java编译器将不会编译代码。 我在4年前就写了详细的文章。
如果您是一位经验丰富的开发人员,那么您可能会认为这是一个奇怪而令人反感的技巧。 除了此hack外,Java非常干净,优雅,简洁,纯净。 还有可能是Integer
缓存的破解,它使用==
使小的Integer
对象(典型的测试值)相等,而较大的值仅equals()
,而==
(典型的生产值)。 但是除了合成类和Integer
缓存hack之外,Java都是干净,优雅,简洁和纯净的。 (您可能会发现我是Monty Python的粉丝。)
这样做的原因是嵌套类不是原始Java的一部分,而是仅添加到1.1版中。解决方案是一个hack,但是那时还有许多重要的事情要做,例如引入JIT编译器,JDBC,RMI,反思和其他一些我们今天认为理所当然的事情。 那个时候的问题不是解决方案是否干净。 相反,问题是Java是否将完全存活下来并成为主流编程语言,或者死掉并仍然是一个不错的尝试。 那个时候我还担任销售代表,编码只是一种业余爱好,因为东欧的编码工作很少,它们主要是无聊的簿记应用程序,而且薪水低。 那段时间有些不同,搜索引擎名为AltaVista,我们从水龙头里喝水,而Java具有不同的优先级。
结果是20多年来,我们的JAR文件略大,Java执行速度稍慢(除非JIT优化了调用链)以及IDE中令人讨厌的警告提示我们最好在嵌套类中使用包保护的方法,而不是private
当我们从顶级或其他嵌套类中使用它时。
巢主机
现在看来,这20年的技术债务将得到解决。 http://openjdk.java.net/jeps/181进入Java 11,它将通过引入一个新概念来解决此问题:nest。 当前,Java字节码包含一些有关类之间关系的信息。 JVM知道某个类是另一个类的嵌套类,而不仅仅是名称。 该信息可以使JVM决定是否允许一个类中的一段代码访问另一类的private
成员,但是JEP-181开发具有更一般的含义。 随着时间的推移,JVM不再是Java虚拟机。 好吧,是的,至少它是名称,但是,它是一个虚拟机,恰好执行从Java编译的字节码。 或其他语言的问题。 有许多针对JVM的语言,请记住JEP-181不想将JVM的新访问控制功能与Java语言的特定功能联系在一起。
JEP-181将NestHost
和NestMembers
的概念定义为类的属性。 编译器将填充这些字段,并且当可以从另一个类访问某个类的私有成员时,JVM访问控制可以检查:两个类是否在同一嵌套中? 如果它们在同一巢中,则允许访问,否则不允许访问。 我们将在反射访问中添加方法,因此我们可以获得嵌套中的类的列表。
简单的嵌套示例
使用
$ java -version
java version "11-ea" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11-ea+25)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11-ea+25, mixed mode)
今天的Java版本我们可以进行实验。 我们可以创建一个简单的类:
package nesttest;
public class NestingHost {public static class NestedClass1 {private void privateMethod() {new NestedClass2().privateMethod();}}public static class NestedClass2 {private void privateMethod() {new NestedClass1().privateMethod();}}
}
很简单,它什么也不做。 私有方法互相调用。 没有这个,编译器就会发现它们只是什么也不做,并且不需要它们,而字节码只是不包含它们。
读取嵌套信息的类
package nesttest;import java.util.Arrays;
import java.util.stream.Collectors;public class TestNest {public static void main(String[] args) {Class host = NestingHost.class.getNestHost();Class[] nestlings = NestingHost.class.getNestMembers();System.out.println("Mother bird is: " + host);System.out.println("Nest dwellers are :\n" +Arrays.stream(nestlings).map(Class::getName).collect(Collectors.joining("\n")));}
}
打印输出符合预期:
Mother bird is: class nesttest.NestingHost
Nest dwellers are :
nesttest.NestingHost
nesttest.NestingHost$NestedClass2
nesttest.NestingHost$NestedClass1
请注意,嵌套主机也列在嵌套成员中,尽管此信息应该非常明显且多余。 但是,这样的使用可能允许某些语言从访问中公开嵌套主机本身的私有成员,并使访问仅允许嵌套。
字节码
使用JDK11编译器进行编译会生成文件
-
NestingHost$NestedClass1.class
-
NestingHost$NestedClass2.class
-
NestingHost.class
-
TestNest.class
没有变化。 另一方面,如果我们使用javap
反编译器查看字节码,则将看到以下内容:
$ javap -v build/classes/java/main/nesttest/NestingHost\$NestedClass1.class
Classfile .../packt/Fundamentals-of-java-18.9/sources/ch08/bulkorders/build/classes/java/main/nesttest/NestingHost$NestedClass1.classLast modified Aug 6, 2018; size 557 bytesMD5 checksum 5ce1e0633850dd87bd2793844a102c52Compiled from "NestingHost.java"
public class nesttest.NestingHost$NestedClass1minor version: 0major version: 55flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #5 // nesttest/NestingHost$NestedClass1super_class: #6 // java/lang/Objectinterfaces: 0, fields: 0, methods: 2, attributes: 3
Constant pool:*** CONSTANT POOL DELETED FROM THE PRINTOUT ***{public nesttest.NestingHost$NestedClass1();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lnesttest/NestingHost$NestedClass1;
}
SourceFile: "NestingHost.java"
NestHost: class nesttest/NestingHost
InnerClasses:public static #13= #5 of #20; // NestedClass1=class nesttest/NestingHost$NestedClass1 of class nesttest/NestingHostpublic static #23= #2 of #20; // NestedClass2=class nesttest/NestingHost$NestedClass2 of class nesttest/NestingHost
如果我们使用JDK10编译器编译相同的类,则反汇编行如下:
$ javap -v build/classes/java/main/nesttest/NestingHost\$NestedClass1.class
Classfile /C:/Users/peter_verhas/Dropbox/packt/Fundamentals-of-java-18.9/sources/ch08/bulkorders/build/classes/java/main/nesttest/NestingHost$NestedClass1.classLast modified Aug 6, 2018; size 722 bytesMD5 checksum 8c46ede328a3f0ca265045a5241219e9Compiled from "NestingHost.java"
public class nesttest.NestingHost$NestedClass1minor version: 0major version: 54flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #6 // nesttest/NestingHost$NestedClass1super_class: #7 // java/lang/Objectinterfaces: 0, fields: 0, methods: 3, attributes: 2
Constant pool:*** CONSTANT POOL DELETED FROM THE PRINTOUT ***{public nesttest.NestingHost$NestedClass1();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #2 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lnesttest/NestingHost$NestedClass1;static void access$100(nesttest.NestingHost$NestedClass1);descriptor: (Lnesttest/NestingHost$NestedClass1;)Vflags: (0x1008) ACC_STATIC, ACC_SYNTHETICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method privateMethod:()V4: returnLineNumberTable:line 6: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 x0 Lnesttest/NestingHost$NestedClass1;
}
SourceFile: "NestingHost.java"
InnerClasses:public static #14= #6 of #25; // NestedClass1=class nesttest/NestingHost$NestedClass1 of class nesttest/NestingHostpublic static #27= #3 of #25; // NestedClass2=class nesttest/NestingHost$NestedClass2 of class nesttest/NestingHost
Java 10编译器生成access$100
方法。 Java 11编译器没有。 相反,它在类文件中具有嵌套主机字段。 我们终于摆脱了那些在一些反映框架的代码中列出所有方法时引起意外的综合方法。
窝窝
让我们玩一些布谷鸟吧。 我们可以稍微修改一下代码,以便现在可以执行以下操作:
package nesttest;
public class NestingHost {
// public class NestedClass1 {
// public void publicMethod() {
// new NestedClass2().privateMethod(); /* <-- this is line 8 */
// }
// }public class NestedClass2 {private void privateMethod() {System.out.println("hallo");}}
}
我们还创建了一个简单的测试类
package nesttest;public class HackNest {public static void main(String[] args) {
// var nestling =new NestingHost().new NestedClass1();
// nestling.publicMethod();}
}
首先,从所有行的开头删除所有//
并编译项目。 它像魅力一样工作并打印出hallo
。 之后,将生成的类复制到安全的位置,例如项目的根目录。
$ cp build/classes/java/main/nesttest/NestingHost\$NestedClass1.class .
$ cp build/classes/java/main/nesttest/HackNest.class .
让我们编译项目,这次使用注释,然后将之前的编译中的两个类文件复制回去:
$ cp HackNest.class build/classes/java/main/nesttest/
$ cp NestingHost\$NestedClass1.class build/classes/java/main/nesttest/
现在我们有了一个NestingHost
,它知道它只有一个NestedClass2
: NestedClass2
。 但是,测试代码认为还有另一个NestedClass1
,并且它还具有可以调用的公共方法。 这样,我们尝试将额外的雏鸟潜入巢中。 如果执行代码,则会出现错误:
$ java -cp build/classes/java/main/ nesttest.HackNest
Exception in thread "main" java.lang.IncompatibleClassChangeError: Type nesttest.NestingHost$NestedClass1 is not a nest member of nesttest.NestingHost: current type is not listed as a nest memberat nesttest.NestingHost$NestedClass1.publicMethod(NestingHost.java:8)at nesttest.HackNest.main(HackNest.java:7)
从代码中认识到导致错误的行是我们要调用私有方法的那一行,这一点很重要。 Java运行时仅在那一点而不是更快地进行检查。
我们喜欢还是不喜欢? 快速失败原则在哪里? 为什么Java运行时仅在非常需要时才开始执行类并检查嵌套结构? 原因,在Java情况下,原因很多:向后兼容。 加载所有类后,JVM可以检查嵌套结构的一致性。 这些类仅在使用时加载。 可以更改Java 11中的类加载,并与嵌套主机一起加载所有嵌套的类,但是这会破坏向后兼容性。 如果没有别的,懒惰的单例模式会崩溃,我们不希望那样。 我们爱单身,但只有单麦芽(是)。
结论
JEP-181是Java的一个小改动。 大多数开发人员甚至不会注意到。 它消除了技术债务,如果核心Java项目没有消除技术债务,那么我们对普通开发人员有何期待?
就像古老的拉丁语所说:“技术需要借记卡。”
翻译自: https://www.javacodegeeks.com/2018/08/nested-classes-private-methods.html