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

28 авг. 2015 г.

Внутренние классы. Часть 5 – анонимные классы.

Анонимный класс – это локальный класс без имени. Можно объявить анонимный (безымянный) класс, который может расширить (extends) другой класс или реализовать (implements) интерфейс. Объявление такого класса выполняется одновременно с созданием его объекта посредством оператора new.

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

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

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

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

Теперь рассмотрим на практике простой пример:

AN0001

На примере слева красными рамками обозначены фрагменты кода где объявляются анонимные классы. Первый имплементирует интерфейс Iout, второй расширяет класс External. Обратите внимание на двоеточие после фигурных скобок закрывающих объявление класса, оно является обязательным, так как по существу объявление анонимного класса представляет собой выражение. Это означает, что его можно записать как часть большего выражения, например вызова метода. Я привел достаточно простые примеры, хотя синтаксис все равно может показаться сложным. И начал я с объявления анонимного класса реализующего интерфейс Iout, так как на этом примере более легко понять почему анонимные классы называются анонимными – так как у них нет имени. Как вы помните невозможно создать экземпляр интерфейса, поскольку он является полностью абстрактным классом, а тут мы создаем класс реализующий интерфейс, но у этого класса нет имени и ссылку на этот анонимный класс мы присваиваем интерфейсной переменной iout. Надеюсь все просто? Ни кто не запутался?

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

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

Первую строку выводит метод outPrint(), другие – extPrint().

Как видите, синтаксис определения анонимного класса включает так же и создание экземпляра этого класса используя для этого ключевое слово new, за которым следует имя супер класса или интерфейса и затем определение самого анонимного класса в фигурных скобках за которыми следует двоеточие. Если имя, следующее за ключевым словом new, это имя класса, то анонимный класс является подклассом этого класса. Если имя, следующее за ключевым словом new, представляет собой интерфейс, то анонимный класс реализует этот интерфейс и расширяет класс Object. Данный синтаксис не позволяет указать секции extends, implements или имя класса. В следствии этого анонимный класс может реализовать только один интерфейс.

AN0003

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

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

AN0004

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

Для анонимных классов, так же как и для локальных, компилятор передает в конструктор скрытую ссылку .this на окружающий класс. Поэтому к объекту окружающего класса можно обращаться так же, как и в локальном классе – через имя_венешнего_класса.this.

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

Анонимные классы, так же как и локальные, имеют доступ к локальным переменным своего блока кода которые объявлены как final или они должны быть effectively final.

Анонимные классы имеют доступ ко всем членам своего вешнего класса.

Если в анонимном классе объявлена переменная с таким же именем как и в окружающем классе, то она затеняет переменную окружающего класса.

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

В анонимном классе вы не можете объявить статические инициализационные блоки.

В анонимном классе вы не можете объявить интерфейс.

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

В анонимном классе вы можете объявить:

  • Поля
  • Дополнительные методы (даже если этих методов нет в классе родителе)
  • Инициализационные блоки экземпляра
  • Локальные классы

Ну и теперь рассмотрим примеры всех вышеприведенных утверждений.

AN0006

Начнем пожалуй с этого :) то есть с .this. В Принципе это можно было еще на первом примере показать, но чет запамятовал. В общем первый пример мутировал в текущий. Я добавил строку str в класс Outer и затем обратился к ней из анонимного класса. Эти две строки подсвечены желтым.

Теперь у программы такой вывод:

AN0007

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

А теперь попробуем смоделировать конструктор анонимного класса :)

AN0008

Чтобы смоделировать конструктор в анонимном классе нам будет необходим класс от которого будет наследоваться наш анонимный класс. С анонимным классом имплементирующем интерфейс такой номер не прокатит, так как у интерфейсов нет конструкторов.

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

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

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

AN0009

Как видно из вывода программы аргумент i переданный в конструктор суперкласса Base доступен как в конструкторе суперкласса, так и в инициализаторе и методах анонимного класса, который в данном случае является и наследником класса Base и так же внутренним его классом. Именно поэтому ему доступно private static поле i. Я умышленно дал одинаковые названия аргументу и полю, чтобы продемонстрировать области видимости переменных и полей.

