Статьи

Как и когда использовать Перечисления и Аннотации

Эта статья является частью нашего Академического курса под названием Advanced Java .

Этот курс призван помочь вам наиболее эффективно использовать Java. В нем обсуждаются сложные темы, включая создание объектов, параллелизм, сериализацию, рефлексию и многое другое. Он проведет вас через ваше путешествие в мастерство Java! Проверьте это здесь !

1. Введение

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

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

С другой стороны, аннотации представляют собой особый вид метаданных, которые могут быть связаны с различными элементами и конструкциями языка Java. Интересно, что аннотации внесли большой вклад в устранение шаблонных XML-дескрипторов, используемых в экосистеме Java в основном повсеместно. Они представили новый, безопасный для типов и надежный способ настройки и настройки.

2. Перечисляет как специальные классы

До того, как перечисления были введены в язык Java, обычным способом моделирования набора фиксированных значений в Java было просто объявить несколько констант. Например:

1
2
3
4
5
6
7
8
9
public class DaysOfTheWeekConstants {
    public static final int MONDAY = 0;
    public static final int TUESDAY = 1;
    public static final int WEDNESDAY = 2;
    public static final int THURSDAY = 3;
    public static final int FRIDAY = 4;
    public static final int SATURDAY = 5;
    public static final int SUNDAY = 6;
}

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

1
2
3
public boolean isWeekend( int day ) {
    return( day == SATURDAY || day == SUNDAY );
}

С логической точки зрения аргумент day должен иметь одно из значений, объявленных в классе DaysOfTheWeekConstants . Однако невозможно догадаться об этом без написания дополнительной документации (и чтения кем-то впоследствии). Для компилятора Java вызов, подобный isWeekend (100) выглядит абсолютно корректно и не вызывает проблем.

Здесь на помощь приходят перечисления. Перечисления позволяют заменять константы на типизированные значения и везде использовать эти типы. Давайте перепишем решение выше, используя перечисления.

