Let's take a dive into the world of nested classes in Java. All is not always as it seems. Check out this post to find out more about nested classes and private methods.
by Peter Verhas
Nested Classes and Private Methods
When you have a class inside another class, they can see each others'
private
methods. However, this fact is not well known amongst Java developers. Many candidates during interviews will say that private
is a visibility that lets code see a member as if it 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 are in. When we have both nested and inner classes, the private
member and the code using it can be 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
and calls the method in A$B.class
. The JVM checks the access control. When we discussed this with juniors, somebody suggested that the JVM does not care about 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
. Once you start the application, you will see that the JVM cares about the access modifiers a lot. Still, you can invoke in the example above from the A.class
method in the 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 concerned. 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 this in greater detail a little more than four 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 for this hack. And also, perhaps, the hack of the
Integer
cache makes small Integer
objects (typical test values) 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 realize that I am a Monty Python fan).
The reason for this is that nested classes were not part of 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 the JIT compiler, JDBC, RMI, reflection, and some other things that we now take for granted. That time, the question was not if the solution is nice and clean, but, rather, the question was if Java will survive at all and if it will be a mainstream programming language. During that time, I was still working as a sales rep and coding was only a hobby since coding jobs were still scarce in Eastern Europe.
After 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/181gets 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 evolve, 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 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, it is 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 following:
We can create a simple class:
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. Because of this, the bytecode just does not contain them.
The following class reads the nesting information:
The following class reads the nesting information:
The printout is as expected:
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 and the private members of the nesting host itself, allowing access only for the nestlings.
Bytecode
The compilation using the JDK11 compiler generates the following files.
There is no change. On the other hand, if we look at the bytecode using the
javap
decompiler, then we will see the following:
If we compile the same class using the JDK10 compiler, then the disassembles lines are the following:
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 like this:
We can also create a simple test class:
First, remove all the
//
from the start of the lines and compile the project. It works like a charm and prints out hallo
. After this copy, the generated classes are transfered to a safe place, like the root of the project.
Let's compile the project. This time, let's do it with the comments, and after this copy, back the two class files from the previous compilation:
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:
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.
Conclusion
The JEP-181 is a small change in Java. Most 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 goes: "Debitum technica necesse est deletur."