И еще один пример на тему эмулирования конструкторов в анонимных классах:

AN0010

AN0011

AN0012

 

 

 

 

 

 

 

В это программе не используются статические методы и поля для того чтобы продемонстрировать доступ к полям экземпляров в классах Base и External. По существу у нас есть два анонимных класса. Анонимный класс в классе Base является и вложенным в него и его же наследником, а в классе External анонимный класс является наследником класса Base и внутренним для класса External. Именно по этому в первом случае анонимный класс имеет доступ к private полю str класса Base, а во втором не имеет и использует для доступа к нему унаследованный метод getStr(). Собственно из вывода программы видно как она работает :)

AN0013Я немного изменил предыдущий пример, чтобы продемонстрировать наследование. Теперь класс External является наследником класса Base. И поэтому от туда был убран метод getThis() и добавлены конструкторы. Класс Main тоже претерпел небольшие изменения, туда была добавлена одна строка.

println("e.getStr = " + e.getStr());

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

AN0014

AN0015

 

С теорией по внутренним классам вроде пока все :) В следующем посте немного попрактикуемся по всей теме внутренних классов. Может по ходу практики еще всплывет что-то, что возможно было упущено в теории.

16 июл. 2015 г.

Интерфейсы. Часть 3 – наследование в интерфейсах.

Ключевое слово extends позволяет одному интерфейсу наследовать другой. Синтаксис определения такого наследования аналогичен синтаксису наследования классов, но в отличие от них интерфейсы могут наследоваться от любого множества интерфейсов, а не от одного как классы.
Интерфейс, который расширяет (наследуется) более одного интерфейса, наследует все методы и константы от каждого родителя и может определять собственные дополнительные абстрактные методы и константы.
Когда класс реализует интерфейс, который наследует другие интерфейсы, он должен предоставлять реализации всех методов, определенных внутри цепочки наследования интерфейса.
Приведем простой пример. На котором, я кстати покажу, что интерфейсы и классы могут содержаться в одном java файле, но в таком случае только один класс может быть public, остальные должны быть с пакетным доступом. Эффект ничем не отличается от создания отдельного java-файла для каждого класса и интерфейса. После компиляции получаются отдельные class файлы для каждого интерфейса и класса.

В этом примере интерфейс В наследуется от интерфейса А, в котором определены два метода с реализациями по умолчанию. В интерфейса В определен один метод без реализации по умолчанию.
Как видим в классе Ext1 мы реализовали только метод methB1(), поскольку для него нет реализации по умолчанию. Другие же методы – methA1() и methA2() были унаследованы в своей реализации по умолчанию.
В классе Ext2 мы реализовали все методы интерфейсов А и В переопределив их по своему.
Внимание стоит обратить на строки 54 и 58, где мы создаем объектные интерфейсные ссылки ext2 и ext3 соответственно. Но ссылка ext3 имеет интерфейсный тип А, именно поэтому на ней нельзя вызывать метод methB2(), хотя он у нас и реализован в классе Ext2.
Вывод у программы следующий:
I0018
GrabliДалее в наследовании интерфейсов начинаются грабельки. А что если в каком либо из родителей и потомков объявлены методы с одинаковой сигнатурой? А что если параметры методов совпадают, но отличается тип возвращаемого значения? И т.д и т.п.
Попробуем со всем этим разобраться… Хотя, возможно, все случаи тут и не перечислю, так как их достаточно много, но основные идеи постараюсь донести. А на остальные случаи грабли вам в помощь :)
  • Если класс реализует интерфейсы в которых есть методы с одинаковой сигнатурой и возвращаемым значением, но сам не переопределяет реализацию этих одинаковых методов, то тогда возникнет ошибка компиляции. Если же он переопределяет эти методы, то тогда все нормально.
  • Если в интерфейсах наследуемых один от другого есть одинаковые методы с реализациями по умолчанию, то предпочтение отдается реализации самого нижнего метода по умолчанию в цепочке, так как действует правило переопределения методов. Но если в классе переопределена реализация этого метода, то предпочтение отдается реализации метода в классе. Это естественное правило. Но благодаря новой форме ключевого слова super, можно обратиться к реализации метода по умолчанию родительского интерфейса из метода интерфейса наследника. Эта форма ключевого слова super выглядит следующим образом:

    имя_интерфейса.super.имя_метода();
    Если, допустим, из метода с реализацией по умолчанию интрефейса В, который унаследовался от интерфейса А, надо обратиться к методу по умолчанию интерфейса А, то это может выглядеть например так:

    A.super
