Что происходит, когда базовый и производный классы имеют переменные с одинаковым именем

#java

#java

Вопрос:

Рассмотрим int a переменные в этих классах:

 class Foo {
    public int a = 3;
    public void addFive() { a  = 5; System.out.print("f "); }
}
class Bar extends Foo {
    public int a = 8;
    public void addFive() { this.a  = 5; System.out.print("b " ); }
}
public class test {
    public static void main(String [] args){
        Foo f = new Bar();
        f.addFive();
        System.out.println(f.a);
    }
}
  

Я понимаю, что метод addFive() был переопределен в дочернем классе, и в тестировании класса, когда ссылка на базовый класс, ссылающаяся на дочерний класс, используется для вызова переопределенного метода, вызывается версия addFive дочернего класса.

Но как насчет общедоступной переменной экземпляра a ? Что происходит, когда и базовый, и производный классы имеют одну и ту же переменную?

Результатом вышеупомянутой программы является

 b 3 
  

Как это происходит?

Комментарии:

1. Я внес некоторые изменения в код и не могу понять, что происходит. Я добавил метод отображения в Bar и вызвал его в main, но получил ошибку компилятора. Добавлен также метод отображения в Foo, но, к удивлению, отображение bar выполнено. Является ли это недостатком Java?

2. @SrujanBarai Это полиморфизм. Время компиляции: раннее связывание: проверка выполняется для ссылочного типа. Время выполнения: поздняя привязка: вызов выполняется для типа фактического объекта.

Ответ №1:

На самом деле вызываются две разные переменные общедоступного экземпляра a .

  • Объект Foo имеет Foo.a переменную.
  • Объект Bar имеет обе переменные — Foo.a и Bar.a .

Когда вы запускаете это:

     Foo f = new Bar();
    f.addFive();
    System.out.println(f.a);
  

addFive метод обновляет Bar.a переменную, а затем считывает Foo.a переменную. Чтобы прочитать Bar.a переменную, вам нужно будет сделать это:

     System.out.println(((Bar) f).a);
  

Технический термин для того, что здесь происходит, — «скрытие». Обратитесь к разделу 8.3 JLS и разделу 8.3.3.2 для примера.

Обратите внимание, что скрытие также применяется к static методам с одинаковой сигнатурой.

Однако методы экземпляра с одинаковой сигнатурой «переопределены», а не «скрыты», и вы не можете получить доступ к версии метода, которая переопределена извне. (Внутри класса, который переопределяет метод, переопределенный метод может быть вызван с помощью super . Однако это единственная ситуация, когда это разрешено. Причина, по которой доступ к переопределенным методам обычно запрещен, заключается в том, что это нарушило бы абстракцию данных.)


Рекомендуемый способ избежать путаницы (случайного) скрытия — объявить переменные вашего экземпляра как private и получить к ним доступ с помощью методов getter и setter. Существует множество других веских причин для использования методов получения и установки.


Следует также отметить, что: 1) Предоставление общедоступных переменных (например a ), как правило, является плохой идеей, поскольку это приводит к слабой абстракции, нежелательной связи и другим проблемам. 2) Намеренное объявление второй общедоступной a переменной в дочернем классе — поистине ужасная идея.

Ответ №2:

Из JLS

8.3.3.2 Пример: Скрытие переменных экземпляра Этот пример похож на приведенный в предыдущем разделе, но использует переменные экземпляра, а не статические переменные. Код:

 class Point {
  int x = 2;
}
class Test extends Point {
  double x = 4.7;
  void printBoth() {
      System.out.println(x   " "   super.x);
  }
  public static void main(String[] args) {
      Test sample = new Test();
      sample.printBoth();
      System.out.println(sample.x   " "   
                                              ((Point)sample).x);
  }
}
  

выдает вывод:

 4.7 2
4.7 2
  

поскольку объявление x в class
Тест скрывает определение x в
класс указывает, поэтому тест класса не
наследовать поле x из его
точка суперкласса. Необходимо отметить,
однако, что, хотя поле x из
точка класса не наследуется классом
Тест, тем не менее, это реализовано
экземплярами класса Test. В других
другими словами, каждый экземпляр class Test
содержит два поля, одно из которых типа int
и один из типов double. Оба поля
имеют имя x, но в пределах
объявление класса Test, простое
имя x всегда ссылается на поле
объявлено в рамках class Test. Код в
методы экземпляра класса Test могут
обратитесь к переменной экземпляра x из
класс указывает как super.x.

Код, который использует выражение доступа к полю для доступа к полю x, получит доступ к полю с именем x в классе, указанном типом ссылочного выражения. Таким образом, выражение sample.x обращается к двойному значению, переменной экземпляра, объявленной в классе Test, потому что тип переменной sample — Test, но выражение ((Point)sample).x обращается к значению int, переменной экземпляра, объявленной в классе Point, из-за приведения к типу Point.

Ответ №3:

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

Итак, вот как Foo f = new Bar(); работает нормально.

Теперь, когда вызывается f.addFive(); инструкция, она фактически вызывает метод ‘addFive() экземпляра производного класса, используя ссылочную переменную базового класса. Таким образом, в конечном итоге вызывается метод класса ‘Bar’. Но, как вы видите, addFive() метод класса ‘Bar’ просто выводит ‘b’, а не значение ‘a’.

Следующий оператор, т.Е. System.out.println(f.a) , фактически выводит значение a, которое в конечном итоге добавляется к предыдущему выводу, и поэтому вы видите конечный результат как ‘b 3’. Здесь используемое значение соответствует значению класса ‘Foo’.

Надеюсь, выполнение и кодирование этого трюка понятны, и вы поняли, как вы получили результат как ‘b 3’.

Комментарии:

1. Разве у вас в Python нет такой же «проблемы»? Или это просто не компилируется?

2. @Angelo’Sphere — В Python объект может иметь только один атрибут с именем a . Родительский и дочерний классы видят / обновляют одну и ту же переменную. (Свободная абстракция!) Но тогда в Python есть другие сложности… например, f.a ссылается ли на обычный атрибут или он сопоставляется с чем-то другим.

Ответ №4:

Здесь F имеет тип Foo, а переменная f содержит объект Bar, но среда выполнения java получает f.a из класса Foo .Это потому, что в Java имена переменных разрешаются с использованием ссылочного типа, а не объекта, на который он ссылается.

Комментарии:

1. Это объяснение неверно. Фактически, это тип времени компиляции f , который определяет, на какую из a переменных f.a ссылается.

2. Разве тип времени компиляции и времени выполнения здесь не одинаковый, Foo ?

3. Нет. Тип времени компиляции f равен Foo , но тип среды выполнения фактического объекта, на который ссылается f является Bar . В объяснении, похоже, говорится, что решение принимается во время выполнения, что вводит в заблуждение … если на самом деле это не так.