Описание классов

Мы с вами уже узнали как сделать описание класса — создать его в отдельном файле. Этот способ наиболее часто встречается, но он не единственный. Описать класс можно не только в отдельном файле. В некоторых случаях такой подход упрощает архитектуру программы, упрощает чтение кода да и внешний вид становится более понятным и удобным.

Два и более классов внутри одного файла

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

Вы можете вообще не использовать классы pubilc и в этом случае их можно называть как угодно. Например в файле FirstClass.java можно написать так:

И этот код будет компилироваться. Если вы используете NetBeans, то при сборке проекта у вас появится директория build/classes в которой вы сможете найти два файла class — их имена совпадают с именами классов. Теперь попробуем разобраться зачем такое описание может потребоваться.
Такие «закрытые» классы вы можете использовать ТОЛЬКО в том же пакете, в котором они находятся. Значит вы можете описать класс, который никто не увидит. Что в некоторых ситуациях бывает удобно. Например вы создаете класс, логика работы которого удобно разбивается на несколько классов. Т.е. удобно объявить еще один и больше классов. Но с другой стороны об этих вспомогательных классах другим классам в других пакетах лучше вообще не знать. Инкапсуляция на классовом уровне 🙂
Я не предлагаю прямо сейчас бросаться придумывать ситуации, когда это может вам потребоваться — как только вы в такую ситуацию попадете, то просто будете знать, что есть и такая возможность объявить класс.
Когда вы набираете определенный опыт, нередко становится достаточным просто узнать о существовании каких-либо интересных механизмов, технологий, конструкций — вы уже «угадываете», что «эта штука любопытная и о ней надо помнить, а может и покопаться». Опыт конкретного использование — это уже второй шаг. Не всегда все работает так, как описано в документации.

Вложенные классы

Итак, с несколькими классами внутри одного файла разобрались. Но это еще не все — вы можете объявить класс ВНУТРИ класса. Причем в отличии от предыдущего пункта здесь есть некоторый полет для фантазии по закрытости/открытости. Для простоты создадим три класса в двух разных пакетах — один класс будет использоваться для объявления классов внутри него (ResearchClass). Еще один класс (FirstClass) будет нахоится в том же пакете, а другой класс (SecondClass) в другом пакете. Вот такие у нас будут классы (обратите внимание на директиву package — именно там видно где какой класс находится):

Я не хочу долго и нудно перечислять правила какие внутренние классы будут видны в других классах в том же пакете, а какие — в других классах в других пакетах. Постарайтесь просто по коду «увидеть» эту зависимость. Сделаю только несколько пояснений.
Здесь важно отметить ключевое слово static при описании класса. Если оно есть, то в других классах этот внутренний класс виден и может быть создан экземпляр этого класса. Если нет — ничего не выйдет.
Само собой, что в классе ResearchClass в методе testInternal вы можете обратиться к любому нашему классу. В двух других классах я привел только те классы, которые могут быть там созданы. Как видите, в другом классе можно создать только те объекты, которые используют классы со словом static и на видимость влияют слова private, protected, public — как вы возможно помните, private не видим нигде, кроме класса-владельца, protected только в том же пакете (у предков тоже только в этом же пакете, но можно не указывать внешний класс ResearchClass — убедитесь сами), отсутствие каких-либо слов — в том же пакете. Ну а public видно всем.
В реальности внутренние классы достаточно широко используются — их можно встретить в стандартных пакетах. Они берут на себя задачи, которые важны для внешнего класса. Например, если у вас внешний класс вычисляет какой-либо алгоритм, то несколько внутренних классов могут быть использованы для разных путей вычисления. В качестве развлечения попроуйте описать класс внутри вложенного класса. Пример можно скачать тут — ManyClasses.zip

Существует возможность описать класс внутри метода — вот так:

Думаю, несложно догадаться, что такой класс можно использовать ТОЛЬКО внутри этого метода. Можно создать экземпляр этого класса так, как показано в примере.

Анонимные классы

Есть еще один вариант описания классов — анонимные классы (anonymous class). Можно встретить еще такое название — inline class. Достаточно интересная возможность, которой многие пользуются. В этом случае вы сразу создаете объект и класс и еще раз использовать этот класс внутри своего кода вы не сможете. Вообще. Анонимный класс создается на основе какого-то класса или интерфейса и сразу в этом же кусочке кода вы переопределяете (в случае с интерфейсом — реализуете) нужный метод.
Для начала мы посмотрим пример кода, который создает анонимный класс для добавления к кнопке слушателя. Как мы уже видели, кнопка принимает любой класс, который реализует интерфейс ActionListenet. Наш слушатель будет просто выводить на экран фразу «Hello, world !». Сначала я покажу кусочек кода, который описывает анонимного слушателя, а потом уже просто пример класса целиком. Итак:

Обратите внимание на синтаксис создания анонимного класса. Сначала я пишу new ActionListener — по сути создаю объект. Потом открываю фигурные скобки, внутри которых я переопределяю метод actionPerformed. Это метод, который любой класс, который реализует интерфейс ActionListener, должен иметь в своем описании. После закрытия фигурных скобок закрываю уже круглые скобки вызова метода addActionListener. Все, класс готов. Теперь посмотрим код для формы в кнопкой целиком:

При нажатии кнопки можно видеть надпись «Hello, world» на консоли. Как-то так

Полный текст можно скачать здесь — HelloFrame.zip.

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

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

А теперь вариант якобы создания объекта абстрактного класса:

Обратите внимание на строку 6 — где создается объект класса SimpleAbstract. Посмотрите ОЧЕНЬ внимательно — там в конце стоят ФИГУРНЫЕ СКОБКИ. Мы получили не класс SimpleAbstract, а его наследника — анонимный класс, который уже НЕ абстрактный. посему и получилось. Вот такой вот любопытный случай.

