Issue with constructors of nested class
Asked Answered
C

2

15

This question is about interesting behavior of Java: it produces additional (not default) constructor for nested classes in some situations.

This question is also about strange anonymous class, which Java produces with that strange constructor.


Consider the following code:

package a;

import java.lang.reflect.Constructor;

public class TestNested {    
    class A {    
        A() {
        }   

        A(int a) {
        }
    }    

    public static void main(String[] args) {
        Class<A> aClass = A.class;
        for (Constructor c : aClass.getDeclaredConstructors()) {
            System.out.println(c);
        }

    }
}

This will prints:

a.TestNested$A(a.TestNested)
a.TestNested$A(a.TestNested,int)

Ok. Next, lets make constructor A(int a) private:

    private A(int a) {
    }

Run program again. Receive:

a.TestNested$A(a.TestNested)
private a.TestNested$A(a.TestNested,int)

It is also ok. But now, lets modify main() method in such way (addition of new instance of class A creation):

public static void main(String[] args) {
    Class<A> aClass = A.class;
    for (Constructor c : aClass.getDeclaredConstructors()) {
        System.out.println(c);
    }

    A a = new TestNested().new A(123);  // new line of code
}

Then input becomes:

a.TestNested$A(a.TestNested)
private a.TestNested$A(a.TestNested,int)
a.TestNested$A(a.TestNested,int,a.TestNested$1) 

What is it: a.TestNested$A(a.TestNested,int,a.TestNested$1) <<<---??

Ok, lets again make constructor A(int a) package local:

    A(int a) {
    }

Rerun program again (we don't remove line with instance of A creation!), output is as in the first time:

a.TestNested$A(a.TestNested)
a.TestNested$A(a.TestNested,int)

Questions:

1) How this could be explained?

2) What is this third strange constructor?


UPDATE: Investigation shown following.

1) Lets try to call this strange constructor using reflection from other class. We will not able to do this, because there isn't any way to create instance of that strange TestNested$1 class.

2) Ok. Lets do the trick. Lets add to the class TestNested such static field:

public static Object object = new Object() {
    public void print() {
        System.out.println("sss");
    }
};

Well? Ok, now we could call this third strange constructor from another class:

    TestNested tn = new TestNested();
    TestNested.A a = (TestNested.A)TestNested.A.class.getDeclaredConstructors()[2].newInstance(tn, 123, TestNested.object);

Sorry, but I absolutely don't understand it.


UPDATE-2: Further questions are:

3) Why Java use special anonymous inner class for an argument type for this third synthetic constructor? Why not just Object type, of constructor with special name?

4) What Java could use already defined anonymous inner class for those purposes? Isn't this some kind of violation of security?

