2 июл. 2015 г.

Наследование. Часть 1 – введение.

Наследование — одно из фундаментальных понятий объектно-ориентированного программирования, поскольку оно позволяет создавать иерархические классификации. Используя наследование, можно создать общий класс, который определяет характеристики, общие для набора связанных элементов. Затем этот класс может наследоваться другими, более специализированными классами, каждый из которых будет добавлять свои уникальные характеристики. В терминологии Java наследуемый класс называют суперклассом. Наследующий класс носит название подкласса. Следовательно, подкласс — это специализированная версия суперкласса. Он наследует все переменные экземпляра и методы, определенные суперклассом, и добавляет собственные, уникальные элементы.

Чтобы еще раз лучше отложилось в голове:

  • Суперкласс – это родительский класс
  • Подкласс – это класс наследник родительского класса

Объявление класса-наследника

Чтобы наследовать класс, достаточно просто вставить определение одного класса в другой с использованием ключевого слова extends.

E0001

В данном примере объявляется что класс Derived является наследником класса Example.

Теперь рассмотрим простой пример. Создадим родительский класс Robot и унаследуем от него класс робота-уборщика – RobotCleaner.

E0002

E0003

Как видите в классе наследнике нет ни каких методов и полей.

E0004

 

В классе с методом main() мы создаем по экземпляру классов Robot и RobotCleaner, устанавливаем имя для объекта rc и затем выводим информацию о наших роботах. И хотя в классе наследнике нет ни каких методов и полей, мы все же можем обращаться к ним, поскольку они унаследованы.

Вывод у программы следующий:

E0005

Доступ к членам и наследование

В нашем примере поле name объявлено с модификатором private, а методы с модификатором protected. Именно по этому мы могли использовать методы, вот если попробуем получить доступ к унаследованным полям на прямую, в обход методов, то компилятор выдаст нам ошибку:

E0006

Хотя подкласс включает в себя все члены своего суперкласса, он не может получать доступ к тем членам суперкласса, которые объявлены как private.

Чтобы исправить эту ситуацию можно объявить поле name в родительском классе Robot как protected и тогда мы сможем к нему обращаться из классов наследников.

Давайте сделаем это…

E0007

После этих изменений ошибка исчезнет и наш класс RobotCleaner откомпилируется.

Надеюсь вы заметили что в класс RobotCleaner мы добавили метод printName(), то есть мы расширили (extends) функционал в классе наследнике.

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

Иерархия классов

У каждого определяемого вами класса есть родительский класс. Если вы не указали родительский класс в операторе extends, то его родительским классом будет класс java.lang.Object. Класс Object уникален по двум причинам:

  • Это единственный класс в Java, у которого нет родителя.
  • Все классы Java наследуют методы класса Object.

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

E0008

Тут стоит отметить, что в Java не поддерживается множественное наследование. То есть у класса потомка может быть только один родительский класс.

Конструкторы подклассов

Конструкторы не наследуются, но подкласс может вызывать конструктор, определенный его суперклассом, с помощью следующей формы ключевого слова super:

super(список_аргументов);

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

E0009Чтобы все стало понятнее, попрактикуемся. Я добавил в класс Robot конструктор по умолчанию (на примере слева). Теперь вывод у программы следующий:

E0010

Мы видим что конструктор по умолчанию был вызван два раза. Одни раз при создании объекта rb, второй – при создании объекта rc.

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

Java гарантирует, что конструктор класса будет вызываться при каждом создании экземпляра класса. Конструктор супер класса будет вызываться всякий раз при создании экземпляра подкласса. Чтобы гарантировать второе утверждение, Java обеспечивает порядок, согласно которому каждый конструктор будет вызывать родительский конструктор. Поэтому, если первый оператор в конструкторе не вызывает другой конструктор с помощью this() или super(), то Java неявно вставляет вызов super(), то есть вызывает родительский конструктор без аргументов. Если у родительского класса нет конструктора без аргументов, но определены другие конструкторы, то подобный неявный вызов приводит к ошибке компиляции.

