Nested classes and private methods
When you have a class inside another class they can see each others private
methods. It is not well known among Java developers. Many candidates during interviews say that private
is a visibility that lets a code see a member if that is in the same class. This is actually true, but it would be more precise to say that there is a class that both the code and the member is in. When we have nested and inner classes it can happen that the private
member and the code using it is in the same class and at the same time they are also in different classes.
As an example, if I have two nested classes in a top-level class then the code in one of the nested classes can see a private
member of the other nested class.
It starts to be interesting when we look at the generated code. The JVM does not care about classes inside other classes. It deals with JVM “top-level” classes. The compiler will create .class
files that will have a name like A$B.class
when you have a class named B
inside a class A
. There is a private
method in B
callable from A
then the JVM sees that the code in A.class
calls the method in A$B.class
. The JVM checks access control. When we discussed this with juniors somebody suggested that probably the JVM does not care the modifier. That is not true. Try to compile A.java
and B.java
, two top-level classes with some code in A
calling a public
method in B
. When you have A.class
and B.class
modify the method in B.java
from being public
to be private
and recompile B
t a new B.class
. Start the application and you will see that the JVM cares about the access modifiers a lot. Still, you can invoke in the example above from A.class
a method in A$B.class
.
To resolve this conflict Java generates extra synthetic methods that are inherently public, call the original private method inside the same class and are callable as far as the JVM access control is considered. On the other hand, the Java compiler will not compile the code if you figure out the name of the generated method and try to call in from the Java source code directly. I wrote about in details more than 4 years ago.
If you are a seasoned developer then you probably think that this is a weird and revolting hack. Java is so clean, elegant, concise and pure except this hack. And also perhaps the hack of the Integer
cache that makes small Integer
objects (typical test values) to be equal using the ==
while larger values are only equals()
but not ==
(typical production values). But other than the synthetic classes and Integer
cache hack Java is clean, elegant, concise and pure. (You may get I am a Monty Python fan.)
The reason for this is that nested classes were not part of the original Java, it was added only to version 1.1 The solution was a hack, but there were more important things to do at that time, like introducing JIT compiler, JDBC, RMI, reflection and some other things that we take today for granted. That time the question was not if the solution is nice and clean. Rather the question was if Java will survive at all and be a mainstream programming language or dies and remains a nice try. That time I was still working as a sales rep and coding was only a hobby because coding jobs were scarce in East Europe, they were the mainly boring bookkeeping applications and were low paid. Those were a bit different times, the search engine was named AltaVista, we drank water from the tap and Java had different priorities.
The consequence is that for more than 20 years we are having slightly larger JAR files, slightly slower java execution (unless the JIT optimizes the call chain) and obnoxious warnings in the IDE suggesting that we better have package protected methods in nested classes instead of private
when we use it from top-level or other nested classes.
Nest Hosts
Now it seems that this 20-year technical debt will be solved. The http://openjdk.java.net/jeps/181 gets into Java 11 and it will solve this issue by introducing a new notion: nest. Currently, the Java bytecode contains some information about the relationship between classes. The JVM has information that a certain class is a nested class of another class and this is not only the name. This information could work for the JVM to decide on whether a piece of code in one class is allowed or is not allowed to access a private
member of another class, but the JEP-181 development has something more general. As times changed JVM is not the Java Virtual Machine anymore. Well, yes, it is, at least the name, however, it is a virtual machine that happens to execute bytecode compiled from Java. Or for the matter from some other languages. There are many languages that target the JVM and keeping that in mind the JEP-181 does not want to tie the new access control feature of the JVM to a particular feature of the Java language.
The JEP-181 defines the notion of a NestHost
and NestMembers
as attributes of a class. The compiler fills these fields and when there is access to a private member of a class from a different class then the JVM access control can check: are the two classes in the same nest or not? If they are in the same nest then the access is allowed, otherwise not. We will have methods added to the reflective access, so we can get the list of the classes that are in a nest.
Simple Nest Example
Using the
$ 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)
version of Java today we can make already experiments. We can create a simple class:
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(); } } }
Pretty simple and it does nothing. The private methods call each other. Without this the compiler sees that they simply do nothing and they are not needed and the byte code just does not contain them.
The class to read the nesting information
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"))); } }
The printout is as expected:
Mother bird is: class nesttest.NestingHost Nest dwellers are : nesttest.NestingHost nesttest.NestingHost$NestedClass2 nesttest.NestingHost$NestedClass1
Note that the nesting host is also listed among the nest members, though this information should be fairly obvious and redundant. However, such a use may allow some languages to disclose from the access the private members of the nesting host itself and let the access allow only for the nestlings.
Byte Code
The compilation using the JDK11 compiler generates the files
NestingHost$NestedClass1.class
NestingHost$NestedClass2.class
NestingHost.class
TestNest.class
There is no change. On the other hand if we look at the byte code using the javap
decompiler then we will see the following:
$ 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.class Last modified Aug 6, 2018; size 557 bytes MD5 checksum 5ce1e0633850dd87bd2793844a102c52 Compiled from "NestingHost.java" public class nesttest.NestingHost$NestedClass1 minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #5 // nesttest/NestingHost$NestedClass1 super_class: #6 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 3 Constant pool: *** CONSTANT POOL DELETED FROM THE PRINTOUT *** { public nesttest.NestingHost$NestedClass1(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 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/NestingHost public static #23= #2 of #20; // NestedClass2=class nesttest/NestingHost$NestedClass2 of class nesttest/NestingHost
If we compile the same class using the JDK10 compiler, then the disassembles lines are the following:
$ 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.class Last modified Aug 6, 2018; size 722 bytes MD5 checksum 8c46ede328a3f0ca265045a5241219e9 Compiled from "NestingHost.java" public class nesttest.NestingHost$NestedClass1 minor version: 0 major version: 54 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #6 // nesttest/NestingHost$NestedClass1 super_class: #7 // java/lang/Object interfaces: 0, fields: 0, methods: 3, attributes: 2 Constant pool: *** CONSTANT POOL DELETED FROM THE PRINTOUT *** { public nesttest.NestingHost$NestedClass1(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #2 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lnesttest/NestingHost$NestedClass1; static void access$100(nesttest.NestingHost$NestedClass1); descriptor: (Lnesttest/NestingHost$NestedClass1;)V flags: (0x1008) ACC_STATIC, ACC_SYNTHETIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method privateMethod:()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 x0 Lnesttest/NestingHost$NestedClass1; } SourceFile: "NestingHost.java" InnerClasses: public static #14= #6 of #25; // NestedClass1=class nesttest/NestingHost$NestedClass1 of class nesttest/NestingHost public static #27= #3 of #25; // NestedClass2=class nesttest/NestingHost$NestedClass2 of class nesttest/NestingHost
The Java 10 compiler generates the access$100
method. The Java 11 compiler does not. Instead, it has a nesting host field in the class file. We finally got rid of those synthetic methods that were causing surprises when listing all the methods in some framework code reflective.
Hack the nest
Let’s play a bit cuckoo. We can modify the code a bit so that now it does something:
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"); } } }
we also create a simple test class
package nesttest; public class HackNest { public static void main(String[] args) { // var nestling =new NestingHost().new NestedClass1(); // nestling.publicMethod(); } }
First, remove all the //
from the start of the lines and compile the project. It works like charm and prints out hallo
. After this copy the generated classes to a safe place, like the root of the project.
$ cp build/classes/java/main/nesttest/NestingHost\$NestedClass1.class . $ cp build/classes/java/main/nesttest/HackNest.class .
Let’s compile the project, this time with the comments and after this copy back the two class files from the previous compilation:
$ cp HackNest.class build/classes/java/main/nesttest/ $ cp NestingHost\$NestedClass1.class build/classes/java/main/nesttest/
Now we have a NestingHost
that knows that it has only one nestling: NestedClass2
. The test code, however, thinks that there is another nestling NestedClass1
and it also has a public method that can be invoked. This way we try to sneak an extra nestling into the nest. If we execute the code then we get an error:
$ 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 member at nesttest.NestingHost$NestedClass1.publicMethod(NestingHost.java:8) at nesttest.HackNest.main(HackNest.java:7)
It is important to recognize from the code that the line, which causes the error is the one where we want to invoke the private method. The Java runtime does the check only at that point and not sooner.
Do we like it or not? Where is the fail-fast principle? Why does the Java runtime start to execute the class and check the nest structure only when it is very much needed? The reason, as many times in the case of Java: backward compatibility. The JVM can check the nest structure consistency when all the classes are loaded. The classes are only loaded when they are used. It would have been possible to change the classloading in Java 11 and load all the nested classes along with the nesting host, but it would break backward compatibility. If nothing else the lazy singleton pattern would break apart and we do not want that. We love singleton, but only when single malt (it is).
Conclusion
The JEP-181 is a small change in Java. Most of the developers will not even notice. It is a technical debt eliminated and if the core Java project does not eliminate the technical debt then what should we expect from the average developer?
As the old Latin saying says: “Debitum technica necesse est deletur.”
Published on Java Code Geeks with permission by Peter Verhas, partner at our JCG program. See the original article here: Nested classes and private methods Opinions expressed by Java Code Geeks contributors are their own. |