Подтвердить что ты не робот

Как работает ключевое слово "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()?

4b9b3361

Ответ 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.