Consentaneous answered 10/1, 2013 at 19:47 Comment(16)
That's an anonymous inner class of A that's being passed to the third constructor. The question is why it's there or what's inside. Care to decompile?Adorable
Perhaps it's just a trick to make the private constructor accessible to the inner classes?Adorable
@JanDvorak, yes, and where this constructor is actually invoked? I suspect, then when creating instance of A passing int value in it. But, really, why?Consentaneous
If an inner class can call an outer class' methods, then this is a way to make this possible on the JVM level (JVM has no concept of inner classes). Then it would be used when trying to call new A(int) from within the inner class.Adorable
@JanDvorak what is the reason for this? Java could doing it easier, just modifying accessible state of the constructor.Consentaneous
but then it would be accessible form the outside...Adorable
@JanDvorak, yes agree. Ok. And what is this third inner class a.TestNested$1? Due to reflection investigation, this class even has not any constructors at all, and can not be instantiated using reflection.Consentaneous
It could be a marker class to indicate the access rights to the constructor. I, however, am not sure how does that class work as a token.Adorable
When you compile this java file, which .class files do you have?Coelom
@Coelom it is a really good question! I have: TestNested.class, TestNested$1.class and TestNested$A.class. This TestNested$1.class is a puzzle for me...Consentaneous
Furthermore! If we will create some static anonymous class in class TestNested (just static field static Object o = new Object {...}, then the number and names of generated .class files will not change. But if we will create second anonymous class we will receive new TestNested$2.class.Consentaneous
@partlov, please see my update about anonymous classes.Consentaneous
I decompiled generated classes and TestNested$1 is just one static class. Inner classes can exists just inside a context of outer class. In this example we are instantiating inner class inside a static method, not somewhere in outer class. So, my opinion here (but I'm not shore) that java creates one more class, that is same like our inner class, except it is static.Coelom
@partlov, Sorry, but nope (as I think). You see, I create just anonymous class new Object {...} and Java uses this anonymous class like that TestNested$1 class. I can even pass its instance to that strange constructor.Consentaneous
@Consentaneous In response to your second update: Java can't just use Object as the argument type because you would be able to have your own constructor with an Object argument and invoke it without reflection (by compiling A with a non-private A(int, Object) constructor, saving the TestNested$A.class file somewhere else, removing the new constructor, recompiling TestNested, and replacing the new TestNested$A.class with the version with the A(int, Object) constructor.) Using an anonymous type (TestNested$1) as the argument type of the synthetic constructor prevents that.Jiujitsu
@Consentaneous and about violating security: not possible since the argument types for the constructor are bound at compile-time (and when you recompile the outer class, it will also recompile the inner one). Even if you have an object that is the same type as the anonymous type, you can't have a constructor with that type as an argument, so the compiler will pick a different constructor to invoke.Jiujitsu
A
8

First of all, thank you for this interesting question. I was so intrigued that I could not resist taking a look at the bytecode. This is the bytecode of TestNested:

Compiled from "TestNested.java"
  public class a.TestNested {
    public a.TestNested();
      Code:
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        

    public static void main(java.lang.String[]);
      Code:
         0: ldc_w         #2                  // class a/TestNested$A
         3: astore_1      
         4: aload_1       
         5: invokevirtual #3                  // Method java/lang/Class.getDeclaredConstructors:()[Ljava/lang/reflect/Constructor;
         8: astore_2      
         9: aload_2       
        10: arraylength   
        11: istore_3      
        12: iconst_0      
        13: istore        4
        15: iload         4
        17: iload_3       
        18: if_icmpge     41
        21: aload_2       
        22: iload         4
        24: aaload        
        25: astore        5
        27: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        30: aload         5
        32: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        35: iinc          4, 1
        38: goto          15
        41: new           #2                  // class a/TestNested$A
        44: dup           
        45: new           #6                  // class a/TestNested
        48: dup           
        49: invokespecial #7                  // Method "<init>":()V
        52: dup           
        53: invokevirtual #8                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        56: pop           
        57: bipush        123
        59: aconst_null   
        60: invokespecial #9                  // Method a/TestNested$A."<init>":(La/TestNested;ILa/TestNested$1;)V
        63: astore_2      
        64: return        
  }

As you can see, the constructor a.TestNested$A(a.TestNested,int,a.TestNested$1) is invoked from your main method. Furthermore, null is passed as the value of the a.TestNested$1 parameter.

So let's take a look at the mysterious anonymous class a.TestNested$1:

Compiled from "TestNested.java"
class a.TestNested$1 {
}

Strange - I would have expected this class to actually do something. To understand it, let's take a look at the constructors in a.TestNested$A: class a.TestNested$A { final a.TestNested this$0;

  a.TestNested$A(a.TestNested);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field this$0:La/TestNested;
       5: aload_0       
       6: invokespecial #3                  // Method java/lang/Object."<init>":()V
       9: return        

  private a.TestNested$A(a.TestNested, int);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field this$0:La/TestNested;
       5: aload_0       
       6: invokespecial #3                  // Method java/lang/Object."<init>":()V
       9: return        

  a.TestNested$A(a.TestNested, int, a.TestNested$1);
    Code:
       0: aload_0       
       1: aload_1       
       2: iload_2       
       3: invokespecial #1                  // Method "<init>":(La/TestNested;I)V
       6: return        
}

Looking at the package-visible constructor a.TestNested$A(a.TestNested, int, a.TestNested$1), we can see that the third argument is ignored.

Now we can explain the constructor and the anonymous inner class. The additional constructor is required in order to circumvent the visibility restriction on the private constructor. This additional constructor simply delegates to the private constructor. However, it cannot have the exact same signature as the private constructor. Because of this, the anonymous inner class is added to provide a unique signature without colliding with other possible overloaded constructors, such as a constructor with signature (int,int) or (int,Object). Since this anonymous inner class is only needed to create a unique signature, it does not need to be instantiated and does not need to have content.

Assorted answered 10/1, 2013 at 21:23 Comment(1)
Very interesting investigation of byte-code. Thank you a lot!Consentaneous
L
10

The third constructor is a synthetic constructor generated by the compiler, in order to allow access to the private constructor from the outer class. This is because inner classes (and their enclosing classes' access to their private members) only exist for the Java language and not the JVM, so the compiler has to bridge the gap behind the scenes.

Reflection will tell you if a member is synthetic:

for (Constructor c : aClass.getDeclaredConstructors()) {
    System.out.println(c + " " + c.isSynthetic());
}

This prints:

a.TestNested$A(a.TestNested) false
private a.TestNested$A(a.TestNested,int) false
a.TestNested$A(a.TestNested,int,a.TestNested$1) true

See this post for further discussion: Eclipse warning about synthetic accessor for private static nested classes in Java?

EDIT: interestingly, the eclipse compiler does it differently than javac. When using eclipse, it adds an argument of the type of the inner class itself:

a.TestNested$A(a.TestNested) false
private a.TestNested$A(a.TestNested,int) false
a.TestNested$A(a.TestNested,int,a.TestNested$A) true

I tried to trip it up by exposing that constructor ahead of time:

class A {    
    A() {
    }   

    private A(int a) {
    }

    A(int a, A another) { }
}

It dealt with this by simply adding another argument to the synthetic constructor:

a.TestNested$A(a.TestNested) false
private a.TestNested$A(a.TestNested,int) false
a.TestNested$A(a.TestNested,int,a.TestNested$A) false
a.TestNested$A(a.TestNested,int,a.TestNested$A,a.TestNested$A) true
Larceny answered 10/1, 2013 at 21:12 Comment(10)
Thank you for your reply! It is clear, that this method is synthetic. All my question is about this fact, that Java produces such method. The questions are: why it needs special InnerClass for this purposes, why it could use existing inner class for this purposes, and where in specification this things are described?Consentaneous
Very good answer. Compiler add some synthetically created class and uses it. In decompiled code it just write null to that parameter: new A(123, null). In that way compiler is shore that you will not have any methods with that signature. I just don't know why they didn't throw compiler error here. We are calling private method of inner class here!?Coelom
@partlov, great! It is really just null. Ok! But may be there is some way for invoke this constructor direct from source code, not through reflection?Consentaneous
@Consentaneous I think it is not possibly. We don't have that "class" while we write our code. That class exists only after compile. So, I think it is only possible with reflection.Coelom
We actually have that class, it could be public static anonymous instance of hand-made class in TestNested. The problem is, how we can program invoke this constructor (not through Reflection).Consentaneous
@Consentaneous I don't think it's possible to invoke the synthetic constructor from Java source code. In the bytecode, the constructor invocation is (I think) an invokespecial bytecode instruction, which invokes a specific constructor. The compiler is not going to generate bytecode that invokes the synthetic constructor.Jiujitsu
Yeah, but don't you think it is just artificially created situation. Maybe compiler has this kind of logic: "Ok, I will add constructor with some anonymous class if it exists somewhere here (if for example Andremoniy created it :)), but if such doesn't exist I will create one". :)Coelom
@partlov, :)))) lol, yes, I also thought so. It could be true :)Consentaneous
@PaulBellora, how it could be so? Eclipse and my IDEA are using same JDK compiler. Aren't them? Eclipse has own Java compiler? O_o Why there is this difference?Consentaneous
@Consentaneous See this post.Larceny
A
8