1
2
3
4
5
6
7
8
9
public enum DaysOfTheWeek {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

Что изменилось, так это то, что class становится enum а возможные значения перечислены в определении enum. Однако отличительной чертой является то, что каждое отдельное значение является экземпляром класса enum, в котором оно объявлено (в нашем примере, DaysOfTheWeek ). Таким образом, всякий раз, когда используется enum, компилятор Java может выполнять проверку типов. Например:

1
2
3
public boolean isWeekend( DaysOfTheWeek day ) {
    return( day == SATURDAY || day == SUNDAY );
}

Обратите внимание, что использование схемы именования в верхнем регистре в перечислениях — это просто соглашение, ничто действительно не мешает вам этого не делать.

3. Перечисления и поля экземпляра

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public enum DaysOfTheWeekFields {
    MONDAY( false ),
    TUESDAY( false ),
    WEDNESDAY( false ),
    THURSDAY( false ),
    FRIDAY( false ),
    SATURDAY( true ),
    SUNDAY( true );
 
    private final boolean isWeekend;
 
    private DaysOfTheWeekFields( final boolean isWeekend ) {
        this.isWeekend = isWeekend;
    }
 
    public boolean isWeekend() {
        return isWeekend;
    }
}

Как мы видим, значения перечислений являются просто вызовами конструктора с тем упрощением, что new ключевое слово не требуется. Свойство isWeekend() можно использовать для определения, представляет ли значение день недели или конец недели. Например:

1
2
3
public boolean isWeekend( DaysOfTheWeek day ) {
    return day.isWeekend();
}

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

4. Перечисления и интерфейсы

Еще одна интересная особенность, которая еще раз подтверждает, что перечисления являются просто специализированными классами, заключается в том, что они могут реализовывать интерфейсы (однако перечисления не могут расширять какие-либо другие классы по причинам, объясненным ниже в разделе « Перечисления и общие сведения »). Например, давайте представим интерфейс DayOfWeek .

1
2
3
interface DayOfWeek {
    boolean isWeekend();
}

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

01
02
03
04
05
06
07
08
09
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
public enum DaysOfTheWeekInterfaces implements DayOfWeek {
    MONDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    TUESDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    WEDNESDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    THURSDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    FRIDAY() {
        @Override
        public boolean isWeekend() {
            return false;
        }
    },
    SATURDAY() {
        @Override
        public boolean isWeekend() {
            return true;
        }
    },
    SUNDAY() {
        @Override
        public boolean isWeekend() {
            return true;
        }
    };
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public enum DaysOfTheWeekFieldsInterfaces implements DayOfWeek {
    MONDAY( false ),
    TUESDAY( false ),
    WEDNESDAY( false ),
    THURSDAY( false ),
    FRIDAY( false ),
    SATURDAY( true ),
    SUNDAY( true );
 
    private final boolean isWeekend;
 
    private DaysOfTheWeekFieldsInterfaces( final boolean isWeekend ) {
        this.isWeekend = isWeekend;
    }
 
    @Override
    public boolean isWeekend() {
        return isWeekend;
    }
}

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

5. Перечисления и дженерики

Хотя это не видно на первый взгляд, в Java существует связь между перечислениями и обобщениями. Каждое перечисление в Java автоматически наследуется от универсального класса Enum< T > , где T — это сам тип перечисления. Компилятор Java выполняет это преобразование от имени разработчика во время компиляции, расширяя объявление public enum DaysOfTheWeek примерно так:

1
2
3
public class DaysOfTheWeek extends Enum< DaysOfTheWeek > {
    // Other declarations here
}

Это также объясняет, почему перечисления могут реализовывать интерфейсы, но не могут расширять другие классы: они неявно расширяют Enum< T > и, как мы знаем из части 2 учебника, используя методы, общие для всех объектов , Java не поддерживает множественное наследование.

Тот факт, что каждое перечисление расширяет Enum< T > позволяет определять универсальные классы, интерфейсы и методы, которые ожидают экземпляры перечислимых типов в качестве аргументов или параметров типа. Например:

1
2
3
public< T extends Enum < ? > > void performAction( final T instance ) {
    // Perform some action here
}

В приведенном выше объявлении метода тип T ограничен экземпляром любого перечисления, и компилятор Java это проверит.

6. Удобные методы Enums

Базовый класс Enum< T > предоставляет несколько полезных методов, которые автоматически наследуются каждым экземпляром enum.

метод Описание
String name() Возвращает имя этой константы перечисления, в точности как объявлено в объявлении перечисления.
int ordinal() Возвращает порядковый номер этой константы перечисления (ее позиция в объявлении перечисления, где начальной константе присвоен порядковый номер нуля).

Таблица 1

Кроме того, компилятор Java автоматически генерирует еще два полезных static метода для каждого встречаемого типа перечисления (давайте обратимся к конкретному типу перечисления как T ).

метод Описание
T[] values() Возвращает все объявленные константы перечисления для перечисления T
T valueOf(String name) Возвращает константу перечисления T с указанным именем.

Таблица 2

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

01
02
03
04
05
06
07
08
09
10
11
12
13
public void performAction( DaysOfTheWeek instance ) {
    switch( instance ) {
        case MONDAY:
            // Do something
            break;
 
        case TUESDAY:
            // Do something
            break;
 
        // Other enum constants here
    }
}

7. Специализированные коллекции: EnumSet и EnumMap

Экземпляры перечислений, как и все другие классы, могут использоваться со стандартной библиотекой Java-коллекции. Однако некоторые типы коллекций были специально оптимизированы для перечислений, и в большинстве случаев их рекомендуется использовать вместо аналогов общего назначения.

Мы рассмотрим два специализированных типа коллекций: EnumSet< T > и EnumSet< T > EnumMap< T, ? > Оба очень просты в использовании, и мы собираемся начать с EnumSet< T > .

EnumSet< T > — это обычный набор, оптимизированный для эффективного хранения перечислений. Интересно, что EnumSet< T > не может быть создан с использованием конструкторов, и вместо этого предоставляет множество полезных фабричных методов (мы рассмотрели фабричный шаблон в первой части урока « Как создавать и уничтожать объекты» ).

Например, allOf фабрики allOf создает экземпляр EnumSet< T > содержащий все константы перечисления рассматриваемого типа перечисления:

1
final Set< DaysOfTheWeek > enumSetAll = EnumSet.allOf( DaysOfTheWeek.class );

Следовательно, noneOf фабрики noneOf создает экземпляр пустого EnumSet< T > для рассматриваемого типа перечисления:

1
final Set< DaysOfTheWeek > enumSetNone = EnumSet.noneOf( DaysOfTheWeek.class );

Также можно указать, какие константы перечисления рассматриваемого типа перечисления следует включить в EnumSet< T > , используя метод фабрики:

1
2
3
4
final Set< DaysOfTheWeek > enumSetSome = EnumSet.of(
    DaysOfTheWeek.SUNDAY,
    DaysOfTheWeek.SATURDAY
);

EnumMap< T, ? > EnumMap< T, ? > очень близок к регулярному отображению с той разницей, что его ключами могут быть константы перечисления рассматриваемого типа перечисления. Например:

1
2
3
final Map< DaysOfTheWeek, String > enumMap = new EnumMap<>( DaysOfTheWeek.class );
enumMap.put( DaysOfTheWeek.MONDAY, "Lundi" );
enumMap.put( DaysOfTheWeek.TUESDAY, "Mardi" );

Обратите внимание, что, как и в большинстве реализаций коллекций, EnumSet< T > и EnumSet< T > EnumMap< T, ? > не являются поточно-ориентированными и не могут использоваться как есть в многопоточной среде (мы обсудим безопасность потоков и синхронизацию в части 9 руководства « Советы и рекомендации по параллелизму» ).

8. Когда использовать перечисления

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

9. Аннотации как специальные интерфейсы

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

Сами по себе аннотации не оказывают прямого влияния на аннотируемый элемент. Однако, в зависимости от аннотаций и того, как они определены, они могут использоваться компилятором Java (замечательным примером этого является аннотация @Override которую мы много видели в третьей части руководства « Как разрабатывать классы и Интерфейсы ), обработчиками аннотаций (более подробную информацию можно найти в разделе « Процессоры аннотаций ») и кодом во время выполнения с использованием рефлексии и других методов самоанализа (подробнее об этом в части 11 учебника, поддержка рефлексии и динамических языков ).

Давайте посмотрим на простейшее объявление аннотации:

1
2
3
4
5
6
7
8
public @interface SimpleAnnotation {
}
 
The @interface keyword introduces new annotation type. That is why annotations could be treated as specialized interfaces. Annotations may declare the attributes with or without default values, for example:
public @interface SimpleAnnotationWithAttributes {
    String name();
    int order() default 0;
}

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

1
@SimpleAnnotationWithAttributes( name = "new annotation" )

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

1
2
3
4
5
6
7
public @interface SimpleAnnotationWithValue {
    String value();
}
 
It could be used like this:
 
@SimpleAnnotationWithValue( "new annotation" )

Есть несколько ограничений, которые в некоторых случаях делают работу с аннотациями не очень удобной. Во-первых, аннотации не поддерживают какое-либо наследование: одна аннотация не может расширять другую аннотацию. Во-вторых, невозможно создать экземпляр аннотации программно с помощью оператора new (мы собираемся взглянуть на некоторые обходные пути к этому в части 11 учебного пособия « Поддержка отражений и динамических языков» ). И в-третьих, аннотации могут объявлять только атрибуты примитивных типов, String или Class< ? > Class< ? > Типы и массивы тех. Никакие методы или конструкторы не могут быть объявлены в аннотациях.

10. Аннотации и политика хранения

Каждая аннотация имеет очень важную характеристику, называемую политикой хранения, которая представляет собой перечисление (типа RetentionPolicy ) с набором политик для сохранения аннотаций. Может быть установлено одно из следующих значений.

политика Описание
CLASS Аннотации должны быть записаны в файле класса компилятором, но не должны сохраняться виртуальной машиной во время выполнения
RUNTIME Аннотации должны быть записаны в файле класса компилятором и сохранены виртуальной машиной во время выполнения, поэтому они могут быть прочитаны рефлексивно.
SOURCE Аннотации должны быть отброшены компилятором.

Таблица 3

Политика хранения оказывает решающее влияние на то, когда аннотация будет доступна для обработки. Политика хранения может быть установлена ​​с @Retention аннотации @Retention . Например:

1
2
3
4
5
6
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
@Retention( RetentionPolicy.RUNTIME )
public @interface AnnotationWithRetention {
}

Установка политики хранения аннотаций в RUNTIME гарантирует ее присутствие в процессе компиляции и в работающем приложении.

11. Аннотации и типы элементов

Другая характеристика, которую должна иметь каждая аннотация, — это типы элементов, к которым она может быть применена. Подобно политике хранения, она определяется как перечисление ( ElementType ) с набором возможных типов элементов.

Тип элемента Описание
ANNOTATION_TYPE Объявление типа аннотации
CONSTRUCTOR Объявление конструктора
FIELD Объявление поля (включает константы перечисления)
LOCAL_VARIABLE Объявление локальной переменной
METHOD Объявление метода
PACKAGE Декларация пакета
PARAMETER Объявление параметров
TYPE Класс, интерфейс (включая тип аннотации) или объявление enum

Таблица 4

В дополнение к описанным выше, Java 8 представляет два новых типа элементов, к которым могут применяться аннотации.

Тип элемента Описание
TYPE_PARAMETER Объявление параметра типа
TYPE_USE Использование типа

Таблица 5

В отличие от политики хранения, аннотация может объявлять несколько типов элементов, с @Target она может быть связана, используя аннотацию @Target . Например:

1
2
3
4
5
6
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
 
@Target( { ElementType.FIELD, ElementType.METHOD } )
public @interface AnnotationWithTarget {
}

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

12. Аннотации и наследование

Важное отношение существует между объявлением аннотаций и наследованием в Java. По умолчанию подклассы не наследуют аннотацию, объявленную в родительском классе. Тем не менее, существует способ распространения определенных аннотаций по всей иерархии классов с @Inherited аннотации @Inherited . Например:

01
02
03
04
05
06
07
08
09
10
11
12
@Target( { ElementType.TYPE } )
@Retention( RetentionPolicy.RUNTIME )
@Inherited
@interface InheritableAnnotation {
}
 
@InheritableAnnotation
public class Parent {
}
 
public class Child extends Parent {
}

В этом примере аннотация @InheritableAnnotation объявленная в классе Parent также будет унаследована классом Child .

13. Повторяющиеся аннотации

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
public @interface RepeatableAnnotations {
    RepeatableAnnotation[] value();
}
 
@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( RepeatableAnnotations.class )
public @interface RepeatableAnnotation {
    String value();
};
@RepeatableAnnotation( "repeatition 1" )
@RepeatableAnnotation( "repeatition 2" )
public void performAction() {
    // Some code here
}

Хотя в Java 8 функция повторяющихся аннотаций требует некоторой работы для того, чтобы ваша аннотация могла повторяться (используя @Repeatable ), конечный результат того стоит: более чистый и компактный аннотированный код.

14. Аннотация процессоров

Компилятор Java поддерживает специальный вид плагинов, называемых процессорами аннотаций (с использованием аргумента командной строки –processor), которые могут обрабатывать аннотации на этапе компиляции. Обработчики аннотаций могут анализировать использование аннотаций (выполнять статический анализ кода), создавать дополнительные исходные файлы или ресурсы Java (которые в свою очередь могут быть скомпилированы и обработаны) или изменять аннотированный код.

Политика хранения (см. « Аннотации и политика хранения» ) играет ключевую роль, сообщая компилятору, какие аннотации должны быть доступны для обработки обработчиками аннотаций.

Процессоры аннотаций широко используются, однако для их написания требуются некоторые знания о том, как работает компилятор Java и сам процесс компиляции.

15. Аннотации и конфигурация над соглашением

Соглашение о конфигурации — это парадигма проектирования программного обеспечения, целью которой является упрощение процесса разработки, когда разработчики следуют набору простых правил (или соглашений). Например, некоторые инфраструктуры MVC (модель-представление-контроллер) следуют соглашению о размещении контроллеров в папке (или пакете) контроллера . Другим примером являются ORM (объектно-реляционные средства отображения), которые часто следуют соглашению, чтобы искать классы в папке (или пакете) модели и получать имя таблицы отношений из соответствующего класса.

С другой стороны, аннотации открывают путь для другой парадигмы дизайна, основанной на явной конфигурации. Учитывая приведенные выше примеры, аннотация @Controller может явно пометить любой класс как контроллер, а @Entity может ссылаться на таблицу реляционной базы данных. Преимущества также вытекают из того факта, что аннотации являются расширяемыми, могут иметь дополнительные атрибуты и ограничены определенными типами элементов. Неправильное использование аннотаций навязывается компилятором Java и выявляет проблемы неправильной конфигурации очень рано (на этапе компиляции).

16. Когда использовать аннотации

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

Интересно, что в сообществе Java предпринимаются постоянные усилия по разработке общих семантических концепций и стандартизации аннотаций по нескольким технологиям Java (для получения дополнительной информации ознакомьтесь со спецификацией JSR-250 ). На данный момент следующие аннотации включены в стандартную библиотеку Java.

аннотирование Описание
@Deprecated Указывает, что отмеченный элемент устарел и больше не должен использоваться. Компилятор генерирует предупреждение всякий раз, когда программа использует метод, класс или поле с этой аннотацией.
@Override Подсказывает компилятору, что элемент предназначен для переопределения элемента, объявленного в суперклассе.
@SuppressWarnings Указывает компилятору подавить определенные предупреждения, которые он может генерировать в противном случае.
@SafeVarargs При применении к методу или конструктору утверждает, что код не выполняет потенциально небезопасные операции с его параметром varargs. Когда используется этот тип аннотации, непроверенные предупреждения, относящиеся к использованию varargs, подавляются (более подробно о varargs будет рассказано в части 6 руководства « Как эффективно писать методы» ).
@Retention Указывает, как отмеченная аннотация сохраняется.
@Target Указывает, к какому типу элементов Java может применяться помеченная аннотация.
@Documented Указывает, что всякий раз, когда указанная аннотация используется, эти элементы должны документироваться с использованием инструмента Javadoc (по умолчанию аннотации не включаются в Javadoc).
@Inherited Указывает, что тип аннотации может быть унаследован от суперкласса (более подробную информацию см. В разделе « Аннотации и наследование »).

Таблица 6

А в версии Java 8 добавлена ​​пара новых аннотаций.

аннотирование Описание
@FunctionalInterface Указывает, что объявление типа предназначено для использования в качестве функционального интерфейса, как определено в Спецификации языка Java (более подробно о функциональных интерфейсах рассказано в части 3 руководства « Как создавать классы и интерфейсы» ).
@Repeatable Указывает, что отмеченная аннотация может быть применена более одного раза к одному и тому же объявлению или типу использования (более подробную информацию см. В разделе « Повторяющиеся аннотации »).

Таблица 7

17. Что дальше

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

18. Загрузите исходный код

Это был урок о том, как проектировать классы и интерфейсы. Вы можете скачать исходный код здесь: advanced-java-part-5