Java 8 наследование

Цикл статей «Учебник Java 8».

Следующая статья — «Java 8 перечисления».
Предыдущая статья — «Java 8 интерфейсы».

Содержание

Введение

Приведение типов

Переопределение (overriding) и скрытие (hiding) методов

Использование ключевого слова super

Класс java.lang.Object

Метод clone()

Метод equals()

Метод finalize()

Метод getClass()

Метод hashCode()

Метод toString()

Методы notify(), notifyAll(), wait(), wait(long timeout), wait(long timeout, int nanos)

Ключевое слово final и неизменяемые классы

Абстрактные методы и классы

Введение

В предыдущих статьях я уже несколько раз упоминал наследование. Настало время написать подробную статью про эту вещь.

В Java класс может наследоваться от другого класса, получая его методы и поля, который в свою очередь может наследоваться от ещё одного класса и т. д. В Java нет множественного наследования классов. Один класс может наследоваться напрямую только от одного другого класса.

Класс, который наследуется от другого класса, называется подклассом (subclass), дочерним классом (child class), потомком или расширенным классом (extended class).

Класс, от которого наследуется дочерний класс, называется родительским классом (parent class), предком, суперклассом (superclass) или базовым классом (base class).

В самой вершине иерархии наследования находится класс Object , от которого наследуются все классы, для которых не указан явно суперкласс. Таким образом все классы (кроме самого Object ) напрямую или через какое-либо количество уровней наследования наследуются от класса Object.

Идея наследования классов состоит в том, что когда вы хотите создать новый класс, например Goblin , и уже существует какой-нибудь класс, который уже реализует часть функциональности, необходимой нашему классу, например Monster , то вы можете указать этот класс в качестве родительского класса, унаследовав таким образом все его члены (поля, вложенные классы и методы экземпляров). Конструкторы не наследуются и не являются членами классов, но можно вызвать конструктор базового класса из конструктора дочернего класса.

java inheritance object

Дочерний класс наследует все public  и protected  члены своего родителя независимо от пакета, в котором расположен родительский класс. Если дочерний и родительский класс находятся в одном пакете, то дочерний класс наследует также package-private члены своего родителя.

  • Унаследованные поля можно использовать напрямую, как все другие поля.
  • Можно объявить в дочернем классе поле с таким же именем, как и поле в родительском классе, тогда это поле скроет (hide) поле родительского класса (НЕ рекомендуется так делать).
  • В дочернем классе можно объявлять поля, которых нет в родительском классе.
  • Унаследованные методы можно использовать напрямую.
  • Можно объявить метод экземпляров в дочернем классе с точно такой же сигнатурой, что и метод экземпляров в родительском классе, тогда этот метод переопределит (override) метод суперкласса.
  • Можно объявить в дочернем классе статический метод с точно такой же сигнатурой, что и статический метод в родительском классе, тогда этот метод скроет (hide) метод родительского класса.
  • В дочернем классе можно объявлять новые методы, которых нет в родительском классе.
  • В дочернем классе можно объявить конструктор, который будет явно (с помощью ключевого слова super ) или неявно вызывать конструктор базового класса.

Дочерний класс не наследует private  члены родительского класса, однако если в родительском классе есть protected , public  или package-private (для случая нахождения дочернего и родительского класса в одном пакете)  методы для доступа к private  полям, то они могут использоваться дочерним классом.

Приведение типов

Посмотрите на создание экземпляра объекта Goblin :

Мы знаем, что Goblin  наследуется от Monster , который в свою очередь наследуется от Object . Таким образом, Goblin  является Monster  и является Object . Экземпляр класса Goblin  можно использовать в любом месте, где ожидается экземпляр класса Monster  или Object .

Но Monster  не обязательно должен являться Goblin . Экземпляр класса Monster  МОЖЕТ быть экземпляром класса Goblin , а может быть экземпляром самого Monster  либо любого другого дочернего класса.

Неявное приведение типов используется, когда мы приводим дочерний класс к классу выше по наследованию:

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

Компилятор вставит проверку на соответствие типа в эту операцию, которая будет проверять, что obj  действительно ссылается на экземпляр класса Goblin. Если obj  ссылается на объект НЕ являющийся экземпляром класса Goblin  или его потомков, то возникнет исключение java.lang.ClassCastException.

Переопределение (overriding) и скрытие (hiding) методов

Метод экземпляров в дочернем классе с той же сигнатурой, что и метод в родительском классе, и тем же типом результата переопределяет (override) этот метод. Эта возможность позволяет указать в качестве родительского класса наиболее подходящий класс, а затем изменить его поведение переопределив некоторые методы и добавив новые.

Можно указать аннотацию @Override  у метода дочернего класса, чтобы указать компилятору, что вы хотите переопределить метод базового класса. В этом случае компилятор будет генерировать ошибку, если не найдёт подобного метода в родительском классе. Более подробно можете прочесть про эту аннотацию в статье про аннотации.

Переопределяющий метод может также вернуть тип, являющийся потомком типа, возвращающегося переопределяемым методом. Этот дочерний тип называется ковариантным возвращаемым типом (covariant return type).