E0011

E0012

E0013

Как видно из трех отрывков кода наших классов, если мы уберем конструктор по умолчанию в классе Robot и добавим другой, с каким-либо параметром, а в классах RobotCleaner и RobotShow появятся ошибки компиляции. Обратите внимание на то, что в классе RobotCleaner нет вообще ни каких конструкторов. Java подставила туда вызов конструктора по умолчанию суперкласса, но поскольку он не определен в суперклассе Robot, то получилась ошибка компиляции.

Все это означает, что вызовы конструкторов объединяются в цепочку; при каждом создании объекта вызывается последовательность конструкторов: конструктор подкласса, конструктор родительского класса и далее вверх по иерархии классов до конструктора класса Object. Так как конструктор родительского класса всегда вызывается первым оператором конструктора подкласса, то операторы конструктора класса Object всегда выполняются первыми. Затем выполняются операторы конструктора подкласса и далее вниз по иерархии классов вплоть до конструктора класса, объект которого создается. Здесь есть важное следствие: когда вызван конструктор, он может положиться на то, что поля родительского класса уже проинициализированы.

Если конструктор не вызывает конструктор родительского класса, то Java делает это неявно. А если класс объявлен без конструктора? как в нашем случае в классе RobotCleaner? В этом случае Java неявно добавляет конструктор по умолчанию. Конструктор по умолчанию не делает ничего, кроме вызова родительского конструктора по умолчанию. В нашем примере это привело к ошибке компиляции, так как у родительского класса Robot не был определен конструктор по умолчанию.

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

E0014Теперь приведем наших роботов в более-менее рабочий вид. Класс Robot я оставил как есть, то есть без конструктора по умолчанию, но с конструктором принимающим строку. А вот класс RobotCleaner я изменил добавив конструктор по умолчанию, который вызывает конструктор этого же класса с параметром принимающим строку и вызывающим конструктор суперкласса, который так же принимает строку. Кажется немного замысловато, но вообще все достаточно просто. Так же пришлось изменить строку создающую объект rb в классе RobotShow. Теперь она имеет вид:

Robot rb = new Robot("NoNaMe");

Так пришлось сделать, поскольку в классе Robot у нас нет конструктора по умолчанию.

E0015

Теперь у нас все работает. Пример вывода программы представлен слева.  Рабочий пример можно посмотреть в коммите Примеры наследования. Вызовы super и this.

 

Затенение полей родительского класса

В нашем классе RobotCleaner мы можем определить свое поле с именем name. В таком случае говорят что поле подкласса затеняет (shadows) или скрывает поле родительского класса. Как же мы тогда можем сослаться на поле name родительского класса Robot? Для этого существует специальный синтаксис, использующий ключевое слово super:

super.член_класса

Где член_класса может быть методом либо переменной экземпляра.

E0016Чтобы лучше понять, попрактикуемся. Я изменил класс RobotCleaner как на примере слева. Другие классы я не менял. Теперь вывод у программы следующий:

E0017

Унаследованный метод setName() установил значение унаследованного от супер класса Robot поля name в объекте rc , а поле name класса RobotCleaner осталось не тронутым.

Теперь изменим классы RobotShow, Robot и RobotCleaner, так чтобы конструктор класса RobotCleaner устанавливал значение поля name для класса RobotCleaner и оставим другой метод этого класса без изменений. В классе Robot расскоментируем конструктор по умолчанию. А в классе RobotShow создадим объект rc при помощи конструктора по умолчанию.

Таким образом мы сможем изменить значение поля name в классе RobotCleaner.

E0019

E0018Вывод у программы сейчас следующий:

E0021

E0020Как видно из вывода поле name класса RobotCleaner получило значение "Cleaner".

Первую из последних двух строчек выводит первая строка в методе printName() класса RobotCleaner, воторя – соответственно выводит вторую.

