Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do method() and super.method() refer to different things in an anonymous subclass?

I was solving some exercises to understand better how inner classes in java work. I found one quite interesting exercise. The condition of the exercise is to make printName() print "sout" instead of "main" with minimum changes. There is its code:

public class Solution {
    private String name;

    Solution(String name) {
        this.name = name;
    }

    private String getName() {
        return name;
    }

    private void sout() {
        new Solution("sout") {
            void printName() {
                System.out.println(getName());
                // the line above is an equivalent to:
                // System.out.println(Solution.this.getName);
            }
        }.printName();
    }

    public static void main(String[] args) {
        new Solution("main").sout();
    }
}

We've got an amusing situation - the two classes have is-A and has-A connections. It means that the anonymous inner class extends the outer class and also objects of the inner class have references to the objects of the outer class. If you run the code above, "main" will be printed. The child cannot invoke getName() of the parent through inheritance. But the child being inner class uses reference to parent(outer class) to access the method.

The simplest way to solve this task is to change the access modifier of getName() from private to anything else. So the child is able to use getName() through inheritance and thanks to late binding "sout" will be printed.

The another way to solve this task is to use super.getName().

private void sout() {
    new Solution("sout") {
        void printName() {
            System.out.println(super.getName());
        }
    }.printName();
}

And I cannot understand how it works. Can someone help me to understand this problem?

Thank you for trying)

like image 441
Yurii Koval Avatar asked Jul 02 '18 22:07

Yurii Koval


2 Answers

The Java Language Specification (JLS), in the context of the compiler resolving a method invocation expression, states

If the form is super . [TypeArguments] Identifier, then the class to search is the superclass of the class whose declaration contains the method invocation.

The class whose declaration contains the method invocation, in this case, is the anonymous Solution subclass and its superclass is Solution. The JLS, in the context of determining which instance will be used to invoke the method, then goes on to say

If the form is super . [TypeArguments] Identifier, then the target reference is the value of this.

this, in this case, refers to the instance of the anonymous Solution subclass. That instance's name field was initialized with the value "sout" as so that is what getName() returns.


In the original sample,

new Solution("sout") {
    void printName() {
        System.out.println(getName());
    }
}.printName();

the getName() method invocation is unqualified and therefore a different rule applies. That is

If there is an enclosing type declaration of which that method is a member, let T be the innermost such type declaration. The class or interface to search is T.

T, here, is the Solution class, since it is the innermost enclosing type of the anonymous Solution subclass and getName() is its member.

Then, the JLS states

Otherwise, let T be the enclosing type declaration of which the method is a member, and let n be an integer such that T is the n'th lexically enclosing type declaration of the class whose declaration immediately contains the method invocation. The target reference is the n'th lexically enclosing instance of this.

Again, T is Solution, the 1st lexically enclosing type since the class whose declaration immediately contains the method invocation is the anonymous Solution subclass. this is the anonymous Solution subclass instance. The target reference is therefore the 1st lexically enclosing instance of this, ie. the Solution instance, whose name field was initialized with the value "main". That's why the original code prints "main".

like image 73
Sotirios Delimanolis Avatar answered Oct 03 '22 21:10

Sotirios Delimanolis


The behavior may seem counter-intuitive but it becomes clear with a bit of refactoring.

So, the sout() method can actually be rewritten as

private void sout() {
  new Solution("sout") {
    void printName() {
      String name = getName();
      System.out.println(name);
    }
  }.printName();
}

public static void main(String[] args) {
  Solution mainSolution = new Solution("main");
  mainSolution.sout();
}

Calling the sout() method of the mainSolution object, creates a child Solution object that has an additional printName() method, which calls

getName();

which only declared in the parent mainSolution object.

If getName() is declared as private, it is not overriden but it is still accessible from the inner class, so getName() refers to the name of the mainSolution, i.e. to main.

If getName() has no modifier, or is declared as protected or public, then it is inherited (overriden) and refers to the child Solution object's name, i.e. to sout, hence "sout" will be printed.

By replacing getName() in sout() with

Solution.this.getName()

the string "main" will be printed in both scenarios.

By replacing it with either of

this.getName()
super.getName()

if the getName() method is declared as private a compilation error will occur, otherwise the string "sout" will be printed.

like image 35
PNS Avatar answered Oct 03 '22 21:10

PNS