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

 

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

10 комментариев:

  1. Спасибо за статью. Лучше этой не нашел. Красавчик!

    ОтветитьУдалить
  2. Последние слова в этом абзаце "Как видите, синтаксис определения анонимного класса включает так же и создание экземпляра этого класса используя для этого ключевое слово new, за которым следует имя супер класса или интерфейса и затем определение самого анонимного класса в фигурных скобках за которыми следует двоеточие."
    Точка с запятой или двоеточие?

    ОтветитьУдалить
    Ответы
    1. Да вы правы, следует точка с запятой. Это я ошибся в тексте. Думал одно напечатал другое :)

      Удалить
    2. И еще чуть выше в тексте есть такая же ошибка. Исправлять лень :) Сохраню для истории как авторское :)

      Удалить
  3. Ничего страшного, все равно крутой блог)

    ОтветитьУдалить
  4. В 3 примере, пишите что необходимо в конструктор подавать final переменную, а в листинге передаете int i в метод getAnonBase(int i). Пример не скомпилируется в таком виде.
    Но в целом спасибо за цикл статей, очень помогло разобраться с вложенными классами.

    ОтветитьУдалить
  5. У меня все компилируется :)

    ОтветитьУдалить
  6. В третьем примере четко говориться что они должны быть effectively final. Видимо вы для компиляции используете JDK 7, а там такого еще не было.

    ОтветитьУдалить
  7. А использование анонимного класса не порождает потенциальную утечку памяти из-за неявного this на внешний класс? Я читал, что именно это один из самых больших недостатков идиомы double brace initialization. Так-то по логике вещей получается, что если у нас есть, допустим, публичный фабричный метод, который наружу отдает объект, созданный с помощью анонимного класса, то в итоге из-за неявного this сборщик мусора не сможет пометить экземпляр внешнего класса как ненужный и очистить занимаемую им память. Это особенно критично, если внешний класс с фабричным методом содержит в своих приватных полях что-то тяжёлое и при этом для использования фабричного метода объект внешнего класса нужно постоянно создавать через new. В этом случае нагрузка на хип "ненужным балластом" будет очень ощутимой. Впрочем, внешний класс можно оформить как синглтон или использовать DependencyInjection IoC-контейнеры.

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