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

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

Смотрите мой код ниже, в котором метод print переопределен, а переменная a - нет. Почему разрешено объявлять дубликаты переменных в подклассе?

class B {
    int a = 10;
    public void print() {
        System.out.println("inside B superclass");
    }
}

class C extends B {
    int a = 20;
    public void print() {
        System.out.println("inside C subclass");
    }
}

public class A {
    public static void main(String[] args) {
        B b = new C();
        b.print(); // prints: inside C subclass
        System.out.println(b.a); // prints superclass variable value 10
    }
}
4b9b3361

Ответ 1

Почему переменная экземпляра суперкласса не переопределяется в методе подкласса, смотрите мой код ниже...

Потому что переменные экземпляра не могут быть переопределены в Java. В Java только методы могут быть переопределены.

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


Потому что переменные экземпляра не могут быть переопределены в Java, но почему? почему это делается в Java? В чем причина?

Почему они спроектировали это так?

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

    Например:

       public class Sup {
           private int foo;
           public int getFoo() {
               return foo;
           }
       }
    
       public class Sub extends Sup {
           private int[] foo;
           ...
       }
    

    Если Sub.foo переопределяет (т.е. заменяет) Sup.foo, как может работать getFoo()? В контексте подкласса он будет пытаться вернуть значение поля неправильного типа!

  2. Если бы поля, которые были переопределены, не были частными, это было бы еще хуже. Это нарушило бы принцип заменяемости Лискова (LSP) довольно фундаментальным образом. Это устраняет основу для полиморфизма.

  3. С другой стороны, переопределение полей не приведет к достижению чего-либо, что не может быть улучшено другими способами. Например, хороший дизайн объявляет все переменные экземпляра как частные и предоставляет для них методы получения/установки по мере необходимости. Методы получения/установки могут быть переопределены, и родительский класс может "защитить" себя от нежелательных переопределений, напрямую используя закрытые поля или объявив метод получения/установки final.


Рекомендации:

Ответ 2

Вы можете ссылаться на следующий раздел/примеры в Спецификация языка Java, которая объясняет эту тему.

Остальная часть моего сообщения - дополнительная информация для тех, кто заинтересован в поцарапании поверхности внутренних элементов jvm по этому вопросу. Мы можем начать с изучения байтовых кодов, сгенерированных для класса A с использованием javap. После разборки байтовых кодов в пользовательские инструкции на основе текста (мнемоники).

javap -c A.class 

Не теряясь во многих деталях всей демонстрационной сборки, мы можем сосредоточиться на строках, соответствующих b.print и b.a

9: invokevirtual #4                  // Method B.print:()V
...
...
16: getfield      #6                  // Field B.a:I

Мы можем сразу заключить, что операционные коды, используемые для доступа к методу и переменной, различны. Если вы из школы С++, вы можете почувствовать, что все вызовы методов виртуальны по умолчанию в java.

Теперь напишем другой класс A1, идентичный A, но просто имеет листинг для доступа к переменной 'a' в C.

открытый класс A1 {
   public static void main (String [] args) {
     B b = новый C();      b.print();//кастинг здесь не имеет значения, потому что методы в любом случае связаны во время выполнения      System.out.println(((C) b).a);//литье позволяет нам получить доступ к значению a в C
     }
}

Скомпилируйте файл и разберите класс.

javap -c A1.class

Вы заметили бы, что dis-assembly теперь указывает на C.a вместо B.a

19: getfield # 6//Поле C.a: I

если вы хотите углубиться в это, здесь идет дополнительная информация:
- invokevirtual соответствует опкоду 0xb6
- getfield соответствует операционному коду 0xb4

Вы можете найти спецификацию JVM, подробно объясняющую об этих кодах операций: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
Зайдите в amazon.com для книг "Виртуальная машина Java", которые могут облегчить жизнь для декодирования спецификации.

Ответ 3

Я изменил ваш код для простого пояснения, вместо переменной 'a', допустим, что класс C содержит переменную 'c'. Это по той же причине, почему класс C не может получить доступ к переменной экземпляра класса c без Typecasting. Пример ниже

class B
{
     int a=10;
     public void print()
     {
         System.out.println("inside B super class");
     }

}
 class C extends B
 {
     int x=20;
     public void print()
     {
         System.out.println("inside C sub class");
     }


 }
public class A  {
    public static void main(String[] args) {
        B b=new C();

        System.out.println(b.x);//will throw compile error unless b is type casted to Class C

    }

}

Итак, в java компилятор идет по ссылке, а не по экземпляру. Для преодоления этого компилятора использует Политизм времени выполнения, но он предназначен для методов, а не для переменных экземпляра. Таким образом, переменные не могут быть доступны без литья типов а также методы, кроме, переопределенных (полипропионизм времени выполнения), не могут быть доступны без литья типов.

Итак, в нашем случае. Это очевидно для refrence суперкласса, несущего экземпляр подкласса, для просмотра в суперклассе.

Ответ 4

Поскольку переменные в Java не следуют полиморфизму, а переопределение применимо только к методам, но не к переменным.

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

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

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

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

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

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

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

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

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

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

Вы можете прочитать больше в моих статьях Что такое скрытие и скрытие переменных в Java, почему переменная экземпляра суперкласса не переопределяется в подклассе

Ответ 5

Поскольку переменные экземпляра не переопределяются в java, не существует связанного с ними полиморфизма времени выполнения и, следовательно, во время компиляции он решается только по ссылке.

В вашем коде

B b = new C();
b.print();

As b is of type Class B which is Parent to C and hence as there is no 
run time polymorphism it is decided at compile time to call instance 
variable of Class B.

Ответ 6

Вот моя перспектива на концептуальном уровне: почему переменные экземпляра не переопределяются. Чтобы это было просто, если мы рассматриваем абстрактные классы, они определяют абстрактные методы и ожидают, что они будут переопределены. Никогда не было ничего подобного абстрактным переменным. Если бы это было так, мы могли бы ожидать, что язык поддержит его, переопределив. Итак, когда разрабатывается абстрактный класс, дизайнер определяет некоторое общее конкретное состояние и общее поведение (включая абстрактные методы) для подтипов. Почти всегда, если состояние наследуется (защищенный доступ), тогда он будет просто унаследован, и я верю, что в очень немногих случаях некоторые из них могут быть переопределены, но очень редко повторно объявлены. Таким образом, состояние, естественно, ожидается, что оно просто наследуется, тогда как поведение ожидается унаследованным и переопределенным.

Ответ 7

Как уже упоминали другие, вы не можете переопределить переменные экземпляра суперкласса, но вы можете использовать конструкторы для назначения правильных значений для ваших объектов. Например, вы можете использовать конструкторы, чтобы сделать значение 'a' равным '20' в классе C.

Это ваш исходный код, который дополнен с помощью конструкторов, чтобы установить значение для 'a', равное '20' в классе C.

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

 public class B {
        private int a; //initialize int a
        public int getA() { //create a getter for a
            return a;
        }
    public B(int size) { //constructor that takes an int
            a = size; //sets a to the value in the parameters
        }
        public void print() {
            System.out.println("inside B superclass");
        }
    }

public class C extends B{
    public C(int a) { //C constructor takes an int
        super(a); //it send the name up to its superclass (B)
    }
    public void print() {
        System.out.println("inside C subclass");
    }
}

public class A {
    public static void main(String[] args) {
        B b = new C(20); //Creates a new object 'b' of type C
        b.print(); // prints: inside C subclass
        System.out.println(b.getA()); // prints the value '20'
    }
}