Аннотации в Java, часть I

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

    Я занимаюсь онлайн обучением Java и опубликую часть учебных материалов в рамках переработки курса Java Core.

    Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).

    Мой метод обучения состоит в том, что я
    1. строю усложняющуюся последовательность примеров
    2. объясняю возможные варианты применения
    3. объясняю логику двигавшую авторами (по мере возможности)
    4. даю большое количество тестов (50-100) всесторонне проверяющее понимание и демонстрирующих различные комбинации
    5. даю лабораторные для самостоятельной работы

    Данная статье следует пунктам #1 (последовательность примеров) и #2(варианты применения).

    Поехали!


    Учебный пример: снабдить классы пользователя мета-информацией о «версии класса».

    Итерация #1:
    Просто ставим @ перед interface.
    public @interface Version {}
    


    Итерация #2:
    У аннотаций могут быть атрибуты.
    public @interface Version {
        public int version();
    }
    

    Заполнятся при использовании
    @Version(version = 42)
    public class MyClass {}
    

    Аннотация выше полностью эквивалентна следующей (без public). В этом аннотации схожи с интерфейсам: отсутствие модификатора области видимости автоматически означает public (а не package private как у классов).
    public @interface Version {
        int version();
    }
    

    С protected и private — не компилируется
    public @interface Version {
        protected int version();
    }
    >> COMPILATION ERROR: Modifier 'protected' not allowed here
    

    Далее я буду использовать вариант без модификатора public

    Итерация #3:
    Если объявить атрибут с именем value, то его можно опускать при использовании
    public @interface Version {
        public int value();
    }
    

    @Version(42)
    public class MyClass {}
    

    Хотя можно и по старинке
    @Version(value = 42)
    public class MyClass {}
    


    Итерация #4:
    Для атрибута можно объявить значения по умолчанию
    public @interface Version {
        int value();
        String author() default "UNKNOWN";
    }
    

    Теперь у нас два варианта использования. Так
    @Version(42)
    public class MyClass {}
    

    Или вот так
    @Version(value = 42, author = "Jim Smith")
    public class MyClass {
    }
    

    Но не вот так (слушай, обидно, да)
    @Version(42, author = "Jim Smith")
    public class MyClass {}
    >> COMPILATION ERROR: Annotation attribute must be of the form 'name=value'
    


    Итерация #5:
    Атрибуты могут иметь тип массива
    public @interface Author {
        String[] value() default {};
    }
    

    @Author({"Anna", "Mike", "Sara"})
    public class MyClass {}
    

    Но только одномерного
    public @interface Author2D {
        String[][] value() default {};
    }
    >> COMPILATION ERROR: Invalid type of annotation member
    


    Итерация #6:
    Возможен забавный трюк: аннотация — атрибут аннотации
    public @interface Version {
        int value();
        String author() default "UNKNOWN";
    }
    

    public @interface History {
        Version[] value() default {};
    }
    

    Применяется вот так
    @History({
            @Version(1),
            @Version(value = 2, author = "Jim Smith")
    })
    public class MyClass {}
    


    У аннотаций много ограничений. Перечислим некоторые из них.

    Ограничение: тип атрибута


    1. Атрибуты могут иметь только следующие типы
    • примитивы
    • String
    • Class или «any parameterized invocation of Class»
    • enum
    • annotation
    • массив элементов любого из вышеперечисленных типов

    Последний пункт надо понимать как то, что допустимы только одномерные массивы.

    Ну что же, давайте действовать в рамках ограничений

    Итерация #7:
    В качестве типа атрибута нельзя использовать «обычные» классы Java (за исключением java.lang.String и java.lang.Class), скажем java.util.Date
    import java.util.Date;
    
    public @interface Version {
        Date date();
    }
    >> COMPILATION ERROR: Invalid type for annotation member
    

    Но можно эмулировать записи/структуры на аннотациях
    public @interface Date {
        int day();
        int month();
        int year();
    }
    

    public @interface Version {
        Date date();
    }
    

    @Date(year = 2001, month = 1, day = 1)
    public class MyClass {}
    


    Итерация #8:
    Атрибутом аннотации может быть enum. Из приятного, его можно объявить в объявлении аннотации (как и в объявлении интерфейса тут может быть объявление enum, class, interface, annotation)
    public @interface Colored {
        public enum Color {RED, GREEN, BLUE}
        Color value();
    }
    

    import static net.golovach.Colored.Color.RED;
    
    @Colored(RED)
    public class MyClass {}
    


    Итерация #9:
    Атрибутом аннотации может быть классовый литерал.
    Аннотация версии включает ссылку на предыдущую версию класса.
    public @interface Version {
        int value();
        Class<?> previous() default Void.class;
    }
    

    Первая версия класса
    @Version(1)
    public class ClassVer1 {}
    

    Вторая версия со ссылкой на первую
    @Version(value = 2, previous = ClassVer1.class)
    public class ClassVer2 {}
    

    // Да, я знаю, что нормальные люди не включают версию класса в имя класса. Но знаете как нудно придумывать примеры согласованные с реальной практикой?

    Итерация #10:
    Менее тривиальный пример с классовым литералом, где я не удержался и добавил generic-ов.
    Интерфейс «сериализатора» — того, кто может записать экземпляр T в байтовый поток вывода
    import java.io.IOException;
    import java.io.OutputStream;
    
    public interface Serializer<T> {
        void toStream(T obj, OutputStream out) throws IOException;
    }
    

    Конкретный «сериализатор» для класса MyClass
    import java.io.IOException;
    import java.io.OutputStream;
    
    public class MySerializer implements Serializer<MyClass> {
        @Override
        public void toStream(MyClass obj, OutputStream out) throws IOException {
            throw new UnsupportedOperationException();
        }
    }
    

    Аннотация, при помощи которой мы «приклеиваем сериализатор» к конкретному классу
    public @interface SerializedBy {
        Class<? extends Serializer> value();
    }
    

    Ну и сам класс MyClass отмеченный, как сериализуемый «своим сериализатором» MySerializer
    @SerializedBy(MySerializer.class)
    public class MyClass {}
    


    Итерация #11:
    Сложный пример
    public enum JobTitle {
        JUNIOR, MIDDLE, SENIOR, LEAD,
        UNKNOWN
    }
    

    public @interface Author {
        String value();
        JobTitle title() default JobTitle.UNKNOWN;
    }
    

    public @interface Date {
        int day();
        int month();
        int year();
    }
    

    public @interface Version {
        int version();
        Date date();
        Author[] authors() default {};
        Class<?> previous() default Void.class;
    }
    

    Ну и наконец использование аннотации
    import static net.golovach.JobTitle.*;
    
    @History({
            @Version(
                    version = 1,
                    date = @Date(year = 2001, month = 1, day = 1)),
            @Version(
                    version = 2,
                    date = @Date(year = 2002, month = 2, day = 2),
                    authors = {@Author(value = "Jim Smith", title = JUNIOR)},
                    previous = MyClassVer1.class),
            @Version(
                    version = 3,
                    date = @Date(year = 2003, month = 3, day = 3),
                    authors = {
                            @Author(value = "Jim Smith", title = MIDDLE),
                            @Author(value = "Anna Lea")},
                    previous = MyClassVer2.class)
    })
    public class MyClassVer3 {}
    


    Ограничение: значения атрибутов — константы времени компиляции/загрузки JVM


    Должна быть возможность вычислить значения атрибутов аннотаций в момент компиляции или загрузки класса в JVM.
    public @interface SomeAnnotation {
        int count();
        String name();
    }
    

    Пример
    @SomeAnnotation(
            count = 1 + 2,
            name = MyClass.STR + "Hello"
    )
    public class MyClass {
        public static final String STR = "ABC";
    }
    

    Еще пример
    @SomeAnnotation(
            count = (int) Math.PI,
            name = "" + Math.PI
    )
    public class MyClass {}
    

    А вот вызовы методов — это уже runtime, это уже запрещено
    @SomeAnnotation(
            count = (int) Math.sin(1),
            name = "Hello!".toUpperCase()
    )
    public class MyClass {}
    >> COMPILATION ERROR: Attribute value must be constant
    


    Заключение


    Это первая часть статьи про аннотации в Java. Во второй части мы рассмотрим следующие темы:
    • Аннотации, модифицирующие поведение других аннотаций: @ Target, @ Retention, @ Documented, @ Inherited
    • Аннотации, модифицирующие поведение компилятора и JVM: @ Deprecated, @ Override, @ SafeVarargs, @ SuppressWarnings
    • Чтение аннотаций с помощью Reflection API


    Контакты



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

    skype: GolovachCourses
    email: GolovachCourses@gmail.com
    GolovachCourses
    35,00
    Компания
    Поделиться публикацией

    Похожие публикации

    Комментарии 21

      0
      Интересно, хоть я и не junior.
      Не хотите скооперироваться с Hexlet?
        +1
        Я детально не разбирался, но Hexlet — это бесплатное образование?
        Мне так показалось.
          0
          По большей части да, но у Рахима были планы и о платных курсах.
            0
            При всем моем уважении к авторам бесплатных курсов, я считаю, что преподавание (подбор материала, чтение лекций, написание статей, составление тестов, ...) должно оплачиваться.
            Меценатство — это личный выбор. Сейчас для меня обучение — главный источник дохода.

            С удовольствием скооперировался бы с теми, кто строит бизнес.
        0
        У аннотаций могут быть атрибуты.

        public @interface Version { 
            public int version(); 
        }
        


        Тут следует отметить, что это не совсем обычные поля, а методы типа version() — не совсем обычные геттеры, и в определенных условиях могут очень больно бить по производительности.
          0
          В Андроиде наверное всё может бить по производительности. :) Нормального JIT-то нету.
            0
            Ну скоро ART будет.
          +5
          Это конечно хорошо, но для тех, кто приступает к изучению языка было бы хорошо дать описание аннотациям, а не сразу с места в карьер :)
            –1
            Я старался выстроить цепочку примеров.
            Введение есть к любом учебнике, в том же Хорстмане — целая глава, кажется.
            +3
            Ну хотя бы пару строк про общее понимание аннотаций, введения небольшого не хватает.
              +1
              Вторая часть будет посвящено рассказу о том, для чего аннотации служат:
              1. Влияем на поведение компилятора/JVM (@ Override, @SafeVarargs, ...).
              2. Добавляем метаинформацию к классам. Можем считать ее при помощи Reflection API.
              Я решил ограничиться разумным объемом, что бы пост было реально написать и вычитать за раз.
              +2
              Вы выбрали хороший метод для преподнесения информации новичкам, показывая ошибки и ограничения на примере, да еще и с результатом компиляции. Такая подача материала хорошо запоминается и легко читается.
              Будет еще здорово, если каждое утверждение будет подкрепляться ссылкой на документацию, ибо «хуниоров нужно приучить MAN-ить с детства».
                0
                Согласен.
                Но тут методическая проблема:
                1. Это делает написание статьи в 2 раза более длительным по времени. Многие вещи просто «размазаны» по java lang spec. И их поиск и собирание по абзацу — длительная процедура.
                2. Спецификации, по моему опыту, склонны читать только около 10%-20% обучаемых.

                Если делать серьезный ресурс, то это абсолютно правильное решение. Но непонятна монетизация такого ресурса. Пример: Эккель, Хорстман, Гослинг и прочие Шилдты и то в своих учебниках так не делают.
                +1
                Возможен забавный трюк: аннотация — атрибут аннотации

                И этот аттрибут может иметь значение по умолчанию. Сэкономлю вам полчаса поисков (спасибо автору блога Todd Fast), для аттрибута-аннотации дефолтное значение выставляется так:

                @Target(ElementType.METHOD)
                public @interface ReallyComplexAnnotation {
                    public SimpleAnnotation value() default @SimpleAnnotation(a="..."); // right!
                )
                


                  0
                  О!
                  Спасибо.

                  Еще есть интересный вопрос: аннотации не поддерживают рекурсивные структуры данных, т.е. сделать что-то типа односвязного списка или дерева «в лоб» не получится:
                  @Node {
                      @Node next();
                  }
                  

                  @TreeNode {
                      @TreeNode left();
                      @TreeNode right();
                  }
                  


                  Вопрос — как красивее организовать такое?
                  Скажем генеалогическое древо на классе Person?
                    +1
                    Не очень понятно, почему генеалогическое древо является мета-информацией класса персон. И как следствие возникает подозрение, что красивее — использовать не аннотации.
                  +1
                  Не хватает примера, хотя бы минимального, с использование аннотаций (не как ими обозначить класс), а как их вытягивать из класса
                    0
                    В конце статьи же сказано, что в след. статье будет рассмотрено:
                    Чтение аннотаций с помощью Reflection API
                    0
                    А продолжение будет?
                      +1
                      Присоеденяюсь к критике: прочел статью, вроде понял технически, но не понял суть. Наверное все же стоит сначала описать проблему… и только потом давать кучу примеров решения.
                        0
                        Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).

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

                        Самое читаемое