Статьи

Почему переменная экземпляра суперкласса не переопределяется в подклассе

Когда мы создаем переменную в родительском и дочернем классах с одним и тем же именем и пытаемся получить к ней доступ, используя ссылку на родительский класс, который содержит объект дочернего класса, тогда что мы получим?

Чтобы понять это, рассмотрим пример ниже, где мы объявляем переменную x с одинаковым именем в Parent и Child классах.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
class Parent {
    // Declaring instance variable by name `x`
    String x = "Parent`s Instance Variable";
 
    public void print() {
        System.out.println(x);
    }
}
 
class Child extends Parent {
 
    // Hiding Parent class's variable `x` by defining a variable in child class with same name.
    String x = "Child`s Instance Variable";
 
    @Override
    public void print() {
        System.out.print(x);
 
        // If we still want to access variable from super class, we do that by using `super.x`
        System.out.print(", " + super.x + "\n");
    }
}

А теперь, если мы попытаемся получить доступ к x с помощью приведенного ниже кода, то что System.out.println(parent.x) будет печатать

1
2
Parent parent = new Child();
System.out.println(parent.x) // Output -- Parent`s Instance Variable

В общем, мы скажем, что класс Child переопределит переменную, объявленную в классе Parent а parent.x даст нам то, Child's содержит объект Child's . Потому что это то же самое, что происходит, когда мы выполняем те же операции над методами.

Но на самом деле это не так, и parent.x даст нам значение Parent`s Instance Variable, которое объявлено в классе Parent но почему?

Поскольку переменные в Java не следуют полиморфизму, а переопределение применимо только к методам, но не к переменным. И когда переменная экземпляра в дочернем классе имеет то же имя, что и переменная экземпляра в родительском классе, тогда переменная экземпляра выбирается из ссылочного типа.

В Java, когда мы определяем переменную в дочернем классе с именем, которое мы уже использовали для определения переменной в родительском классе, переменная дочернего класса скрывает родительскую переменную, даже если их типы различны. И эта концепция известна как скрытие переменных.

Другими словами, когда дочерний и родительский класс имеют переменную с одинаковым именем, переменная дочернего класса скрывает переменную родительского класса. Вы можете прочитать больше о скрытии переменных в статье Что такое скрытие и скрытие переменных в Java .

Скрытие переменных — это не то же самое, что переопределение метода

Хотя скрытие переменных выглядит как переопределение переменной, аналогично переопределению метода, но это не так, переопределение применимо только к методам, в то время как скрытие применимо к переменным.

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

Но в скрытии переменных дочерний класс скрывает унаследованные переменные вместо замены, что в основном означает, что объект класса Child содержит обе переменные, а переменная Child скрывает переменную Parent. поэтому, когда мы пытаемся получить доступ к переменной из дочернего класса, она будет доступна из дочернего класса.

И если я упросту раздел Пример 8.3.1.1-3. Скрытие переменных экземпляра спецификации языка Java :

Когда мы объявляем переменную в Child классе с таким же именем, например, x как переменная экземпляра в Parent классе, тогда

  1. Объект дочернего класса содержит обе переменные (одна унаследована от Parent класса, а другая объявлена ​​в самом дочернем классе), но переменная дочернего класса скрывает переменную родительского класса.
  2. Поскольку объявление x в классе Child скрывает определение x в классе Parent , в объявлении класса Child простое имя x всегда ссылается на поле, объявленное в классе Child . И если код в методах класса Child хочет ссылаться на переменную x класса Parent то это можно сделать как super.x .
  3. Если мы пытаемся получить доступ к переменной вне класса Parent и Child , тогда переменная экземпляра выбирается из ссылочного типа. Таким образом, выражение parent2.x в следующем коде дает значение переменной, которая принадлежит родительскому классу, даже если она содержит объект Child но ((Child) parent2).x обращается к значению из класса Child потому что мы произвели то же самое ссылка на Child .

Переменная экземпляра

Почему переменная скрывается так

Итак, мы знаем, что переменные экземпляра выбираются из ссылочного типа, а не типа экземпляра, и полиморфизм не применим к переменным, но реальный вопрос — почему? почему переменные предназначены для сокрытия, а не переопределения.

Потому что переопределение переменной может нарушить методы, унаследованные от родительского, если мы изменим ее тип в дочернем классе.

Мы знаем, что каждый дочерний класс наследует переменные и методы (состояние и поведение) от своего родительского класса. Представьте себе, если Java допускает переопределение переменных, и мы меняем тип переменной с int на Object в дочернем классе. Это сломает любой метод, использующий эту переменную, и поскольку дочерний объект унаследовал эти методы от родительского, компилятор выдаст ошибки в child классе.

Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class Parent {
    int x;
    public int increment() {
        return ++x;
    }
    public int getX() {
        return x;
    }
}
 
class Child extends Parent {
    Object x;
    // Child is inherting increment(), getX() from Parent and both methods returns an int
    // But in child class type of x is Object, so increment(), getX() will fail to compile.
}

Если Child.x переопределяет Parent.x , как могут работать increment() и getX() ? В подклассе эти методы будут пытаться вернуть значение поля неправильного типа!

И, как уже упоминалось, если Java допускает переопределение переменных, тогда переменная Child не может заменить переменную Parent, и это нарушит принцип заменяемости Лискова (LSP).

Почему переменная экземпляра выбирается из ссылочного типа вместо экземпляра

Как объясняется в разделе Как внутренняя перегрузка и переопределение JVM обрабатывает методы, во время компиляции вызовы переопределенных методов обрабатываются только из ссылочного класса, но все переопределенные методы заменяются переопределяющим методом во время выполнения с использованием vtable, и это явление называется полиморфизмом времени выполнения.

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

Вообще говоря, никто никогда не будет рекомендовать скрывать поля, так как это затрудняет чтение кода и создает путаницу. Такой путаницы не будет, если мы всегда будем придерживаться
Общие рекомендации по созданию POJO и инкапсуляции наших полей, объявляя их частными и предоставляя средства получения / установки по мере необходимости, так что переменные не видны вне этого класса, и дочерний класс не может получить к ним доступ.

Вы можете найти полный код в этом репозитории Github и, пожалуйста, не стесняйтесь, оставляйте ценные отзывы.

Опубликовано на Java Code Geeks с разрешения Нареша Джоши, партнера нашей программы JCG . См. Оригинальную статью здесь: Почему переменная экземпляра суперкласса не переопределяется в подклассе

Мнения, высказанные участниками Java Code Geeks, являются их собственными.