Если дочерний класс определяет статический метод с той же сигнатурой, что и метод в родительском классе, то этот метод скрывает (hide) метод родительского класса.

Разница между скрытием статических методов и переопределением методов экземпляров приводит к важным последствиям:

  • При вызове переопределённого метода экземпляра будет вызван метод дочернего класса.
  • Версия скрытого статического метода зависит от того, где будет производится вызов: из суперкласса или дочернего класса.

Пример (файл «Main.java»):

Этот код выведет в консоль:

Как видно из результата, при вызове метода экземпляра вызывается переопределивший метод дочернего класса. Обратите внимание, что переменная goblin  типа Monster , но фактически ссылается на экземпляр его дочернего класса Goblin , чей переопределивший метод и вызывается. Статический метод вызывается у того класса, у которого он вызван.

При вызове метода объекта Java вызывает подходящий метод экземпляра класса, на который ссылается переменная, но не метод типа, с которым переменная объявлена. Это называется полиморфизмом в Java.

Методы по умолчанию (default методы) и абстрактные методы интерфейсов наследуются так же, как и методы экземпляров.

Если несколько супертипов класса или интерфейса объявляют методы с одинаковыми сигнатурами, то компилятор Java использует следующие правила для разрешения конфликтов:

Правило 1: Методы экземпляров имеют преимущество перед методами по умолчанию (default методами). Это вполне логично, так как методы по умолчанию появились только в Java 8, и они не должны ломать старого кода, где уже могут быть объявлены методы с такими же сигнатурами.

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

Второе правило лучше всего рассматривать на примере:

Класс DangerousMonster  получает метод writeName  из Dangerous , который в свою очередь наследует этот метод от Obstacle , и из Monster , который переопределяет метод, унаследованный от Obstacle . Пример выше выведет “Monster” , так как writeName  из Obstacle , который имеет Dangerous , уже переопределён в Monster , который тоже наследует этот метод от Obstacle.

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

Правило 4: Унаследованные методы экземпляров могут переопределять абстрактные методы интерфейсов:

Статические методы никогда НЕ наследуются.

Чтобы подвести итог переопределению (overriding) и скрытию (hiding) приведу здесь такую таблицу:

  Метод экземпляров родителя Статический метод родителя
Метод экземпляров дочернего класса переопределяет (override) Ошибка компиляции
Статический метод дочернего класса Ошибка компиляции скрывает (hide)

Использование ключевого слова super

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

Если ваш класс определяет поле с тем же именем, что и поле в родительском классе,пусть даже с другим типом, то это поле скрывает (hide) поле родительского класса. В этом случае вы не можете напрямую обратиться к полю родительского класса по имени. Если всё же нужно обратиться к этому полю родительского класса, то нужно использовать ключевое слово super .

Пример:

Результат в консоли:

С помощью ключевого слова super  можно вызвать конструктор родительского класса в классе потомке:

Вызов конструктора суперкласса должен быть первой инструкцией в конструкторе дочернего класса. Можно вызвать конструктор суперкласса без параметров (конструктор по умолчанию):

Если вы не вставили ни одного явного вызова конструктора родительского класса, то компилятор Java автоматически добавит вызов конструктора родительского класса без параметров (конструктора по умолчанию). Если конструктор родительского класса без параметров недоступен из-за модификатора доступа, или конструктора без параметров нет в родительском классе, то возникнет ошибка компиляции.

При создании экземпляра любого объекта происходит цепочка вызовов конструкторов от конструктора создаваемого объекта до конструктора класса Object . Это называется цепочкой вызова конструкторов (constructor chaining).

Класс java.lang.Object

Класс Object  является прямо или через череду других классов суперклассом для всех других классов в языке Java.

Он имеет некоторое количество методов экземпляров, которых от него наследуют все его потомки.

Метод clone()

Метод clone()  создаёт копию объекта, если класс объекта реализует интерфейс java.lang.Cloneable, а в противном случае генерирует исключение CloneNotSupportedException. Сам класс Object  не реализует объект Cloneable, поэтому если вы хотите воспользоваться методом clone() , то вы должны реализовать интерфейс Cloneable  в своём классе. Сам интерфейс Cloneable  не содержит никаких методов, а просто является меткой того, что объект, который его реализует поддерживает метод clone().

Согласно соглашению объект, который возвращается методом clone()  должен быть того же типа, что и объект, у которого он вызван, и этот возвращённый объект должен быть копией исходного, а не исходным ( obj.clone() != obj ), но метод equals  должен возвращать true  ( obj.equals(obj.clone()) == true ).

Согласно соглашению классы, которые реализуют интерфейс Cloneable , и для объектов которых планируется использовать метод clone() , должны переопределить этот метод с protected  на public :

Реализация метода clone()  в Object  создаёт копию объекта просто копируя поля, что вполне нормально для полей примитивных типов. Для полей ссылочных типов и различных сложных структур (деревьев, массивов и прочего) вы должны сами создать копии при переопределении метода clone() .

Метод equals()

