Описание классов
Мы с вами уже узнали как сделать описание класса — создать его в отдельном файле. Этот способ наиболее часто встречается, но он не единственный. Описать класс можно не только в отдельном файле. В некоторых случаях такой подход упрощает архитектуру программы, упрощает чтение кода да и внешний вид становится более понятным и удобным.
Два и более классов внутри одного файла
Начнем с простого и достаточно удобного механизма — на самом деле вы можете описать не один, а много классов внутри одного файла. Единственное ограничение — только один класс внутри файла может быть объявлен как public и имя файла должно быть таким же как название этого класса.
Выглядит это достаточно просто:
1 2 3 4 5 6 7 |
public class FirstClass { } class SecondClass { } |
Вы можете вообще не использовать классы pubilc и в этом случае их можно называть как угодно. Например в файле FirstClass.java можно написать так:
1 2 3 4 5 6 7 |
class FirstSimpleClass { } class SecondClass { } |
И этот код будет компилироваться. Если вы используете NetBeans, то при сборке проекта у вас появится директория build/classes в которой вы сможете найти два файла class — их имена совпадают с именами классов. Теперь попробуем разобраться зачем такое описание может потребоваться.
Такие «закрытые» классы вы можете использовать ТОЛЬКО в том же пакете, в котором они находятся. Значит вы можете описать класс, который никто не увидит. Что в некоторых ситуациях бывает удобно. Например вы создаете класс, логика работы которого удобно разбивается на несколько классов. Т.е. удобно объявить еще один и больше классов. Но с другой стороны об этих вспомогательных классах другим классам в других пакетах лучше вообще не знать. Инкапсуляция на классовом уровне 🙂
Я не предлагаю прямо сейчас бросаться придумывать ситуации, когда это может вам потребоваться — как только вы в такую ситуацию попадете, то просто будете знать, что есть и такая возможность объявить класс.
Когда вы набираете определенный опыт, нередко становится достаточным просто узнать о существовании каких-либо интересных механизмов, технологий, конструкций — вы уже «угадываете», что «эта штука любопытная и о ней надо помнить, а может и покопаться». Опыт конкретного использование — это уже второй шаг. Не всегда все работает так, как описано в документации.
Вложенные классы
Итак, с несколькими классами внутри одного файла разобрались. Но это еще не все — вы можете объявить класс ВНУТРИ класса. Причем в отличии от предыдущего пункта здесь есть некоторый полет для фантазии по закрытости/открытости. Для простоты создадим три класса в двух разных пакетах — один класс будет использоваться для объявления классов внутри него (ResearchClass). Еще один класс (FirstClass) будет нахоится в том же пакете, а другой класс (SecondClass) в другом пакете. Вот такие у нас будут классы (обратите внимание на директиву package — именно там видно где какой класс находится):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
package edu.javacourse.many; public class ResearchClass { private class InternalOne { } protected class InternalTwo { } class InternalThree { } public class InternalFour { } static private class InternalStaticOne { } static protected class InternalStaticTwo { } static class InternalStaticThree { } static public class InternalStaticFour { } public void testInternal() { InternalOne inOne = new InternalOne(); InternalTwo inTwo = new InternalTwo(); InternalThree inThree = new InternalThree(); InternalFour inFour = new InternalFour(); InternalStaticOne inStOne = new InternalStaticOne(); InternalStaticTwo inStTwo = new InternalStaticTwo(); InternalStaticThree inStThree = new InternalStaticThree(); InternalStaticFour inStFour = new InternalStaticFour(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package edu.javacourse.many; import edu.javacourse.many.ResearchClass.InternalThree; public class FirstClass { public void testInternal() { ResearchClass.InternalStaticTwo inStTwo = new ResearchClass.InternalStaticTwo(); ResearchClass.InternalStaticThree inStThree = new ResearchClass.InternalStaticThree(); ResearchClass.InternalStaticFour inStFour = new ResearchClass.InternalStaticFour(); } } |
1 2 3 4 5 6 7 8 9 10 11 |
package edu.javacourse.one; import edu.javacourse.many.ResearchClass; public class SecondClass { public void testInternal() { ResearchClass.InternalStaticFour inStFour = new ResearchClass.InternalStaticFour(); } } |
Я не хочу долго и нудно перечислять правила какие внутренние классы будут видны в других классах в том же пакете, а какие — в других классах в других пакетах. Постарайтесь просто по коду «увидеть» эту зависимость. Сделаю только несколько пояснений.
Здесь важно отметить ключевое слово static при описании класса. Если оно есть, то в других классах этот внутренний класс виден и может быть создан экземпляр этого класса. Если нет — ничего не выйдет.
Само собой, что в классе ResearchClass в методе testInternal вы можете обратиться к любому нашему классу. В двух других классах я привел только те классы, которые могут быть там созданы. Как видите, в другом классе можно создать только те объекты, которые используют классы со словом static и на видимость влияют слова private, protected, public — как вы возможно помните, private не видим нигде, кроме класса-владельца, protected только в том же пакете (у предков тоже только в этом же пакете, но можно не указывать внешний класс ResearchClass — убедитесь сами), отсутствие каких-либо слов — в том же пакете. Ну а public видно всем.
В реальности внутренние классы достаточно широко используются — их можно встретить в стандартных пакетах. Они берут на себя задачи, которые важны для внешнего класса. Например, если у вас внешний класс вычисляет какой-либо алгоритм, то несколько внутренних классов могут быть использованы для разных путей вычисления. В качестве развлечения попроуйте описать класс внутри вложенного класса. Пример можно скачать тут — ManyClasses.zip
Существует возможность описать класс внутри метода — вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package edu.javacourse.many; public class ThirdClass { public void testInternal() { class TestInternal { } TestInternal ti = new TestInternal(); } } |
Думаю, несложно догадаться, что такой класс можно использовать ТОЛЬКО внутри этого метода. Можно создать экземпляр этого класса так, как показано в примере.
Анонимные классы
Есть еще один вариант описания классов — анонимные классы (anonymous class). Можно встретить еще такое название — inline class. Достаточно интересная возможность, которой многие пользуются. В этом случае вы сразу создаете объект и класс и еще раз использовать этот класс внутри своего кода вы не сможете. Вообще. Анонимный класс создается на основе какого-то класса или интерфейса и сразу в этом же кусочке кода вы переопределяете (в случае с интерфейсом — реализуете) нужный метод.
Для начала мы посмотрим пример кода, который создает анонимный класс для добавления к кнопке слушателя. Как мы уже видели, кнопка принимает любой класс, который реализует интерфейс ActionListenet. Наш слушатель будет просто выводить на экран фразу «Hello, world !». Сначала я покажу кусочек кода, который описывает анонимного слушателя, а потом уже просто пример класса целиком. Итак:
1 2 3 4 5 6 7 |
btn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Hello, world"); } }); |
Обратите внимание на синтаксис создания анонимного класса. Сначала я пишу new ActionListener — по сути создаю объект. Потом открываю фигурные скобки, внутри которых я переопределяю метод actionPerformed. Это метод, который любой класс, который реализует интерфейс ActionListener, должен иметь в своем описании. После закрытия фигурных скобок закрываю уже круглые скобки вызова метода addActionListener. Все, класс готов. Теперь посмотрим код для формы в кнопкой целиком:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package edu.javacourse.frame; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; public class HelloFrame extends JFrame { public HelloFrame() { JButton btn = new JButton("Say 'Hello'"); // Вот наш пример анонимного класса btn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Hello, world"); } }); // Кладем кнопку на СЕВЕР add(btn, BorderLayout.NORTH); // Задаем размеры формы setBounds(100, 100, 200, 100); // Устанавливаем свойство для закрытия приложения // при закрытии формы setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Делаем форму выидимой setVisible(true); } public static void main(String[] args) { HelloFrame hf = new HelloFrame(); } } |
При нажатии кнопки можно видеть надпись «Hello, world» на консоли. Как-то так
Полный текст можно скачать здесь — HelloFrame.zip.
Внутренние классы и анонимные классы применяются в разных ситуациях и многое зависит от вкуса и предпочтений программиста. В общем весьма субъективно и опыт использования этого механизма просто копится с практикой. Не могу загадывать, но думаю, что мы обязательно встретимся с этими конструкциями в дальнейшем.
Недавно мне попался очень любопытный случай, когда мой собеседник утверждал, что создал экземпляр абстрактного класса. Давайте рассмотрим этот забавный случай.
Итак, вот его абстрактный класс:
1 2 3 4 5 6 7 8 |
package edu.javacourse.abs; public abstract class SimpleAbstract { public void sayHello() { System.out.println("HELLO"); } } |
А теперь вариант якобы создания объекта абстрактного класса:
1 2 3 4 5 6 7 8 9 |
package edu.javacourse.abs; public class StartClass { public static void main(String[] args) { SimpleAbstract sa = new SimpleAbstract() {}; sa.sayHello(); } } |
Обратите внимание на строку 6 — где создается объект класса SimpleAbstract. Посмотрите ОЧЕНЬ внимательно — там в конце стоят ФИГУРНЫЕ СКОБКИ. Мы получили не класс SimpleAbstract, а его наследника — анонимный класс, который уже НЕ абстрактный. посему и получилось. Вот такой вот любопытный случай.
Инициализация
В данном разделе я расскажу о весьма удобном механизме установки начальных значений полей у объекта и класса. Если забежать вперед, то задача установки начальных значений настолько важна, что для нее придумана и реализована не одна технология и библиотека. Но мы пока не будем углубляться столь сильно — просто познакомимся с некоторыми возможностями языка Java.
Итак, как вы возможно помните, для установки значений поля мы уже использовали два варианта:
- Установка при объявлении свойства — вот так: private int f = 0;
- Установка в конструкторе
Кроме этих способов вы можете использовать еще один (в двух модификациях):
- Статический блок инициализации
- Блок инициализации сущности (объекта)
Первый блок вызывается при создании класса, после установки значений статических свойств при объявлении.
Второй вызывается во время создания объекта сразу перед конструктором, но после того, как будут установлены поля, которым при объявлении присваивается какое-то значение.
Никогда не любил много слов и букв — на примере всегда проще и понятнее. Так что сразу смотрим пример объявления обеих секций.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package edu.javacourse.init; public class InitField { protected static String staticField; protected String field; // вызывается при загрузке класса в Java-машину static { staticField = "Static test"; System.out.println("Static init:" + staticField); } // вызывается при создании объекта { field = "Test"; System.out.println("Object init:" + field); } public static void main(String[] args) { InitField init1 = new InitField(); System.out.println(init1.field); InitField init2 = new InitField(); System.out.println(init2.field); } } |
При запуске этого примера вы должны увидеть вот такой вывод:
Static init:Static test
Object init:Test
Test
Object init:Test
Test
Сразу видно, что секция static вызывается только один раз, а секция для экземпляра (инстанса — есть такой термин у программистов. На английском Instance — экземпляр объекта) вызывается при создании каждого объекта.
ВАЖНО !!! Обе секции могут использоваться для инициализации полей final. static для статических полей, а блок для инстанса — для полей объекта.
Можно посмотреть, как будут вести себя такие секции при наследовании. Создадим новый класс-наследник от нашего InitField.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package edu.javacourse.init; public class InitFieldTwo extends InitField { static { staticField = "Other static"; System.out.println("Static init two:" + staticField); } { field = "Other"; System.out.println("Object init two:" + field); } } |
Теперь изменим метод main где будем создавать объект класса InitFieldTwo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package edu.javacourse.init; public class InitField { protected static String staticField; protected String field; static { staticField = "Static test"; System.out.println("Static init:" + staticField); } { field = "Test"; System.out.println("Object init:" + field); } public static void main(String[] args) { InitFieldTwo init2 = new InitFieldTwo(); System.out.println(init2.field); } } |
Вывод теперь будет вот такой
Static init:Static test
Static init two:Other static
Object init:Test
Object init two:Other
Other
Как видим, секции инициализации наследуются — вызывается у родителя, потом у потомка. Как говорил герой фильма «Здравствуйте, я ваша тетя» — «Она любит выпить. Этим надо воспользоваться» (Видео).
В принципе ничего сложного и поразительного в этих возможностях нет. Но когда программист предлагает читать такой код:
1 2 3 4 |
context.checking(new Expectations() {{ oneOf(subscriber).receive(message); will(returnValue("Hello world")); }}); |
не сразу можно сообразить, что это анонимный класс, который имеет секцию инициализации и смотреть его удобнее так:
1 2 3 4 5 6 |
context.checking(new Expectations() { { oneOf(subscriber).receive(message); will(returnValue("Hello world")); } }); |
Я привел вам реальный пример для пакета JMock — специальный пакет для написания автоматических тестов. Как я неоднократно говорил и буду говорить — учитесь читать чужой код. Я сам редко использую конструкции инициализации и вообще предпочитаю писать пусть иногда избыточный, но простой и понятный код. Но это не значит. что «простой и понятный код» для меня будет понятен кому-то другому. На понятность влияет знание всевозможных конструкций языка. Этим мы сейчас и занимаемся — изучаем конструкции языка.
Перечисления
Перечисления (enum) — еще один достаточно удобный механизм, который появился в Java версии 1.5. Нередко в программе удобно описать некоторое конечное множество констант. Например список планет солнечной системы, дни недели и т.п. С одной стороны делать это динамическим множеством бессмысленно — множество достаточно устоявшееся. С другой стороны просто описать несколько констант тоже не самое лучшее решение. Например для дней недели такой вариант записи не очень красиво выглядит:
1 2 3 4 5 |
public static final String MONDAY = "MONDAY"; public static final String TUESDAY = "TUESDAY"; public static final String WEDNESDAY = "WEDNESDAY"; ... public static final String SUNDAY = "SUNDAY"; |
да и пользоваться им неудобно — это же ДНИ НЕДЕЛИ, а не СТРОКИ. Почему рождаются такие рассуждения мы уже говорили — сложность программ требует декомпозиции и абстрагирования. Перечисление — это еще один способ абстрагироваться.
Для дней недели (и подобных типов данных) введено понятие перечисления — enum. Записывается оно достаточно несложно.
1 2 3 |
public enum Weekdays { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } |
Во-первых мы получаем жесткое множество по количеству элементов. И что еще более важно — это совершенно отдельный тип, который можно использовать в каком-либо описании. Например выходной в расписании.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package edu.javacourse.init; public class Scheduler { public enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } public static void main(String[] args) { Weekday wd = Weekday.FRIDAY; System.out.println(wd.toString()); } } |
Можно видеть. что переменная wd имеет понятный тип — день недели. Именно день недели — его нельзя спутать с чем-либо иным. Я специально привел пример с вложенным описанием — такое описание достаточно часто используется — есть класс и есть некоторый набор перечислений, который этот класс использует.
Если запустить наш пример, то на консоли будет выведено слово FRIDAY.
Но enum не заканчивается на этом (хотя следующая возможность встречалась мне крайне редко). Вы можете определить значение, которое «привязывается» к константе и потом можете ее использовать. Давайте посмотрим как это может быть записано.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package edu.javacourse.init; public class Scheduler { public enum Weekday { MONDAY("Понедельник"), TUESDAY("Вторник"), WEDNESDAY("Среда"), THURSDAY("Четверг"), FRIDAY("Пятница"), SATURDAY("Суббота"), SUNDAY("Воскресенье"); String value; Weekday(String value) { this.value = value; } @Override public String toString() { return value; } } public static void main(String[] args) { Weekday wd = Weekday.FRIDAY; System.out.println(wd.toString()); } } |
Сначала обратим внимание, что рядом с именем константы (MONDAY, FRIDAY) появилось значение в скобках. Чтобы это было возможно надо определить КОНСТРУКТОР для enum с таким же типом — в данном случае это String. Также надо создать поле, в котором будет хранится значение строки — в данном случае именно в ней хранятся русские название дней недели. В самом конце мы переопределили метод toString() в котором мы используем наше поле value. При запуске этого примера вы получите уже строку «Пятница».
Есть еще один способ переопределить строковое значение переменной типа Weekday — переопределить метод toString для каждого значения — ниже он представлен. Запись конечно громоздкая и наверно не очень будет интересно с ней работать, но тем не менее такая возможность есть. По своему опыту могу сказать — если возможность есть, то всегда найдется тот, кому захочется ее использовать. Так что надо о ней знать. Я такой вариант в своей практике никогда не использовал.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
package edu.javacourse.init; public class Scheduler { public enum Weekday { MONDAY { @Override public String toString() { return "Понедельник"; } }, TUESDAY { @Override public String toString() { return "Вторник"; } }, /** * */ WEDNESDAY { @Override public String toString() { return "Среда"; } }, THURSDAY { @Override public String toString() { return "Четверг"; } }, FRIDAY { @Override public String toString() { return "Пятница"; } }, SATURDAY { @Override public String toString() { return "Суббота"; } }, SUNDAY { @Override public String toString() { return "Воскресенье"; } } } public static void main(String[] args) { // Как можно видеть - будет выводится слово "Пятница" при // вызове всеми нижеприведенными способами System.out.println(Weekday.FRIDAY); Weekday wd = Weekday.FRIDAY; System.out.println(wd); System.out.println(wd.toString()); } } |
Думаю, что в этой статье я перечислил подавляющее большинство конструкций для определения классов — внутреннего, анонимного, перечисления. Еще раз повторюсь — практика использования этих конструкций копится ТОЛЬКО тогда, когда вы что-то действительно пишете — пусть несложные, но полностью рабочие программы надо писать обязательно.
И теперь нас ждет следующая статья: Исключения.