#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
. В объяснении, похоже, говорится, что решение принимается во время выполнения, что вводит в заблуждение … если на самом деле это не так.