Показаны сообщения с ярлыком protected. Показать все сообщения
Показаны сообщения с ярлыком protected. Показать все сообщения

26 авг. 2015 г.

Внутренние классы. Часть 3 – inner classes.

Вот тут у нас и пойдет речь о внутренних (inner) классах, то есть о нестатических внутренних классах. Эти классы объявляются внутри внешних (окружающих) классов без ключевого слова static и они существенно отличаются от вложенных статических классов.

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

Определение внутреннего класса может выглядеть так:

IN0001

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

IN0002IN0003

Как правило внешний класс содержит метод, возвращающий ссылку на внутренний класс. Чтобы стало понятней обратимся к примерам представленным выше. У нас есть внешний класс OuterClass и внутренний класс InnerClass. OuterClass содержит приватную переменную типа int outInt. Класс OuterClass не имеет метода возвращающего значение этой переменной, но внутренний класс содержит метод getOutInt(), который возвращает значение переменной outInt принадлежащей внешнему классу. Класс же OuterClass имеет метод getInnerClass(), который создает экземпляр класса InnerClass и возвращает ссылку на него. Создать экземпляр внутреннего класса можно и другим способом который мы рассмотрим чуть позже.

В главном классе программы Main мы сперва создаем экземпляр класса OuterClass – outClass и затем используя его метод getInnerClass() создаем экземпляр внутреннего класса InnerClass – innerClass, который связан с экземпляром своего внешнего класса и поэтому имеет доступ к его приватной переменной outInt, для получения которой мы используем метод getOutInt класса InnerClass, который вызывается на экземпляре этого класса innerClass. Этот пример достаточно простой, как мне кажется.

Из этого примера так же видно, что код внутреннего класса имеет доступ ко всем полям и методам экземпляра (так же как и к статическим членам) окружающего класса, включая все члены, даже объявленные как private. А вот обратный доступ, до есть доступ из окружающего класса к членам внутреннего, имеет свои ограничения. Экземпляр внешнего класса не имеет доступа ни к каким членам экземпляра внутреннего класса на прямую, то есть без создания экземпляра внутреннего класса внутри своих методов. И это логично, так как экземпляров внутреннего класса может быть создано сколько угодно много, и к какому же из них тогда обращаться?

Чтобы лучше понять этот момент рассмотрим новую мутацию предыдущего примера:

IN0004

IN0005

Теперь в главном классе мы создаем два экземпляра внутреннего класса InnerClass который связан с одним и тем же экземпляром outClass внешнего класса OuterClass.

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

IN0006

Как видно из вывода каждый экземпляр класса InnerClass хранит свое личное значение в поле innerInt, а вот при вызове метода getOutInt() выводит одно и то же значение, поскольку связаны с одним и тем же объектом.

Здесь еще раз стоит упомянуть об одном нюансе, который вы возможно не заметили. Объект внутреннего класса получает ссылку на внешний объект, который его создал, и поэтому может обращаться к членам внешнего объекта без дополнительных уточнений. Именно поэтому в методе getOutInt() внутреннего класса InnerClass мы можем возвратить значение поля outInt внешнего класса без каких либо уточнений. Это важный момент и его надо запомнить. Идем далее.

IO0001

IO0002

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

IO0003

Вывод программы простой. Обратите внимание на закомментированную строку в классе Outer. Такой номер не пройдет. Необходимо создать экземпляр внутреннего класса. И тогда уже по имени внутреннего класса можно будет обращаться на прямую к любым его членам, включая private члены. Это был достаточно простой пример. Теперь мы его немного усложним для лучшего понимания происходящего. Сразу приведу оба класса и вывод новой мутации. Суть в том, что экземпляров внутреннего класса может быть много, но экземпляр внешнего класса имеет прямой доступ к любым членам экземпляров внутренних классов, даже к private членам.

S0010S0009

IO0004

 

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

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

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

IN0007

Еще немного попрактикуемся на примере приведенном слева. Он генерирует простой вывод:

IN0008

Здесь мы еще раз видим, что внутренний класс может обращаться к любым полям и методам внешнего класса на примере класса SequnceSelector, в коде которого идет обращение к массиву Objects items, который является членом внешнего класса Sequence. Внутренний класс реализует интерфейс ISelector и вот тут есть немножко магии.

Как уже говорилось, внутренний класс можно создать только в связке с его внешним классом. В нашем примере внутренний класс создается строкой

ISelector selector = sequence.selector();

Метод selector() возвращает ссылку на экземпляр внутреннего класса SequnceSelector приведенную к типу интерфейса ISelector, что может вас немного запутать, но про такие трюки мы уже говорили когда изучали интерфейсы.

