Wednesday, 19 September 2012

Puzzle 90: It's Absurd, It's a Pain, It's Superclass!


The following program doesn't actually do anything. Worse, it won't compile. Why not? How can you fix it?
public class Outer {

    class Inner1 extends Outer {}

    class Inner2 extends Inner1 {}

}


Solution 90: It's Absurd, It's a Pain, It's Superclass

This program looks too simple to have anything wrong with it, but if you try to compile it, you get this helpful error message:
Outer.java:3: cannot reference this before

              supertype constructor has been called

    class Inner2 extends Inner1 {} 

    ^


OK, maybe it's not so helpful, but we'll work on that. The problem is that the compiler-generated default constructor for Inner2 cannot find an appropriate enclosing instance for its super invocation. Let's look at the program with the default constructors included explicitly:
public class Outer {

    public Outer() {}



    class Inner1 extends Outer {

        public Inner1() {

            super();  // invokes Object() constructor

        }

    }



    class Inner2 extends Inner1 {

        public Inner2() {

            super();  // invokes Inner1() constructor

        }

    }

}


Now the error message gives a bit more information:
Outer.java:12: cannot reference this before

               supertype constructor has been called

    super(); // invokes Inner1() constructor

    ^


Because the superclass of Inner2 is itself an inner class, an obscure language rule comes into play. As you know, the instantiation of an inner class, such as Inner1, requires an enclosing instance to be supplied to the constructor. Normally, it is supplied implicitly, but it can also be supplied explicitly with a superclass constructor invocation of the form expression.super(args) [JLS 8.8.7].
If the enclosing instance is supplied implicitly, the compiler generates the expression: It uses the this reference for the innermost enclosing class of which the superclass is a member. This is, admittedly, quite a mouthful, but it is what the compiler does. In this case, the superclass is Inner1. Because the current class, Inner2, extends Outer indirectly, it has Inner1 as an inherited member. Therefore, the qualifying expression for the superclass constructor is simply this. The compiler supplies an enclosing instance, rewriting super to this.super. Had we done this ourselves, the compilation error would have made even more sense:
Outer.java:12: cannot reference this before

               supertype constructor has been called

    this.super();

    ^


Now the problem is clear: The default Inner2 constructor attempts to reference this before the superclass constructor has been called, which is illegal [JLS 8.8.7.1]. The brute-force way to fix this problem is to provide the reasonable enclosing instance explicitly:
public class Outer {

    class Inner1 extends Outer { }

    

    class Inner2 extends Inner1 {

        public Inner2() {

            Outer.this.super();

        }

    }

}


This compiles, but it is mind-numbingly complex. There is a better solution: Whenever you write a member class, ask yourself, Does this class really need an enclosing instance? If the answer is no, make it static. Inner classes are sometimes useful, but they can easily introduce complications that make a program difficult to understand. They have complex interactions with generics (Puzzle 89), reflection (Puzzle 80), and inheritance (this puzzle). If you declare Inner1 to be static, the problem goes away. If you also declare Inner2 to be static, you can actually understand what the program does: a nice bonus indeed.
In summary, it is rarely appropriate for one class to be both an inner class and a subclass of another. More generally, it is rarely appropriate to extend an inner class; if you must, think long and hard about the enclosing instance. Also, prefer static nested classes to nonstatic [EJ Item 18]. Most member classes can and should be declared static.

No comments:

Post a Comment

Your comments are welcome!