Другой способ сослаться на затененное поле – привести this (или любой экземпляр
класса) к соответствующему родительскому классу и обратиться к полю. Вспомните как мы приводили примитивные типы малой разрядности к примитивным типам бОльшей разрядности.

System.out.println(((Robot)this).name);

Вывод у программы не изменится. Мы просто поменяли метод обращения к полю name суперкласса.

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

Для примера возьмем три класса: А, В и С. Класс В является потомком класса А, а класс С потоком класса В. В каждом классе есть поле x. А так же есть методы выводящие значение поля x для каждого класса. И еще в классе А есть метод printX(), который выводит значение поля х для класса А.

E0022

E00023

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

E0024

Класс В использует свой метод printB() для вывода своего поля х, которое затеняет поле х, класса А.

Так же в этом методе выводится значение поля х из класса А.

 

 

E0025

Как видно из кода класса С, мы можем обратиться к полю х класса А, который не является прямым родителем класса С через приведение типов (строка 10).

Вы не можете ссылаться на затененное поле x в родителе родителя с помощью вызова super.super.x. Это неправильный синтаксис.

Благодаря приведению классов можно ссылаться на поля вышестоящих родителей, если они открыты для доступа. Пример этого приведен в строках 25-27 класса АВС.

До настоящего времени мы обсуждали поля экземпляров. Поля класса (static) также могут быть затенены. Но в этом нет особого смысла.

Вывод у данной программы следующий:

E0026

Первые две строки выводятся командами в строках 12 и 13 класса АВС.

Вторые три строки выводятся командами в строках 15 и 16.

Третьи четыре строки выводятся командами в строках 18 и 19.

Затем вывод делают строки 21 – 23, ну это мы уже проходили. Это простой доступ к полям экземпляров.

Ну и на последок, в строках 25 – 27 мы видим доступ к полям родителей через приведение классов. Синтаксис может показаться немного запутанным, но на самом деле он логичный и простой.

На что следует обратить особенное внимание в этом примере так это на метод printX() в классе А, и на его вызовы на экземплярах классов В и С. Не смотря на затенение поля х в этих классах, метод printX() все время выводит поле х класса А. Это происходит потому, что методы родительского класса А могут работать только с полями своего же класса, так как ни чего не знают о полях в классах потомках.

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

Чтобы еще чуть лучше усвоить как работает сокрытие полей, в класс АВС можно добавить еще три строчки:

b.printA();
c.printA();
c.printB();

Которые будут выводить следующее:

Класс А
Класс А
Класс B
Из В Класс А

То есть метод каждого класса выводит только сове собственное поле x.

Информацию о затенении полей я привел для полноты картины и понимания, но вообще использование их – это не очень хорошая практика.

Переменная суперкласса может ссылаться на объект подкласса

Ссылочной переменной суперкласса может быть присвоена ссылка на любой объект подкласса, производного от данного суперкласса. Этот аспект наследования будет весьма полезен во множестве ситуаций. Особенно когда познакомимся с полиморфизмом. И сразу лучше покажем на примере. Добавим в наш класс АВС еще несколько строк:

A ab;
ab = new B();
ab.printA();
//ab.printB(); // ОШИБКА!
B bc = new C();
bc.printA();
bc.printB();
// bc.printc(); // ОШИБКА!

Данный код сгенерирует следующий вывод:

Класс А
Класс А
Класс B
Из В Класс А

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

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

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

2 комментария:

  1. Очередное огромное спасибо!

    ОтветитьУдалить
  2. Вы пишете "Подобно полям, могут "затенятся" и методы, но это уже называется перегрузкой (override) методов, что является основой полиморфизма о котором мы скоро поговорим."
    Это не перегрузка, а переопределение (override). А перегрузка (overloading) - это когда методы с одним и тем же именем, но с разными типами и/или количеством параметров.

    ОтветитьУдалить