Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do extensions of inner classes get duplicate outer class references?

I have the following Java file:

class Outer {
    class Inner { public int foo; }
    class InnerChild extends Inner {}
}

I compiled then disassembled the file using this command:

javac test.java && javap -p -c Outer Outer.Inner Outer.InnerChild

This is the output:

Compiled from "test.java"
class Outer {
  Outer();
    Code:
       0: aload_0
       1: invokespecial #1            // Method java/lang/Object."<init>":()V
       4: return
}
Compiled from "test.java"
class Outer$Inner {
  public int foo;

  final Outer this$0;

  Outer$Inner(Outer);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1            // Field this$0:LOuter;
       5: aload_0
       6: invokespecial #2            // Method java/lang/Object."<init>":()V
       9: return
}
Compiled from "test.java"
class Outer$InnerChild extends Outer$Inner {
  final Outer this$0;

  Outer$InnerChild(Outer);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1            // Field this$0:LOuter;
       5: aload_0
       6: aload_1
       7: invokespecial #2            // Method Outer$Inner."<init>":(LOuter;)V
      10: return
}

The first inner class has its this$0 field, pointing to the instance of Outer. That's fine. The second inner class, which extends the first, has a duplicate field of the same name, which it initializes before calling the super class's constructor with the same value.

The purpose of the int foo field above is just to confirm that inherited fields from the superclass do not show up in the javap output of a child class's dissassembly.

The first this$0 field is not private, so InnerChild should be able to use it. The extra field just seems to waste memory. (I first discovered it using a memory analysis tool.) What is its purpose and is there a way I can get rid of it?

like image 990
Boann Avatar asked Dec 31 '13 01:12

Boann


2 Answers

The two classes may not be inner classes of the same class (if you had a complex hierarchy), so there does exist cases where the two references would be different.

For example:

 class Outer {

     class InnerOne {
     } 

     class Wrapper {
         class InnerTwo extends InnerOne {
         }
     }
 }

InnerTwo has a reference to Wrapper, InnerOne has a reference to Outer.

You can try it with the following Java code:

public class Main{

    static class Outer {

        class InnerOne {
             String getOuters() {
                return this+"->"+Outer.this;
             }
        } 

        class Wrapper {
           class InnerTwo extends InnerOne {
             String getOuters() {
               return this+"->"+Wrapper.this+"->"+super.getOuters();
             }
           }
        }
    }

    public static void main(String[] args){
       Outer o = new Outer();
       Outer.Wrapper w = o.new Wrapper();
       Outer.Wrapper.InnerTwo i2 = w.new InnerTwo();
       System.out.println(w);
       System.out.println(i2);
       System.out.println(i2.getOuters());
    }

}

I've set it up as a snippet on tryjava8: http://www.tryjava8.com/app/snippets/52c23585e4b00bdc99e8a96c

Main$Outer$Wrapper@1448139f 
Main$Outer$Wrapper$InnerTwo@1f7f1d70 
Main$Outer$Wrapper$InnerTwo@1f7f1d70->Main$Outer$Wrapper@1448139f->Main$Outer$Wrapper$InnerTwo@1f7f1d70->Main$Outer@6945af95

You can see that the two getOuters() calls are referencing a different object.

like image 92
Tim B Avatar answered Oct 21 '22 00:10

Tim B


@TimB's answer is interesting because it shows how subtle the outer class references can be, but I think I've found a simpler case:

class Outer {
    class Inner {}
}

class OuterChild extends Outer {
    class InnerChild extends Inner {}
}

In this case, InnerChild's this$0 reference is of type Child rather than Outer, so it can't use the this$0 field from its parent because even though the value is identical, it's the wrong type. I suspect this is the origin of the extra field. Still, I believe Javac could eliminate it when InnerChild and Inner have the same outer class, because then the field has the same value and the same type. (I would report it as a bug but they never fix them, and if it's not a bug they don't give feedback anyway.)

I've finally figured out some decent workarounds though.

The plainest workaround (which took me far too long to think of) is to make the member classes "static", and store the ref to Outer as an explicit field on the base class:

class Outer {
    static class Inner {
        protected final Outer outer;

        Inner(Outer outer) {
            this.outer = outer;
        }
    }

    static class InnerChild extends Inner {
        InnerChild(Outer outer) {
            super(outer);
        }
    }
}

Through the outer field, both Inner and InnerChild can access instance members of Outer (even private ones). This is equivalent to the code that I think Javac should generate anyway for the original non-static classes.

Second workaround: I discovered by complete accident that the child class of a non-static member class may be static! All these years in Java I never knew about the obscure syntax which makes this work... it is not in the official tutorials. I would have always believed it impossible except that Eclipse automatically filled in the constructor for me. (However, Eclipse doesn't seem to want to repeat this magic... I'm so confused.) Anyway, apparently it's called a "qualified superclass constructor invocation", and it's buried in the JLS in section 8.8.7.1.

class Outer {
    class Inner {
        protected final Outer outer() { return Outer.this; }
    }

    static class InnerChild extends Inner {
        InnerChild(Outer outer) {
            outer.super(); // wow!
        }
    }
}

This is very similar to the previous workaround, except Inner is a instance class. InnerChild avoids the duplicate this$0 field because it's static. Inner provides a getter so that InnerChild can access the ref to Outer (if it wants to). This getter is final and non-virtual, so is trivial for the VM to inline.

A third workaround possibility is to store the outer class ref only in InnerChild, and let Inner access it through a virtual method:

class Outer {
    static abstract class Inner {
        protected abstract Outer outer();
    }

    class InnerChild extends Inner {
        @Override protected Outer outer() { return Outer.this; }
    }
}

This is probably less useful(?). One advantage is it lets InnerChild have a simpler constructor invocation, since the ref to its enclosing class will usually be passed to it implicitly. It might also be an obscure optimization if InnerChild has a different enclosing class (OuterChild, extending Outer), which it needs to access members of more often than Inner needs to access members of Outer -- it lets InnerChild use members of OuterChild without casting, but requires Inner to invoke a virtual method to access members of Outer.

In practice, all these workarounds are a bit of a nuisance, so they're only worth it if you have many thousands of instances of classes like this (which I do!).


Update!

Just realized the "qualified superclass constructor invocation" is a big piece of the puzzle as to why Javac generates duplicate fields in the first place. It is possible for an instance of InnerChild, extending Inner, both member classes of Outer, to have a different enclosing instance to what its superclass considers to be the enclosing instance:

class Outer {
    class Inner {
        Inner() {
            System.out.println("Inner's Outer: " + Outer.this);
        }
    }

    class InnerChild extends Inner {
        InnerChild() {
            (new Outer()).super();
            System.out.println("InnerChild's Outer: " + Outer.this);
        }
    }
}

class Main {
    public static void main(String[] args) {
        new Outer().new InnerChild();
    }
}

Output:

Inner's Outer: Outer@1820dda
InnerChild's Outer: Outer@15b7986

I still think that with some effort, Javac could eliminate the duplicate field in the common case, but I'm not sure. It's certainly a more complex problem than I first thought.

like image 44
Boann Avatar answered Oct 20 '22 23:10

Boann