15

Why doesn't this code compile?

public class A {
    public class B extends A {
        public B(A a) { }
    }
    void foo() {
        A a = new A();
        new B(a) { };
    }
}

A.java:[7,17] cannot reference this before supertype constructor has been called

Compilation is successful if either of these changes are made:

  • B is private instead of public
  • line 7 reads new B(A); instead of new B(A) { }

Using javac version: 1.6.0_20

Chris Martin
  • 30,334
  • 10
  • 78
  • 137
  • 1
    What do you think `new B(a) {}` does? – kyoryu Aug 01 '10 at 19:43
  • new B(a) {} should instantiate an anonymous subclass of B whose default constructor calls super(a) – Chris Martin Aug 01 '10 at 19:45
  • Obviously, the actual code I'm trying to write has overriding methods in that block - new B(a) { /* stuff goes here */} - but I removed unnecessary code for the purpose of simplifying this question. – Chris Martin Aug 01 '10 at 19:47
  • +1; good hard question and awesome job at minimizing the snippet to illustrate the problem. – polygenelubricants Aug 01 '10 at 19:54
  • "B is private instead of public" - this does not make the problem go away in my case. Making it `static` obviously does, though. – polygenelubricants Aug 01 '10 at 19:59
  • polygenelubricants, have you tried compiling it? It works (for me, at least) if B is private. If you're getting different behavior, then maybe we're looking at a javac bug. – Chris Martin Aug 01 '10 at 20:04
  • 2
    It's #90 in Joshua Bloch's Java Puzzlers. Available as #9 in its sample chapter: http://www.javapuzzlers.com/java-puzzlers-sampler.pdf. Personally, I can't understand it even after reading the solution. – axtavt Aug 01 '10 at 20:06
  • @axtavt: Yes, you're right I think this is related (the anon class is analogous to `Inner2 extends Inner1`); @Christopher: I was able to compile with `javac` making `private class B`, but Eclipse does not compile! Yet another case where `javac` and Eclipse compiler disagrees, it seems. – polygenelubricants Aug 01 '10 at 20:14
  • 1
    I'll also note that Intellij IDEA disagrees with everyone - It thinks all of this code is fine. – Chris Martin Aug 01 '10 at 20:26

2 Answers2

21

It should be noted that Eclipse, javac, and Intellij IDEA exhibit differences in behaviors with regards to these snippets. javac and the Java Puzzlers behavior is used for reference in this discussion.

I was able to cut down the snippet to the following:

public class A {
    class B extends A {
    }
    void foo() {
        new B() { }; // DOES NOT COMPILE!!
    }
}

This scenario is discussed in Java Puzzlers, Puzzle 90: It's Absurd, It's a Pain, It's Superclass!

The snippet given is the following:

public class Outer {                   // "A"
    class Inner1 extends Outer  {}     // "B"
    class Inner2 extends Inner1 {}     // "B" anonymous
}
// DOES NOT COMPILE!!

The problem is that due to how default constructor is defined, we really have the following:

// Same as above but with default constructor included explicitly
public class Outer {
    class Inner1 extends Outer  { 
        Inner1() { super(); }
    }
    class Inner2 extends Inner1 {
        Inner2() { super(); }    
    }
}
// STILL DOES NOT COMPILE!!

The problem is that Inner2's superclass is itself an inner class Inner1, thus making Inner2's default constructor illegal since it requires an enclosing instance to be supplied to the constructor.

The "brute-force" way to fix the problem is to provide this explicitly with a qualified-this expression:

// "brute-force" fix
public class Outer {
    class Inner1 extends Outer  { 
        Inner1() { super(); }
    }
    class Inner2 extends Inner1 {
        Inner2() { Outer.this.super(); }    
    }
}
// NOW COMPILES!

However, the puzzle prescribes that such complicated situation is best avoided in the first place. Here are some quotes:

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 non-static. Most member classes can and should be declared static.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
polygenelubricants
  • 376,812
  • 128
  • 561
  • 623
  • 1
    The document linked by axtavt simplifies this even further. using only named classes: public class A { class B extends A { } class C extends B { } } This time, even IDEA notices that something is wrong: "Cannot reference C.this before supertype constructor has been called" – Chris Martin Aug 01 '10 at 20:31
  • 1
    Great answer - This makes technical sense, and furthermore, its conclusion is absolutely correct: I ran into trouble, ultimately, because B really should have been static in the first place. I ended up making the "B" in my real code a top-level class. – Chris Martin Aug 01 '10 at 20:38
0

Not sure what the goal is exactly, but try this. Note I also cut out the passing of A as an argument as being non-static classes they are already linked. I included the syntax for referring to the outer class "this" for those situations were the inner class may cover up an outer class field/method

public class A {

    protected String field = "a";

    public class B extends A {
        protected String field = "b";

        public B() {
            System.out.println("" + A.this.field + " " + this.field);
        }

        @Override
        void foo() {
            System.out.println("b.foo()");
        }
    }

    void foo() {
        A a = new A();
        a.field = "a2";
        B b = a.new B() {
            @Override
            void foo() {
                System.out.println("b.anon.foo()");
            }
        };

        b.foo();
    }

    public static void main(String[] args) {
        new A().foo();
    }
}

Running this will output:

a2 b
b.anon.foo()

Hope this helps!

David Blevins
  • 19,178
  • 3
  • 53
  • 68