Translate

Monday, October 1, 2018

Nested Classes in Java

Source: https://dzone.com/articles/nesting-java-classes

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 privatemethod 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 Integercache 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 Integercache 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:
$ 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)

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. Because of this, the bytecode just does not contain them.
The following class reads 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 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:
$ 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 like this:
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 can 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 a charm and prints out hallo. After this copy, the generated classes are transfered 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, let's do it 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. 

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."