Почему методы() и super.method() ссылаются на разные вещи в анонимном подклассе?

Я решал некоторые упражнения, чтобы лучше понять, как работают внутренние классы в java. Я нашел одно довольно интересное упражнение. Условие упражнения заключается в том, чтобы printName() печатать "sout" вместо "main" с минимальными изменениями. Существует его код:

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();
    }
}

У нас есть забавная ситуация - у двух классов есть -A и есть-A-соединения. Это означает, что анонимный внутренний класс расширяет внешний класс, а также объекты внутреннего класса имеют ссылки на объекты внешнего класса. Если вы запустите код выше, будет напечатан "main". Ребенок не может вызывать getName() родителя через наследование. Но внутренний класс child использует ссылку на родительский (внешний класс) для доступа к методу.

Самый простой способ решить эту задачу - изменить модификатор доступа getName() от private к чему-либо еще. Таким образом, ребенок может использовать getName() через наследование и благодаря позднему связыванию "sout" будет напечатан.

Другой способ решить эту задачу - использовать super.getName().

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

И я не понимаю, как это работает. Может кто-нибудь помочь мне понять эту проблему?

Спасибо за попытку)

Ответ 1

Спецификация языка Java (JLS) в контексте компилятора, разрешающего выражение вызова метода,

Если форма super. [TypeArguments] Identifier super. [TypeArguments] Identifier, тогда класс для поиска - это суперкласс класса, декларация которого содержит вызов метода.

Класс, декларация которого содержит вызов метода, в этом случае является подклассом анонимного Solution а его суперкласс - это Solution. JLS в контексте определения того, какой экземпляр будет использоваться для вызова метода, затем далее говорит:

Если форма super. [TypeArguments] Identifier super. [TypeArguments] Identifier, то целевой ссылкой является значение this.

this в данном случае относится к экземпляру подкласса анонимного Solution. Это поле name экземпляра было инициализировано значением "sout" так, как getName() возвращает getName().


В исходном образце,

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

getName() метода getName() является неквалифицированным и, следовательно, применяется другое правило. То есть

Если есть объявление типа закрытого типа, членом которого является этот метод, пусть T является самым внутренним объявлением такого типа. Класс или интерфейс для поиска - T

T, здесь, является классом Solution, поскольку он является самым внутренним охватывающим типом подкласса анонимного Solution и getName() является его членом.

Тогда состояние JLS

В противном случае пусть T - объявляющее объявление типа, в котором этот метод является членом, и пусть n является целым числом, так что T является объявлением типа n'th lexically enclosing класса, объявление которого немедленно содержит вызов метода. Целевой ссылкой является n-й лексически охватывающий экземпляр this.

Опять же, T является Solution, 1-м лексически охватывающим типом, так как класс, декларация которого немедленно содержит вызов метода, является подклассом анонимного Solution. this анонимный экземпляр подкласса Solution. Поэтому целевой ссылкой является 1-й лексически охватывающий экземпляр this, т.е. экземпляр Solution, чье поле name было инициализировано значением "main". Вот почему исходный код печатает "main".

Ответ 2

Поведение может показаться противоречивым, но это становится понятным с небольшим количеством рефакторинга.

Таким образом, метод sout() может быть переписан как

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();
}

Вызов метода sout() объекта mainSolution, создает дочерний объект Solution который имеет дополнительный printName(), который вызывает

getName();

который объявляется только в родительском объекте mainSolution.

Если getName() объявлен как закрытый, он не переопределяется, но он все еще доступен из внутреннего класса, поэтому getName() ссылается на имя mainSolution, то есть на main.

Если getName() не имеет модификатора или объявлен как защищенный или общедоступный, он наследуется (переопределяется) и ссылается на дочернее имя объекта Solution, то есть на sout, поэтому будет напечатан "sout".

Заменив getName() в sout() на

Solution.this.getName()

строка "main" будет напечатана в обоих сценариях.

Заменив его любым из

this.getName()
super.getName()

если метод getName() объявлен как private, произойдет ошибка компиляции, иначе будет напечатана строка "sout".