Java-трюк, переопределение дочернего класса

Я тренируюсь для экзамена по Java, и я сталкивался с тем, чего не понимаю в прошлом году. Вот код

class Mother {
    int var = 2;

    int getVar() {
        return var;
    }
}

class Daughter extends Mother {
    int var = 1;

    int getVar() { 
        return var;
    }

    public static void main(String[] args) {
        Mother m = new Mother();
        System.out.println(m.var);
        System.out.println(m.getVar());
        m = new Daughter();
        System.out.println(m.var);
        System.out.println(m.getVar());
    }
}

Вопрос: "Каков результат этой программы?". Я бы пошел с 2 2 1 1, но когда компилирую и запускаю этот кусок кода, я получаю 2 2 2 1.

Кто-нибудь может объяснить мне, почему?

Спасибо за чтение!

Ответы

Ответ 1

Вызов метода m.getVar() - вызов виртуального метода. Во второй раз, когда вы его вызываете, он динамически отправляется на производный Daughter.getVar(), который делает то, что вы ожидаете (обращается к Daugther.var и возвращает это).

Нет такого виртуального диспетчерского механизма для полей-членов. Таким образом, m.var всегда ссылается на Mother.var, то есть на версию базового класса этой переменной.

Класс Daughter может рассматриваться как имеющий два разных члена var: один из Mother и свой. Его собственный член "скрывает" один в Mother, но может быть доступен из класса Daughter с помощью super.var.

Официальная спецификация для этого находится в разделе 8.3 Объявления полей JLS. Цитата:

Если класс объявляет поле с определенным именем, считается, что объявление этого поля скрывает любые и все доступные объявления полей с таким же именем в суперклассах и суперинтерфейсы класса. Объявление поля также затеняет (§6.3.1) объявления любых доступных полей в окружающих классах или интерфейсах и любые локальные переменные, параметры формального метода и параметры обработчика исключений с тем же именем в любых закрывающих блоках.

Обратите внимание, что это может стать довольно интересным (выделено мной):

Если объявление поля скрывает объявление другого поля, два поля не должны иметь один и тот же тип.

и

Может существовать несколько путей, по которым одно и то же объявление поля может быть унаследовано от интерфейса. В такой ситуации поле считается наследуемым только один раз, и его можно назвать простым именем без двусмысленности.

Итак, этот параграф стоит прочитать: -)

Ответ 2

Сосредоточьтесь на этих строках:

Mother m;
 m = new Daughter();
 System.out.println(m.var);
 System.out.println(m.getVar());

Вы строите объект Дочь, но вы относитесь к нему так же, как к материнской материнской категории. Поэтому, когда вы обращаетесь к m.var, вы получаете доступ к переменной базового класса var. Между тем, когда вы вызываете метод, даже если вы ссылаетесь на ссылку базового класса, вызывается метод переопределения. Это другое поведение для методов и полей. Ссылка на поля не может быть переопределена.

Ответ 3

Методы могут быть переопределены, однако поля могут быть скрыты. Разница заключается в том, что нестатический метод использует тип объекта, на который делается ссылка, поле принимает тип ссылки. Вы видите аналогичную вещь со статическими методами, которые должны быть скрыты только там, где игнорируется класс "ссылки" и объекта (если предоставлено).

Для вашего интереса попробуйте дать полям разные типы.;)

Вы также можете попробовать

System.out.println(((Mother)m).var); // uses var in Mother
System.out.println(((Daughter)m).var); // uses var in Daughter

Ответ 4

m = new Daughter();

Хотя вы создали объект daughter, вы ссылаетесь на этот объект с помощью ссылки Mother m. Таким образом, любой вызов с использованием m будет называть членов класса Mother, а не дочерних

Ответ 5

Я запустил это в Eclipse и проверил значения с помощью отладчика, отладчик фактически показывает локальный m -variable, имеющий два разных var-члена после строки m = new Daugher() со значениями 2 и 1. m.var кажется разрешить это в Мать, а m.getVar() вызывает getVar в Дочке (как и ожидалось).

Однако, когда я изменяю основной метод, чтобы выглядеть так:

    Mother m = new Mother();
    System.out.println(m.var);
    System.out.println(m.getVar());
    Daughter d = new Daughter();
    System.out.println(d.var);
    System.out.println(d.getVar());

Он фактически выводит 2, 2, 1, 1, поэтому кажется, что объявление переменной влияет на класс var.

Ответ 6

Я прочитал ответы, а не их (пока) дал хорошую причину, почему на объектно-ориентированном языке, как Java, это должно быть так. Я попытаюсь объяснить.

Предположим, что у вас есть функция, которая принимает Мать как arg:

void foo(Mother m) {
  print(m.var);
}

Эта функция (на самом деле компилятор) понятия не имеет, если вы вызовете ее с помощью Mother, Daughter или с другим Dauther2, который даже не имеет объявленной переменной var, Из-за этого, когда ссылка имеет тип Мать, ссылка на переменную-член должна быть связана (компилятором) с членом Mother. Аналогичное относится и к функции, поэтому функции связаны с объявлением <