Ну и далее экземпляр внутреннего класса SequenceSelector – selector работает с данными экземпляра внешнего класса Sequence – sequence. То есть перебирает и выводит на консоль массив объектов items.IN0009

И чтобы плавно перейти к следующей теме приведу еще один пример который показывает, что из внутреннего класса любой вложенности доступны элементы любых внешних классов, даже объявленные как private.
Здесь у нас есть внешний класс Outer и внутри него класс Inner01 внутри которого, в свою очередь находится класс Inner02. Но обратите внимание что в методах getOutStr() классов Inner01 и Inner02 происходит обращение к приватному полю класса Outer outstr без каких либо уточнений. Так же и в классе Inner02 мы обращаемся к приватному полю strInn01 класса Inner01 тоже без каких либо уточнений. Но особый интерес представляет последняя строчка в данном коде.

Inner01.Inner02 getInner02() { return new Inner01().new Inner02();}

Здесь мы видим интересное применение оператора new. Данная строка возвращает нам ссылку на объект класса Inner02, который не может быть создан без привязки к экземплярам внешних классов. Таким образом, для его создания мы должны сперва создать и экземпляр класса Inner01, что и выполняет данная строка. Об этом более подробно мы расскажем уже скоро. Цель же этого примера в том, чтобы показать доступность элементов внешних классов из внутренних классов без каких либо уточнений. Хотя тут тоже есть некоторые интересные моменты, но мы рассмотрим их чуть позже.

IN0010

Применение вложенных этих классов показано на скрине слева.

Как видите, сперва нам необходимо создать объект внешнего класса out, затем используя его мы создаем объекты внутренних классов in01 и in02. Обратите внимание на то как они создаются.

Так же эти объекты можно было бы создать при помощи оператора new.

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

IN0011

.this  .super  .new

Как уже не однократно говорилось, создать объект внутреннего класса можно непосредственно при помощи особого синтаксиса оператора new. Как вы помните чтобы создать объект внутреннего класса обязательно должен присутствовать объект внешнего класса, поскольку каждый экземпляр внутреннего класса связан с экземпляром своего внешнего класса. До этого момента для создания объектов внутренних классов мы пользовались методами внешнего класса который возвращал ссылки на объекты внутренних классов. Но есть и другой способ. Рассмотрим его на предыдущем примере, модифицировав его:

IN0012

Как видим для создания объекта внутреннего класса указывается не имя его внешнего класса, как можно было бы ожидать, а объект внешнего класса, за которым, через точку, следует  оператор .new.
Обратите внимание что для создания объекта класса Inner01 мы использовали объект out, а для создания объекта класса Inner02 мы использовали объект inn01.
Чтобы создать объект внутреннего класса обязательно должен существовать объект его внешнего класса.
Строку создания объекта внутреннего класса Inner02, можно записать и по другому. Но это будет очень длинный и не особо читабельный синтаксис и он будет отличаться тем, что будут созданы новые объекты классов Outer и Inner01, а не использоваться уже созданыне out и in01.

В этом случае строка создания объекта класса Inner02 может выглядеть так:

Outer.Inner01.Inner02 in02 = new Outer().new Inner01().new Inner02();

Не правда ли немного замысловато :) Но если подумать то все логично.

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

Когда класс затеняет или замещает члены родительского класса, для ссылки на скрытый член можно применить ключевое слово super. Так же, данное ключевое слово можно задействовать и для работы во внутренних классах. Для этого используется имя текущего класса, затем точка, ключевое слово super, опять точка и имя затененного поля или метода. Хотя имя текущего класса можно и опускать (это зависит от версии JDK, все последние поддерживают упрощенный синтаксис). Рассмотрим это на простой новой мутации предыдущего примера.

IN0013

IN0014

В этом примере мы унаследовали классы Inner01 и Inner02 от внешнего класса Outer. Все три класса содержат строковое поле str. Соответственно оно затеняется в классах наследниках. Чтобы получить доступ к полю str класса Outer необходимо использовать ключевое слово super.

IN0015

Вывод программы представлен слева. Последние четыре строки генерируются последними двумя строками в классе Main, которые вызывают переопределенный метод printStr(), на объектах классов Inner01 и Inner02.

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

IN0016

И так в нашем примере слева у нас есть как иерархия вложенности, так и иерархия наследования. В нашем случае они параллельны, то есть класс Inner02 является внутренним классом класса Inner01, который в свою очередь является внутренним классом класса Outer. Так же и с наследованием. Класс Inner02 является наследником класса Inner01, который в свою очередь является наследником класса Outer. Все это кажется немного запутанным, хотя на самом деле так и есть :)