First of all, thank you for this interesting question. I was so intrigued that I could not resist taking a look at the bytecode. This is the bytecode of TestNested:

Compiled from "TestNested.java"
  public class a.TestNested {
    public a.TestNested();
      Code:
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        

    public static void main(java.lang.String[]);
      Code:
         0: ldc_w         #2                  // class a/TestNested$A
         3: astore_1      
         4: aload_1       
         5: invokevirtual #3                  // Method java/lang/Class.getDeclaredConstructors:()[Ljava/lang/reflect/Constructor;
         8: astore_2      
         9: aload_2       
        10: arraylength   
        11: istore_3      
        12: iconst_0      
        13: istore        4
        15: iload         4
        17: iload_3       
        18: if_icmpge     41
        21: aload_2       
        22: iload         4
        24: aaload        
        25: astore        5
        27: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        30: aload         5
        32: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        35: iinc          4, 1
        38: goto          15
        41: new           #2                  // class a/TestNested$A
        44: dup           
        45: new           #6                  // class a/TestNested
        48: dup           
        49: invokespecial #7                  // Method "<init>":()V
        52: dup           
        53: invokevirtual #8                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
        56: pop           
        57: bipush        123
        59: aconst_null   
        60: invokespecial #9                  // Method a/TestNested$A."<init>":(La/TestNested;ILa/TestNested$1;)V
        63: astore_2      
        64: return        
  }

As you can see, the constructor a.TestNested$A(a.TestNested,int,a.TestNested$1) is invoked from your main method. Furthermore, null is passed as the value of the a.TestNested$1 parameter.

So let's take a look at the mysterious anonymous class a.TestNested$1:

Compiled from "TestNested.java"
class a.TestNested$1 {
}

Strange - I would have expected this class to actually do something. To understand it, let's take a look at the constructors in a.TestNested$A: class a.TestNested$A { final a.TestNested this$0;

  a.TestNested$A(a.TestNested);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field this$0:La/TestNested;
       5: aload_0       
       6: invokespecial #3                  // Method java/lang/Object."<init>":()V
       9: return        

  private a.TestNested$A(a.TestNested, int);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #2                  // Field this$0:La/TestNested;
       5: aload_0       
       6: invokespecial #3                  // Method java/lang/Object."<init>":()V
       9: return        

  a.TestNested$A(a.TestNested, int, a.TestNested$1);
    Code:
       0: aload_0       
       1: aload_1       
       2: iload_2       
       3: invokespecial #1                  // Method "<init>":(La/TestNested;I)V
       6: return        
}

Looking at the package-visible constructor a.TestNested$A(a.TestNested, int, a.TestNested$1), we can see that the third argument is ignored.

Now we can explain the constructor and the anonymous inner class. The additional constructor is required in order to circumvent the visibility restriction on the private constructor. This additional constructor simply delegates to the private constructor. However, it cannot have the exact same signature as the private constructor. Because of this, the anonymous inner class is added to provide a unique signature without colliding with other possible overloaded constructors, such as a constructor with signature (int,int) or (int,Object). Since this anonymous inner class is only needed to create a unique signature, it does not need to be instantiated and does not need to have content.

Assorted answered 10/1, 2013 at 21:23 Comment(1)
Very interesting investigation of byte-code. Thank you a lot!Consentaneous

© 2022 - 2024 — McMap. All rights reserved.