Инициализация

В данном разделе я расскажу о весьма удобном механизме установки начальных значений полей у объекта и класса. Если забежать вперед, то задача установки начальных значений настолько важна, что для нее придумана и реализована не одна технология и библиотека. Но мы пока не будем углубляться столь сильно — просто познакомимся с некоторыми возможностями языка Java.
Итак, как вы возможно помните, для установки значений поля мы уже использовали два варианта:

  1. Установка при объявлении свойства — вот так: private int f = 0;
  2. Установка в конструкторе

Кроме этих способов вы можете использовать еще один (в двух модификациях):

  1. Статический блок инициализации
  2. Блок инициализации сущности (объекта)

Первый блок вызывается при создании класса, после установки значений статических свойств при объявлении.
Второй вызывается во время создания объекта сразу перед конструктором, но после того, как будут установлены поля, которым при объявлении присваивается какое-то значение.
Никогда не любил много слов и букв — на примере всегда проще и понятнее. Так что сразу смотрим пример объявления обеих секций.

При запуске этого примера вы должны увидеть вот такой вывод:

Static init:Static test
Object init:Test
Test
Object init:Test
Test

Сразу видно, что секция static вызывается только один раз, а секция для экземпляра (инстанса — есть такой термин у программистов. На английском Instance — экземпляр объекта) вызывается при создании каждого объекта.
ВАЖНО !!! Обе секции могут использоваться для инициализации полей final. static для статических полей, а блок для инстанса — для полей объекта.
Можно посмотреть, как будут вести себя такие секции при наследовании. Создадим новый класс-наследник от нашего InitField.

Теперь изменим метод main где будем создавать объект класса InitFieldTwo

Вывод теперь будет вот такой

Static init:Static test
Static init two:Other static
Object init:Test
Object init two:Other
Other

Как видим, секции инициализации наследуются — вызывается у родителя, потом у потомка. Как говорил герой фильма «Здравствуйте, я ваша тетя» — «Она любит выпить. Этим надо воспользоваться» (Видео).

В принципе ничего сложного и поразительного в этих возможностях нет. Но когда программист предлагает читать такой код:

не сразу можно сообразить, что это анонимный класс, который имеет секцию инициализации и смотреть его удобнее так:

Я привел вам реальный пример для пакета JMock — специальный пакет для написания автоматических тестов. Как я неоднократно говорил и буду говорить — учитесь читать чужой код. Я сам редко использую конструкции инициализации и вообще предпочитаю писать пусть иногда избыточный, но простой и понятный код. Но это не значит. что «простой и понятный код» для меня будет понятен кому-то другому. На понятность влияет знание всевозможных конструкций языка. Этим мы сейчас и занимаемся — изучаем конструкции языка.

Перечисления

Перечисления (enum) — еще один достаточно удобный механизм, который появился в Java версии 1.5. Нередко в программе удобно описать некоторое конечное множество констант. Например список планет солнечной системы, дни недели и т.п. С одной стороны делать это динамическим множеством бессмысленно — множество достаточно устоявшееся. С другой стороны просто описать несколько констант тоже не самое лучшее решение. Например для дней недели такой вариант записи не очень красиво выглядит:

да и пользоваться им неудобно — это же ДНИ НЕДЕЛИ, а не СТРОКИ. Почему рождаются такие рассуждения мы уже говорили — сложность программ требует декомпозиции и абстрагирования. Перечисление — это еще один способ абстрагироваться.
Для дней недели (и подобных типов данных) введено понятие перечисления — enum. Записывается оно достаточно несложно.

Во-первых мы получаем жесткое множество по количеству элементов. И что еще более важно — это совершенно отдельный тип, который можно использовать в каком-либо описании. Например выходной в расписании.

Можно видеть. что переменная wd имеет понятный тип — день недели. Именно день недели — его нельзя спутать с чем-либо иным. Я специально привел пример с вложенным описанием — такое описание достаточно часто используется — есть класс и есть некоторый набор перечислений, который этот класс использует.
Если запустить наш пример, то на консоли будет выведено слово FRIDAY.
Но enum не заканчивается на этом (хотя следующая возможность встречалась мне крайне редко). Вы можете определить значение, которое «привязывается» к константе и потом можете ее использовать. Давайте посмотрим как это может быть записано.

Сначала обратим внимание, что рядом с именем константы (MONDAY, FRIDAY) появилось значение в скобках. Чтобы это было возможно надо определить КОНСТРУКТОР для enum с таким же типом — в данном случае это String. Также надо создать поле, в котором будет хранится значение строки — в данном случае именно в ней хранятся русские название дней недели. В самом конце мы переопределили метод toString() в котором мы используем наше поле value. При запуске этого примера вы получите уже строку «Пятница».
Есть еще один способ переопределить строковое значение переменной типа Weekday — переопределить метод toString для каждого значения — ниже он представлен. Запись конечно громоздкая и наверно не очень будет интересно с ней работать, но тем не менее такая возможность есть. По своему опыту могу сказать — если возможность есть, то всегда найдется тот, кому захочется ее использовать. Так что надо о ней знать. Я такой вариант в своей практике никогда не использовал.

Думаю, что в этой статье я перечислил подавляющее большинство конструкций для определения классов — внутреннего, анонимного, перечисления. Еще раз повторюсь — практика использования этих конструкций копится ТОЛЬКО тогда, когда вы что-то действительно пишете — пусть несложные, но полностью рабочие программы надо писать обязательно.

И теперь нас ждет следующая статья: Исключения.