javajvmjavacbytecode.class-file

How do inner class access enclosing class' private members in higher versions of Java?


I have been trying to understand the mechanism of nested classes in java.

Consider the following code:

public class OuterClass {

private String privateOuterField = "Private Outer Field";

public static String publicStaticOuterField = "Public static outer field";

private static String privateStaticOuterField = "private static outer field";


class InnerClass{
    private String privateInnerField = "Private Inner Field";
    
    //non-final static data members not allowed in java 1.8 but allowed in java 17.0
    //private static String innerClassStaticField = "Private Inner Class Static Field";   
    
    public void accessMembers() {
        System.out.println(privateOuterField);
        System.out.println(publicStaticOuterField);
    }
}

static class StaticInnerClass{
    
    private String privateStaticInnerField = "Private Inner Field of static class";
    
    public void accessMembers(OuterClass outer) {
        //System.out.println(privateOuterField);  //error
        System.out.println(outer.privateOuterField);
        System.out.println(publicStaticOuterField);
        System.out.println(privateStaticOuterField);
    }
}
    
public static void main(String[] args) {
    
    OuterClass outerObj = new OuterClass();
    OuterClass.InnerClass innerObj = outerObj.new InnerClass();
    
    StaticInnerClass staticInnerObj = new StaticInnerClass();
    
    innerObj.accessMembers();
    staticInnerObj.accessMembers(outerObj);
    

}

}

I know that inner classes are a phenomenon of the compiler, virtual machine are not aware of them. Inner classes are translated into regular class files with $ delimiting outer and inner class name.

To understand this mechanism in more details, I tried to disassemble the class file compiled in java version 1.8 using javap -p command.

I got the following results: OuterClass:

public class staticNestedClasses.OuterClass {
  private java.lang.String privateOuterField;
  public static java.lang.String publicStaticOuterField;
  private static java.lang.String privateStaticOuterField;
  public staticNestedClasses.OuterClass();
  public static void main(java.lang.String[]);
  static java.lang.String access$000(staticNestedClasses.OuterClass);
  static java.lang.String access$100();
  static {};
}

InnerClass:

class staticNestedClasses.OuterClass$InnerClass {
  private java.lang.String privateInnerField;
  final staticNestedClasses.OuterClass this$0;
  staticNestedClasses.OuterClass$InnerClass(staticNestedClasses.OuterClass);
  public void accessMembers();
}

Here we can see that the compiler passes the reference of outer class to inner class through constructor so that it can assess outer class's fields and methods:

staticNestedClasses.OuterClass$InnerClass(staticNestedClasses.OuterClass);

this outer class reference is stored in final staticNestedClasses.OuterClass this$0

But OuterClass$InnerClass class cannot directly access private members through outer class reference, so whenever the compiler detects access to private members from inner class, it generates accessor method (or getter methods) in outer class.

In the disassembled file of outer class we can see that the compiler generated these accessor methods.

static java.lang.String access$000(staticNestedClasses.OuterClass);
static java.lang.String access$100();

But when I compiled the same code in java 17.0 and disassembled the class file I got the following result.

Outer class:

public class staticNestedClasses.OuterClass {
  private java.lang.String privateOuterField;
  public static java.lang.String publicStaticOuterField;
  private static java.lang.String privateStaticOuterField;
  public staticNestedClasses.OuterClass();
  public static void main(java.lang.String[]);
  static {};
}

OuterClass$InnerClass:

class staticNestedClasses.OuterClass$InnerClass {
  private java.lang.String privateInnerField;
  private static java.lang.String innerClassStaticField;
  final staticNestedClasses.OuterClass this$0;
  staticNestedClasses.OuterClass$InnerClass(staticNestedClasses.OuterClass);
  public void accessMembers();
  static {};
}

Here compiler did not generate any accessor methods but the code worked fine.

So how did the inner class access private members of outer class?


Solution

  • The only thing that stops a class from accessing another class’s private members, is the JVM (or precisely its verifier) rejecting the access. So all it needs to make it possible, is the collaboration of the JVM to allow it.

    While Java 1.1 introduced inner classes in a way that did not require changes to the JVM, the JVM has gone through so many changes in the meanwhile, that it is rather surprising that it took until Java 11 to change that.

    Java 11 introduced the NestHost and NestMembers bytecode attributes to allow class files to denote that they belong to a so called “nest”. All classes belonging to the same nest are allowed to access each others private members. As said, the only thing that needed to be changed, is the JVM’s verifier to allow such access. And, of course, the compiler to utilize this feature. See also JEP 181.

    So you could say that the JVM still doesn’t know anything about inner classes, because which classes belong to a nest, is decided by whichever tool generated the class files (e.g. the Java source code compiler). So it is possible to produce class files with other tools using nests without following the inner class semantic.

    For completion, it should be mentioned that class files do also contain the information about inner class relationships, using the InnerClasses attribute. But this is only used by compilers and Reflection, whereas the JVM doesn’t use this information when deciding whether an access is legal or not.