.method();
  • Если наследуемые методы интерфейсов имеют одинаковые параметры, но различный тип возвращаемого значения, то возникает ошибка компиляции.

  • Теперь посмотрим это на примере:

    Программа генерирует следующий вывод:
    I0019
    Первые четыре строки генерируются имплементацией метода по умолчанию meth() интерфейса D, вызванные на объекте класса Dd. Последняя строка выводится имплементацией метода meth() в классе Cb вызванного на объекте этого класса. Опять же, обратите внимание на строки 47 и 50, на то каким образом определены типы ссылок.

    15 июл. 2015 г.

    Интерфейсы. Часть 1 – введение.

    Чтобы было проще понять интерфейсы в Java, можно их представлять как полностью абстрактные классы, то есть классы все методы которых не имеют реализации, а только лишь объявлены. Но так было до Java 8, в которой появилась возможность создавать в интерфейсе реализацию метода по умолчанию. Интерфейсы так же могут содержать поля, но все они будут объявлены как final и static, то есть по существу являются константами. Кроме того, интерфейсы служат для реализации подобия множественного наследования в Java, которое в чистом виде в Java не поддерживается.

    Как уже говорилось, унаследоваться (extends) в Java можно только от одного класса, каждый класс В или С происходит из неполной семьи, как показано на рисунке а ниже. Все классы происходят только от "Адама", от класса Object. Но часто возникает необходимость породить класс D от двух классов B и С, как показано на рисунке б. Это называется множественным наследованием (multiple inheritance). В множественном наследовании нет ничего плохого. Трудности возникают, если классы B и C сами порождены от одного класса А, как показано на рисунке в. Это так называемое "ромбовидное" наследование.

    I0001

    В самом деле, пусть в классе А определен метод f(), к которому мы обращаемся из некоторого метода класса D. Можем мы быть уверены, что метод f() выполняет то, что написано в классе А, т. е. это метод A.f()? Может, он переопределен в классах B и С? Если так, то каким вариантом мы пользуемся: В.f() или C.f()? Конечно, допустимо определить экземпляры классов и обращаться к методам этих экземпляров, но это совсем другая ситуация.

    В различных языках программирования этот вопрос решается по-разному, главным образом уточнением имени метода f(). Но при этом всегда нарушается принцип KISS. Вокруг множественного наследования всегда много споров, есть его ярые приверженцы и столь же ярые противники. Не будем встревать в эти споры, наше дело — наилучшим образом использовать средства языка для решения своих задач.

    Создатели языка Java после долгих споров и размышлений поступили радикально — запретили множественное наследование классов вообще. При расширении класса после слова extends можно написать только одно имя суперкласса. С помощью уточнения super можно обратиться только к членам непосредственного суперкласса.

    Но что делать, если все-таки при порождении надо использовать несколько предков? Например, у нас есть общий класс автомобилей Automobile, от которого можно породить класс грузовиков Truck и класс легковых автомобилей Car. Но вот надо описать пикап Pickup. Этот класс должен наследовать свойства и грузовых, и легковых автомобилей.

    В таких случаях используется еще одна конструкция языка Java — интерфейс. Внимательно проанализировав ромбовидное наследование, теоретики ООП выяснили, что проблему создает только реализация методов, а не их описание.

    Интерфейс (interface), в отличие от класса, содержит только константы, заголовки методов, и с Java 8 может содержать реализацию методов по умолчанию.

    Интерфейсы тоже размещаются в файлах с расширением .java, которые в свою очередь могут находится в пакетах и подпакетах, часто в тех же самых, что и классы, и тоже компилируются в class-файлы.

    Описание интерфейса начинается со слова interface, перед которым может стоять модификатор public, означающий, как и для класса, что интерфейс доступен всюду. Если же модификатора public нет, интерфейс будет виден только в своем пакете.

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

    Затем в фигурных скобках записываются в любом порядке константы, заголовки методов и реализации методов по умолчанию. Можно сказать, что в интерфейсе все методы абстрактные (те у которых нет реализации по умолчанию), но слово abstract писать не надо. Константы всегда статические, но слова static и final указывать не нужно. Все эти модификаторы принимаются по умолчанию. Все константы и методы в интерфейсах всегда открыты, не обязательно даже указывать модификатор public.

    Вот какую схему можно предложить для иерархии автомобилей:

    interface Automobile{ . . . }
    interface Car extends Automobile{ . . . }
    interface Truck extends Automobile{ . . . }
    interface Pickup extends Car, Truck{ . . . }

    Таким образом, интерфейс — это только набросок, эскиз. В нем указано, что делать, но не указано, как это делать (если нет реализации по умолчанию).

    Поскольку интерфейсы – это как-бы полностью абстрактные классы, то невозможно создать экземпляр интерфейса, но можно создать объектную ссылку интерфейсного типа. Ссылочная переменная интерфейса располагает сведениями только о тех методах, которые объявлены в этом интерфейсе.

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

    Использовать нужно не интерфейс, а его реализацию (implementation). Реализация интерфейса — это класс, в котором расписываются методы одного или нескольких интерфейсов. В заголовке класса после его имени или после имени его суперкласса, если он есть, записывается слово implements и, через запятую, перечисляются имена интерфейсов.

    Вот как можно реализовать иерархию автомобилей:

    interface Automobile{ . . . }
    interface Car extends Automobile{ . . . }
    class Truck implements Automobile{ . . . }
    class Pickup extends Truck implements Car{ . . . }

    или так

    interface Automobile{ . . . }
    interface Car extends Automobile{ . . . }
    interface Truck extends Automobile{ . . . }
    class Pickup implements Car, Truck{ . . . }

    Реализация интерфейса может быть неполной, некоторые методы интерфейса могут быть реализованы, а другие — нет. Такая реализация — абстрактный класс, его обязательно надо пометить модификатором abstract.

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

    Методы, которые реализуют интерфейс, должны быть объявлены как public.

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

    I0006

    I0005

    Слева представлены основной класс с методом main() и интерфейс в котором мы опеделили метод getSound() с реализацией по умолчанию, которая будет срабатывать если в классе имплементирующем этот интерфейс не будет описана реализация данного метода.

    Стоит обратить внимание что в классе AnimalSound мы создали массив объектных ссылок типа Soud (интерфейсный тип) и поместили туда ссылки на объекты, реализующие данный интерфейс.

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

    I0007

     

    И теперь посмотрим на классы Cow, Cat и Dog:

    I0002

    I0003

    I0004

    Как видно наши классы Cow, Cat и Dog которые имплементируют интерфейс Sound имеют каждый свою реализацию метода getSound().

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

    I0010

    I0011

    Как видно из примера класс MaxMin реализует два интрефейса.

     

    I0008

    I0009

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

    I0012

     

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

    Интерфейсы в Java
    Интерфейсы в Java
    Интерфейсы в Java
    Интерфейсы в Java. Но тут автор делает несколько ошибочных заявлений

    2 июл. 2015 г.

    Наследование. Практика – вызов конструкторов суперклассов.

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

    И так, у нас есть три класса А, В и С. Класс С наследуется от класс В, а класс В от класса А.

    C0001

    C0002

    C0003

    C0004

    C0005

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

    При создании объектов классов наследников сперва вызываются конструкторы суперклассов, начиная с самого первого суперкласса, в нашем случае класса А.

    Но так же надо понимать всю внутреннюю кухню этого процесса, что просто по умолчанию в конструкторах подклассов не явно присутствует вызов конструктора суперкласса – super().

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

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

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

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