Самая изюминка тут заключается в конструкторах класса Inner02. В классе Inner01, такой изврат не нужен, так как он наследуется не от внутреннего класса, а от класса верхнего уровня, хотя при желании можно так же извратиться и там :)

Класс же Inner02 у нас расширяет внутренний класс Inner01 для которого, кстати он, так же еще, является и внутренним. Этот нюанс надо запомнить, так как унаследовать внутренний класс может и любой класс верхнего уровня и там будет немного другая ситуация с доступом к членам внешних классов.

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

IN0017

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

ссылкаНаВнешнийКласс.super();

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

IN0018

Сравните этот вывод с предыдущим выводом программы.

Теперь код

Inner02.super.str

Выводит нам строку Inner02>strInner01, а до этого выводил строку Inner02>strOuter00.

Это произошло потому, что сейчас класс Inner02 является наследником класса Inner01.

Если внутренний класс наследуется обычным образом, то он теряет доступ к private членам своего внешнего класса, в котором он был объявлен. Но доступ к членам protected, public  и пакетного доступа сохраняется. Все то же самое справедливо при наследовании класса любой глубины вложенности. Это означает что любой внутренний класс любой глубины вложенности имеет доступ ко всем членам внешних классов без дополнительного уточнения в именах этих членов. Наследник внутреннего класса (который наследуется обычным образом), так же может обращаться к этим членам без дополнительного уточнения их имени. Исключение составляют только члены внешних классов объявленные как private.IN0019

На примере слева красными стрелками показаны ошибки компиляции. Это произошло потому что данные поля объявлены как private во внешнем классе Outer и в классе родителе Inner01. Поэтому они не доступны.

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

 

IN0020

В новой мутации нашей программы все работает без ошибок.

Особое внимание обращайте на модификаторы доступа для членов классов и то каким образом к ним происходит обращение в классе External.

Клас External это класс верхнего уровня, который унаследовался от внутреннего класса Inner01.

На этом примере видна разница между обычным наследованием от внутреннего класса, как в классе External и между наследованием внутренним классом от от своего внешнего класса, как в классе Inner02.

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

IN0021

В классе Main были добавлены толкьо эти строки:

IN0022

По существу весь вывод после звездочек генерируется вызовом метода printStr() на объекте ex класса External.

Работа метода printStr() достаточно проста поэтому расписывать ее не буду.

तत् त्वम् असि

тат твам аси

ты есть то

Теперь будем говорить об этом или о том :) короче… о .this

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

Ты видишь суслика?

Теперь попытаемся увидеть суслика эту с…  ссылку.

IN0023IN0024Как вы уже знаете this хранит ссылку на текущий объектНа простом примере слева у нас есть два класса, внешний – Outer, и внутренний – Inner, который к тому же является наследником внешнего.

Вывод программы представлен ниже:

IN0025

Метод getThis() возвращает this :) как это ни странно :) И далее мы просто выводим это значение в консоль.

Тут все просто и нет ни чего такого чтобы мы еще не знали.

Если же вам потребуется получить ссылку на объект внешнего класса из объекта внутреннего класса, то укажите имя внешнего класса и через точку this:

имя_внешнего_класса.this;

IN0026IN0027Выведем новы штамм нашей программы, который представлен слева.

Здесь видно каким образом происходит обращение к объекту внешнего класса из объекта внутреннего класса.

 

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

IN0028

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

Теперь подвергнем прогу новой мутации и посмотрим как будет происходить обращение к затененным полям и родительским методам которые были переопределены в классе наследнике.

IN0029

IN0030

Вывод обновленной программы:

IN0031

Если вы помните, то мы так же могли обращаться к затененным полям и переопределенным методам родителей через ключевое слово .super. А сейчас мы это сделали с помощью ключевого слова .this. Но все таки между двумя этими способами есть разница. И сейчас мы попробуем разобраться с этим. Кроме того стоит напомнить, что в начале я сказал, что имя текущего класса перед .super можно опускать и тогда можно просто использовать обычно ключевое слово super (без точки в начале), для обращения к членам класса родителя. А вот с .this так уже сделать не получиться, для получения ссылки на объект внешнего класса это ключевое слово всегда необходимо предварять именем внешнего класса. Надеюсь вы уже поняли разницу в применении и значении этих ключевых слов при работе с внутренними классами? И так ключевое слово super относится к наследованию классов, в то время как ключевое слово .this предваренное именем внешнего класса относится к вложенности классов, то есть позволяет обращаться к объекту внешнего класса. И при этом совсем не обязательно, что вложенный класс должен быть наследником внешнего класса. Он может быть наследником любого другого класса. Точно так же любой класс может быть наследником внутреннего класса, если у него для этого достаточно прав. Не стоит путать линии наследования и линии вложенности – это разные вещи. Теперь рассмотрим пример и его вывод:

IN0032

IN0033

В данном примере у нас есть внешний класс Outer в него вложены два класса Inner и SuperInner. Класс SuperInner является наследником класса Inner. А класс Inner является наследником класса Outer. Уровень вложенности классов Inner и SuperInner одинаковый, а вот уровень наследования разный.

IN0034

Вывод программы представлен слева. Желтым подсвечена интересная строка.

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

IN0035

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

Но это так, напоминание о прошлом материале. Самое интересное и нужное для понимания текущего разговора о this и super находится в переопределенном методе getStr() в классе SuperInner. Там мы сперва обращаемся к локальному полю str, затем к полю str класса Inner (от которого унаследовались) при помощи ключевого слова super и затем обращаемся к полю str внешнего класса Outer используя ключевое слово .this предваренное именем внешнего класса.

Так же здесь стоит отметить один очень интересный момент. Как вы видите метод getThis() у нас определен только в классе Outer. Классы Inner и SuperInner его наследуют. Но в отличие от последнего случая, когда мы закомментировали метод getStr() в классе Inner, он всегда возвращает ссылку this на текущий объект для которого был вызван, в то время как унаследованный (не переопределенный) метод getStr() вернул нам строку str класса Outer хотя и был вызван на экземпляре класса Inner (это поведение было объяснено чуть выше).

Надеюсь ни кто не запутался? :) Ну тогда еще один интересный пример:

IN0036

IN0037

Вся хитрость состоит в том, что класс Inner является внутренним для класса Outer и наследником класса External и оба этих класса имеют поле str.

Вопрос на засыпку, какое значение выведет команда println(str) и почему?  Ведь для класса Inner видны и доступны оба поля str в классах External и Outer.

IN0038

Из вывода программы слева видно, что команда println(str) вывела значение унаследованного поля str от класса External. Хотя при этом поле str в классе Outer так же доступно через имя_класса.this.

С введением внутренних классов появляются две отдельные иерархии, к которым может принадлежать любой класс. Первая – это иерархия классов, от родителя к подклассу, определяющая поля и методы, наследуемые внутренним классом. Вторая – это иерархия вложенности, от окружающего класса к внутреннему классу, определяющая набор полей и методов, который входит в область видимости внутреннего класса (и поэтому этот набор ему доступен).

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

IN0039Я надеюсь что новая мутация класса Outer данной программы добавит ясности в этом вопросе. Теперь мы добавили поле str еще и в класс Inner. А так же строку для его вывода и строку для вывода поля str родительского класса. После этого вывод у программы стал такой:

IN0040

Первая строка println(str) выводит поле str класса Inner, вторя строка println(super.str) выводит поле str родительского класса External, а третья строка println(Outer.this.str) – поле str внешнего класса Outer.

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

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

17 июл. 2015 г.

Интерфейсы. Часть 5 – вложенные интерфейсы.

Интерфейс может быть объявлен членом класса или другого интерфейса. Такой интерфейс называется интерфейсом-членом или вложенным интерфейсом. Вложенный в класс интерфейс может быть объявлен как public, private или protected. Это отличает его от интерфейса верхнего уровня или интерфейса вложенного в другой интерфейс, который должен быть либо объявлен как public, либо, как уже было отмечено, должен использовать уровень доступа, заданный по умолчанию.

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

Вложенный в класс интерфейс объявленный с модификатором private не может быть имплементирован каким-либо классом. Он может использоваться только внутри того класса где был объявлен.

Ну и небольшой примерчик, который демонстрирует все вышеописанное…

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

I0023

Первая строка выводится статическим методом aintPrt2() вложенного в класс А интерфейса Aint1. Вывод генерирует строка 80. Вторая строка выводится при помощи некоторый танцев с бубном, а именно при помощи метода AaPrint() вложенного класса Aa. Вложенные классы мы пока не изучали, поэтому данный бубен может быт не понятен. Далее уже все просто :) ну и по правде сказать лень расписывать подробно. Если чё, то пишите комменты :)

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, но она может обратиться только к тем членам класса о которых известно классу А, поскольку является ссылочной переменной этого класса.

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

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