Этот метод проверяет равенство двух объектов. Реализация в классе Object  возвращает true  только тогда, когда две ссылки ссылаются на один и тот же экземпляр объекта. Если нужно более нормальное сравнение по содержимому полей, то нужно переопределить этот метод в своём классе. При переопределении метода equals()  нужно обязательно переопределить метод hashCode() , который должен возвращать одинаковое значение для любых объектов, для которых equals()  вернул true .

Метод finalize()

Метод finalize()  МОЖЕТ быть вызван, когда сборщик мусора решит удалить ваш объект. Реализация finalize()  в Object  не делает ничего. Вам не стоит полагаться на вызов этого метода для очищения ресурсов, так как он может никогда не вызваться. Я, честно говоря, смутно представляю, где вообще может понадобиться использование этого метода.

Метод getClass()

Возвращает объект java.lang.Class , который содержит информацию о классе, для которого вызван метод.

Настоящий результат этого метода Class<? extends |X|> (это будет описано в более поздних статьях), где |X| — выведенный тип для выражения, на котором вызван метод getClass . Например, для следующего кода не нужно приведения типа:

Вы не можете переопределить этот метод.

Метод hashCode()

Возвращает хеш-код для объекта. Реализация в Object  возвращает адрес объекта. Этот хеш-код используется в хеш-таблицах вроде HashMap . Согласно соглашению если метод equals()  для двух объектов возвращает true , то hashCode()  для для них должен возвращать одинаковое значение.

Метод toString()

Возвращает строковое представление объекта, понятное человеку. Старайтесь всегда переопределять этот метод в своих классах, так как он очень полезен во время отладки. Реализация в Object возвращает следующее значение:

Методы notify(), notifyAll(), wait(), wait(long timeout), wait(long timeout, int nanos)

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

Ключевое слово final и неизменяемые классы

Если при определении метода указать его как final , то этот метод будет нельзя переопределять в классах потомках. Это может быть полезно для методов, используемых конструкторами, так как переопределение метода, который используется конструктором, может привести к нежелательным последствиям. Старайтесь всегда делать final  методы, которые используются в конструкторе объекта.

Можно сделать весь класс final , что запретит указывать его в качестве родительского класса. Это может быть полезно для создания неизменяемых классов (вроде String ). Неизменяемые классы (immutable) могут безопасно использоваться при многопоточном программировании.

 

Абстрактные методы и классы

Если класс объявлен с ключевым словом abstract , то он называется абстрактным классом. Он может иметь, а может и не иметь абстрактных методов.

Абстрактным методом называется метод, объявленный с ключевым словом abstract  и не имеющий тела метода.

Если в классе есть абстрактные методы, то он ДОЛЖЕН быть объявлен абстрактным.

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

Дочерний класс от абстрактного класса должен либо дать реализацию всем его абстрактным методам, либо сам быть абстрактным классом.

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

Выбрать между абстрактным классом и интерфейсом бывает довольно сложно. Старайтесь руководствоваться правилами, описанными ниже.

Используйте абстрактные классы, если:

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

Используйте интерфейсы в следующих ситуациях:

  • Вы ожидаете, что интерфейс будут реализовывать не связанные друг с другом классы. Интерфейсы Comparable  и Cloneable , например, реализует очень большое количество совершенно разных классов.
  • Вы хотите указать поведение определённого типа, но вам абсолютно не важно, кто будет реализовывать это поведение.
  • Вам нужно множественное наследование типов.

Для примера абстрактного класса представьте ситуацию, что вам нужно реализовать несколько различных видов монстров: Goblin , Hobgoblin , Orc , Gremlin  и Genie. Каждый из эти монстров имеет свои различные особенности, которые будут реализовываться в соответствующем классе, но все эти монстры будут уметь ходить и иметь координаты в пространстве, и у каждого из них будет уровень здоровья. В этом случае можно заложить умение ходить, координаты и уровень здоровья в базовом классе Monster, который сделать абстрактным, и в котором объявить абстрактные методы для управления повадками и прочими вещами, реализации которых будут в соответствующих дочерних классах.

Цикл статей «Учебник Java 8».

Следующая статья — «Java 8 перечисления».
Предыдущая статья — «Java 8 интерфейсы».

Java 8 наследование: 3 комментария

  1. В конце темы переопределений пишете: «Статические методы никогда НЕ наследуются.»

    Но вот такой код, где метод func унаследован от класса А, выполняется корректно.

    class A{
    protected static int func () {
    return 1;
    };
    }
    class B extends A{
    public void print() {
    System.out.println(func());
    }
    }

    class Main {
    public static void main(String… args) {
    B b = new B();
    b.print();
    }
    }

    У Вас опечатка или я что-то не так понял?

    1. Тут вопрос больше терминологии. В вашем примере просто вызывается метод func класса A. Он доступен для вызова, так как объявлен с модификатором protected, а не private. Класс B не имеет этого метода. Статические методы всегда относятся к самому классу, а не к экземпляру, на них не действует наследование в том плане, в котором мы его понимаем. И для статических методов нет таблицы виртуальных методов. Например:

      Именно из-за подобного поведения и принято вызывать B.staticMethod(), A.staticMethod(), чтобы было сразу очевидно, какой метод и какого класса вызывается.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *