Как работает ключевое слово "this" в наследовании Java?
В приведенном ниже фрагменте кода результат действительно запутан.
public class TestInheritance {
public static void main(String[] args) {
new Son();
/*
Father father = new Son();
System.out.println(father); //[1]I know the result is "I'm Son" here
*/
}
}
class Father {
public String x = "Father";
@Override
public String toString() {
return "I'm Father";
}
public Father() {
System.out.println(this);//[2]It is called in Father constructor
System.out.println(this.x);
}
}
class Son extends Father {
public String x = "Son";
@Override
public String toString() {
return "I'm Son";
}
}
Результат
I'm Son
Father
Почему "this" указывает на Сына в конструкторе Отца, но "this.x" указывает на поле "x" у Отца. Как работает ключевое слово "this"?
Я знаю о полиморфной концепции, но разве не будет различий между [1] и [2]? Что происходит в памяти при запуске нового Son()?
Ответы
Ответ 1
Все функции-члены являются полиморфными в Java по умолчанию. Это означает, что при вызове this.toString() Java использует динамическое связывание для разрешения вызова, вызывая дочернюю версию. Когда вы обращаетесь к члену x, вы получаете доступ к члену вашей текущей области (отцу), потому что члены не являются полиморфными.
Ответ 2
Здесь происходит две вещи: взгляните на них:
Прежде всего, вы создаете два разных поля. Взглянув на (очень изолированные) куски байт-кода, вы увидите следующее:
class Father {
public java.lang.String x;
// Method descriptor #17 ()V
// Stack: 2, Locals: 1
public Father();
...
10 getstatic java.lang.System.out : java.io.PrintStream [23]
13 aload_0 [this]
14 invokevirtual java.io.PrintStream.println(java.lang.Object) : void [29]
17 getstatic java.lang.System.out : java.io.PrintStream [23]
20 aload_0 [this]
21 getfield Father.x : java.lang.String [21]
24 invokevirtual java.io.PrintStream.println(java.lang.String) : void [35]
27 return
}
class Son extends Father {
// Field descriptor #6 Ljava/lang/String;
public java.lang.String x;
}
Важны строки 13, 20 и 21; другие представляют сам System.out.println();
или неявный return;
. aload_0
загружает ссылку this
, getfield
извлекает значение поля из объекта, в данном случае, из this
. Здесь вы видите, что имя поля квалифицировано: Father.x
. В одной строке в Son
вы можете увидеть, что есть отдельное поле. Но Son.x
никогда не используется; только Father.x
есть.
Теперь, если мы удалим Son.x
и вместо этого добавим этот конструктор:
public Son() {
x = "Son";
}
Сначала рассмотрим байт-код:
class Son extends Father {
// Field descriptor #6 Ljava/lang/String;
public java.lang.String x;
// Method descriptor #8 ()V
// Stack: 2, Locals: 1
Son();
0 aload_0 [this]
1 invokespecial Father() [10]
4 aload_0 [this]
5 ldc <String "Son"> [12]
7 putfield Son.x : java.lang.String [13]
10 return
}
Строки 4, 5 и 7 выглядят хорошо: this
и "Son"
загружаются, а поле устанавливается с помощью putfield
. Почему Son.x
? потому что JVM может найти наследуемое поле. Но важно отметить, что даже если поле ссылается как Son.x
, поле, найденное JVM, фактически Father.x
.
Так он дает правильный результат? К сожалению, нет:
I'm Son
Father
Причиной является порядок высказываний. Строки 0 и 1 в байте -коде являются неявным вызовом super();
, поэтому порядок операторов выглядит следующим образом:
System.out.println(this);
System.out.println(this.x);
x = "Son";
Конечно, он напечатает "Father"
. Чтобы избавиться от этого, можно сделать несколько вещей.
Возможно, самое чистое: не печатать в конструкторе! Пока конструктор еще не закончил, объект не полностью инициализирован. Вы работаете над тем, что, поскольку println
- это последние операторы в вашем конструкторе, ваш объект завершен. Как вы уже испытали, это не так, когда у вас есть подклассы, потому что конструктор суперкласса всегда будет закончен, прежде чем ваш подкласс сможет инициализировать объект.
Некоторые считают это недостатком в концепции самих конструкторов; и некоторые языки даже не используют конструкторы в этом смысле. Вы можете использовать метод init()
вместо. В обычных методах у вас есть преимущество полиморфизма, поэтому вы можете вызвать init()
по ссылке Father
и вызывается Son.init()
; тогда как new Father()
всегда создает объект Father
. (конечно, в Java вам все равно нужно вызвать правый конструктор в какой-то момент).
Но я думаю, что вам нужно что-то вроде этого:
class Father {
public String x;
public Father() {
init();
System.out.println(this);//[2]It is called in Father constructor
System.out.println(this.x);
}
protected void init() {
x = "Father";
}
@Override
public String toString() {
return "I'm Father";
}
}
class Son extends Father {
@Override
protected void init() {
//you could do super.init(); here in cases where it possibly not redundant
x = "Son";
}
@Override
public String toString() {
return "I'm Son";
}
}
У меня нет имени для этого, но попробуйте. Он напечатает
I'm Son
Son
Итак, что здесь происходит? Ваш самый верхний конструктор (Father
) вызывает метод init()
, который переопределяется в подклассе. Поскольку во всех конструкторах вызов super();
во-первых, они эффективно выполняют суперкласс для подкласса. Поэтому, если первый вызов первого конструктора - init();
, тогда все init происходит до любого кода конструктора. Если ваш метод init полностью инициализирует объект, все конструкторы могут работать с инициализированным объектом. А поскольку init()
является полиморфным, он даже может инициализировать объект, когда есть подклассы, в отличие от конструктора.
Обратите внимание, что init()
защищен: подклассы смогут его вызывать и переопределять, но классы в другом пакете не смогут его вызвать. Это небольшое улучшение по сравнению с public
и должно учитываться и для x
.
Ответ 3
Как указано в других, вы не можете переопределять поля, их можно скрыть. См. JLS 8.3. Декларации полей
Если класс объявляет поле с определенным именем, то объявление этого поля, как говорят, скрывает любые и все доступные объявления полей с тем же именем в суперклассах и суперинтерфейсы класса.
В этом отношении скрытие полей отличается от скрытия методов (§8.4.8.3), поскольку не существует различия между статическими и нестатические поля при скрытии полей, тогда как между статическими и нестационарными методами при скрытии метода проводится различие.
Доступ к скрытому полю можно получить с помощью квалифицированного имени (§6.5.6.2), если он является статичным или с использованием доступа к полю выражение, содержащее ключевое слово super (§15.11.2) или приведение в класс суперкласса.
В этом отношении скрытие полей похоже на скрытие методов.
Класс наследует от своего прямого суперкласса и прямых суперинтерфейсов все нефайловые поля суперкласса и суперинтерфейсов, которые оба доступны для кода в классе и не скрыты объявлением в классе.
Вы можете получить доступ к скрытым полям Father
из области Son
с помощью ключевого слова super
, но противоположное невозможно, так как класс Father
не знает своих подклассов.
Ответ 4
В то время как методы могут быть переопределены, атрибуты могут быть скрыты.
В вашем случае атрибут x
скрыт: в вашем классе Son
вы не можете получить доступ к значению Father
x
, если вы не используете ключевое слово super
. Класс Father
не знает об атрибуте Son
x
.
В противоположности метод toString()
переопределяется: реализация, которая всегда будет вызвана, является экземпляром экземпляра класса (если только он не переопределяет его), т.е. в вашем случае Son
, независимо от типа переменной (Object
, Father
...).
Ответ 5
Это поведение сделано специально для доступа к закрытым членам. Итак, this.x смотрит на переменную X, которая объявляется для Отца, но когда вы передаете это значение как параметр System.out.println
в методе в папке "Отец" - он смотрит на метод вызова в зависимости от типа параметра - в вашем дело Сын.
Итак, как вы называете метод суперклассов? Использование super.toString()
и т.д.
От Отца он не может получить доступ к переменной x от Сына.
Ответ 6
Вызовы полиморфных методов применяются только к методам экземпляра. Вы всегда можете ссылаться на объект с более общим типом ссылочной переменной (суперкласс или интерфейс), но во время выполнения ТОЛЬКО вещи, которые динамически выбираются на основе фактического объекта (а не ссылочного типа), являются методами экземпляра НЕ СТАТИЧЕСКИЕ МЕТОДЫ. NOT VARIABLES. Только методы переопределенного экземпляра динамически вызывают на основе типа реальных объектов.
Таким образом, переменная x
не имеет полиморфного поведения, потому что ЭТО НЕ ВЫБРАТЬ ДИНАМИЧНО ПРИ RUNTIME.
Объяснение кода:
System.out.println(this);
Тип объекта Son
, поэтому toString()
будет вызываться метод Overridden Son
.
System.out.println(this.x);
Тип объекта здесь отсутствует, this.x
находится в классе Father
, поэтому будет напечатана версия x
variable Father
.
См. больше: Полиморфизм в java
Ответ 7
Это обычно называют затенением. Обратите внимание на объявления класса:
class Father {
public String x = "Father";
и
class Son extends Father {
public String x = "Son";
Это создает две различные переменные с именем x
при создании экземпляра Son
. Один x
принадлежит к суперклассу Father
, а второй x
принадлежит подклассу Son
. На основе вывода мы видим, что когда в области Father
this
обращается к переменной экземпляра Father
x
. Поэтому поведение не связано с "тем, что this
указывает на"; это результат того, как среда выполнения ищет переменные экземпляра. Для поиска переменных используется иерархия классов вверх. Класс может ссылаться только на переменные от себя и своих родительских классов; он не может напрямую обращаться к переменным из своих дочерних классов, потому что он ничего не знает о своих дочерних элементах.
Чтобы получить требуемое полиморфное поведение, вы должны объявить x
в Father
:
class Father {
public String x;
public Father() {
this.x = "Father"
}
и
class Son extends Father {
public Son() {
this.x = "Son"
}
В этой статье обсуждалось поведение, которое вы испытываете в точности: http://www.xyzws.com/Javafaq/what-is-variable-hiding-and-shadowing/15.