Adnotacje w Javie

Zgodnie z zapowiedzią czytasz krótki artykuł na temat adnotacji w Javie. Dokumentacja Javy mówi tyle:

Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

A w moim autorskim tłumaczeniu na polski:

Adnotacje są rodzajem metainformacji (metadanych). Zawierają informacje o programie, ale nie są częścią programu jako takiego (chociaż są częścią kodu źródłowego). Adnotacje nie mają bezpośredniego wpływu na działanie kodu, w którym się znajdują.

Aż się przez chwilę zastanowiłem, jak wytłumaczyć słowo metadane. Nie pamiętam już, gdzie słyszałem (czy też czytałem) wytłumaczenie, czym są metadane na podstawie służb bezpieczeństwa. Z reguły (mowa o czasach przed internetowych) służby bezpieczeństwa były w stanie dowiedzieć się kto z kim się spotyka, jak długo spotkanie trwa, co osoby robią w czasie spotkania – ale ciężko się dowiedzieć najważniejszej rzeczy – o czym osoby rozmawiały. Treść rozmowy jest więc konkretną daną, a to co udało się ustalić – metadanymi. Opisują w jakiś sposób otoczkę danych. Podobnie mówimy, że z plikiem graficznym związane są metadane – na przykład autor zdjęcia, identyfikator aparatu, dane o ekspozycji itd. Więc są to informacje dotyczące zdjęcia, ale często bez zdjęcia niekoniecznie mają one znaczenie. Nie powinno się tłumaczyć na przykładach, więc sięgnijmy do słownika języka polskiego:

w informatyce: dane o danych, informacje o strukturze, miejscu przechowywania i innych cechach zbiorów danych

Jeżeli chodzi o adnotacje w Javie, służą one do:

  • przekazywania danych kompilatorowi
  • zapisania dodatkowych informacji, które są wykorzystywane przez wirtualną maszynę Javy w czasie przetwarzania kodu binarnego lub w czasie działania programu

Adnotacje wbudowane w język

Cała zaleta adnotacji leży w tym, że każdy może je zdefiniować i używać. Ale jest kilka adnotacji, które są częścią języka Java. Jeżeli widziałeś wcześniej jakikolwiek kod Javy najprawdopodobniej widziałeś jedną z nich.

@Override

Mowa o adnotacji @Override. Jest to dodatkowa informacja, którą możemy dodać do metody i która zostanie wykorzystana przez kompilator. Jeżeli na metodzie jest położona adnotacja @Override, to kompilator zweryfikuje, czy ta metoda nadpisuje jakąś metodę z klasy nadrzędnej. Jeżeli nie znajdzie oczekiwanej metody w klasie nadrzędnej, to zgłosi błąd kompilacji. Używanie tej adnotacji nie jest obowiązkowe, ale pozwala uniknąć wielu potencjalnych błędów uruchomienia programu.

Rozważmy prosty przykład. Mamy trzy klasy:

  • Nadklasę:
    public class SuperClass {
        public void f(int a) {
            System.out.println("Jestes w klasie nadrzednej: " + a);
        }
    }
  • Podklasę:
    public class SubClass extends SuperClass {
        public void f(long a) {
            System.out.println("Jestes w klasie podrzednej: " + a);
        }
    }
  • Klasę główną:
    public class Main {
        public static void main(String [] args) {
            SubClass a = new SubClass();
            a.f(1);
        }
    }

Te trzy klasy skompilują się bez najmniejszego problemu:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/00-override (master)
$ ls
Main.java  SubClass.java  SuperClass.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/00-override (master)
$ javac *.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/00-override (master)
$ ls
Main.class  Main.java  SubClass.class  SubClass.java  SuperClass.class  SuperClass.java

Ale uruchomienie klasy Main będzie inne od spodziewanego. Przynajmniej ja myślałem, że metoda f z klasy podrzędnej nadpisze metodę z klasy nadrzędnej, ale okazało się, że nie:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/00-override (master)
$ java Main
Jestes w klasie nadrzednej: 1

Skoro chcemy, żeby jakaś metoda nadpisywała metodę z klasy nadrzędnej, wystarczy dodać tę adnotację:

public class SubClass extends SuperClass {
    @Override
    public void f(long a) {
        System.out.println("Jestes w klasie podrzednej: " + a);
    }
}

I teraz już w momencie kompilacji dostaniemy błąd:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/01-override (master)
$ javac *.java
SubClass.java:3: error: method does not override or implement a method from a supertype
        @Override
        ^
1 error

Możesz mi wierzyć, lub nie, ale ta adnotacja może zaoszczędzić Ci sporo czasu. Jak widać, adnotację @Override możemy położyć tylko na metodę. Położenie na innym elemencie spowoduje błąd kompilacji. Dlaczego o tym wspominam? Bo za chwilę się przekonasz, że są adnotację, którą można położyć na innych elementach, z których jest zbudowany program.

@Deprecated

Taka adnotacją jest wbudowana w język @Deprecated. Tę adnotację możemy położyć na:

  • konstruktorze
  • polu
  • zmiennej lokalnej
  • metodzie
  • pakiecie
  • module
  • argumencie funkcji
  • definicji typu (czyli klasie, interfejsie itp)

Oznacza ona mniej więcej tyle, że jakaś część naszego kodu została uznana za przestarzałą. Użycie w kodzie elementu przestarzałego spowoduje zgłoszenie ostrzeżenia w czasie kompilacji. Jest to niezbędne, jeżeli chcemy usunąć z rozwijanej przez nas biblioteki jakiś element. Takie brutalne, natychmiastowe usunięcie elementu (np. klasy) może spowodować bolesne problemy u użytkowników naszej biblioteki. Oznaczenie klasy jako przestarzałej pozwoli naszym odbiorcom przerobienie programu tak, aby usunięcie elementu nie spowodowało problemów. Więc znów, nie jest to nic obowiązkowego, ale jest użyteczne dla ludzi, z którymi współpracujemy. Zobaczmy jak to będzie wyglądało w praktyce. Mamy klasę, którą uznajemy za przestarzałą:

@Deprecated
public class Library {
    public double areaOfCircle(double radius) {
        return Math.PI * radius * radius;
    }
}

I mamy klasę główną, która z niej korzysta:

public class Main {
    public static void main(String [] args) {
        Library l = new Library();
        System.out.println(l.areaOfCircle(3.0));
    }
}

Jeżeli teraz skompilujemy nasz mały program to dostaniemy ostrzeżenie, kompilacja się powiedzie i będziemy mogli uruchomić program:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/02-deprecated (master)
$ ls
Library.java  Main.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/02-deprecated (master)
$ javac *.java
Note: Main.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/02-deprecated (master)
$ java Main
28.274333882308138

Posłuchajmy sugestii kompilatora i skompilujmy przy użyciu opcji -Xlint:deprecation:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/02-deprecated (master)
$ javac -Xlint:deprecation *.java
Main.java:3: warning: [deprecation] Library in unnamed package has been deprecated
                Library l = new Library();
                ^
Main.java:3: warning: [deprecation] Library in unnamed package has been deprecated
                Library l = new Library();
                                ^
2 warnings

Od wersji 9, adnotacja może zostać uzupełniona o dwa atrybuty – chętnie się dowiem, jak w polskiej literaturze określa się te cosie. Czyli oprócz samej wiadomości o tym, że klasa jest przestarzała możemy dodać informację:

  • o tym, od której wersji biblioteki dany element jest przestarzały – domyślna wartość do pusty napis ""
  • o tym, czy w najbliższym releasie element zostanie usunięty – domyślna wartość to false

Uzupełnienie takiej adnotacji o dodatkowe dane jest bardzo proste. Polega na tym, że w nawiasach okrągłych wpisujemy nazwę atrybutu i po znaku równa się jego wartość. Atrybuty od siebie oddzielamy przecinkiem.

@Deprecated(since = "1.0", forRemoval = true)
public class Library {
    public double areaOfCircle(double radius) {
        return Math.PI * radius * radius;
    }
}

Po takiej zmianie komunikat generowany w czasie kompilacji wygląda trochę inaczej:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/03-deprecated (master)
$ javac *.java
Main.java:3: warning: [removal] Library in unnamed package has been deprecated and marked for removal
                Library l = new Library();
                ^
Main.java:3: warning: [removal] Library in unnamed package has been deprecated and marked for removal
                Library l = new Library();
                                ^
2 warnings

@SuppressWarnings

Kolejna adnotacja jest trochę odpowiedzią na poprzednią (ale nie tylko). Służy do tuszowania ostrzeżeń (warningów). To znaczy, że możemy ją położyć na przykład na metodzie, w której występuje ostrzeżenie, i ta adnotacja powie kompilatorowi, żeby się nie odzywał. Żeby nie było zbyt wygodnie, musimy podać nazwę ostrzeżenia, które chcemy zignorować. Przyznam się z przykrością, że kod, który mi działał w poprzedniej wersji Javy, w aktualnej mi nie działa. Więc nie mogę pokazać, że położenie na metodzie main adnotacji @SuppressWarnings("deprecation") powoduje zignorowanie ostrzeżenia. Mogę Ci pokazać tuszowanie innego ostrzeżenia. Weźmy prostą klasę:

public class Main {
    List list = new ArrayList();
    
    public void add(int element) {
        list.add(element);
    }
}

Jej kompilacja się powiedzie, ale zostanie zgłoszone ostrzeżenie:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/04-suppress-warnings (master)
$ javac Main.java
Note: Main.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Ozywiście, z premedytacją możemy zatuszować ten warning:

public class Main {
    List list = new ArrayList();
    
    @SuppressWarnings("unchecked")
    public void add(int element) {
        list.add(element);
    }
}

Widać, że to pomogło:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/05-suppress-warnings (master)
$ javac Main.java

Nie możesz przesadzić z używaniem tej adnotacji. Każde ostrzeżenie może oznaczać jakieś potencjalne niebezpieczeństwo.

@FunctionalInterface

Jest dość nowym elementem na tej liście. Został wprowadzony w Javie 8. Służy do oznaczania interfejsów funkcyjnych – same interfejsy funkcyjne są niezwykle interesujące i jeszcze kiedyś wrócę do tego tematu. Interfejs jest to taka specjalna klasa, w której definiujemy sygnatury metod. Jeżeli jakaś klasa implementuje interfejs, to musi te metody zdefiniować. Metody bez ciała nazywamy metodami abstrakcyjnymi. A interfejs funkcyjny to taki interfejs, który ma tylko jedną metodę abstrakcyjną. To tak w dużym skrócie.

Zobaczmy to na prostym przykładzie:

@FunctionalInterface
public interface Function {
    double val(double x);
}

Kompilacja tej klasy (a właściwie interfejsu) się powiedzie. Z kolei kompilacja takiego interfejsu:

@FunctionalInterface
public interface Function {
    double val(double x);
    double f(double x);
}

zakończy się błędem:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/07-functional-interface (master)
$ javac Function.java
Function.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Function is not a functional interface
    multiple non-overriding abstract methods found in interface Function
1 error

@SafeVarargs

Jest jeszcze jedna wbudowana adnotacja. Wykorzystuje się ją do wytłumienia ostrzeżeń związanych z metodami o zmiennej liczbie argumentów. Sam bardzo rzadko piszę takie funkcje, więc nigdy nie miałem okazji użyć tej adnotacji. Nie będę się więc wymądrzał i zainteresowanych wyślę do bloga baeldung. Ogólnie polecam tę stronę tak po prostu do poczytania.

Definiowanie własnych adnotacji

Jak już wspomniałem, definiowanie swoich własnych adnotacji daje niesamowite możliwości. Dziś pokażę jedną z takich możliwości. Zacznijmy od zdefiniowania naszej pierwszej adnotacji:

@interface SimpleAnnotation {}

Widzimy, że adnotacja jest takim prawie interfejsem (małpka na początku słowa kluczowego interface). Możemy ją teraz położyć na klasie, polu i metodzie:

@SimpleAnnotation
public class MyClass {
    
    @SimpleAnnotation
    public int field;
    
    @SimpleAnnotation
    public void myMethod() {
        System.out.println("FOO");
    }
}

Atrybuty adnotacji

Tak jak widzieliśmy na poprzednich przykładach, dla adnotacji możemy zdefiniować atrybuty. Atrybut w adnotacji będzie po prostu metodą bez argumentów. Nazwa metody to nazwa atrybutu, a typ zwracany przez metodę to typ atrybutu. Typy atrybutów są ograniczone przez typy prymitywne, klasę String, klasę Class<T>, dowolnego enumeratora, inną adnotację oraz tablice poprzednich. Więc możemy dodać atrybut do naszej adnotacji:

@interface SimpleAnnotation {
    String attribute();
}

Kompilacja razem z następującą klasą Main:

@SimpleAnnotation
public class MyClass {
    
    @SimpleAnnotation
    public int field;
    
    @SimpleAnnotation
    public void myMethod() {
        System.out.println("FOO");
    }
}

zakończy się błędem (a nawet trzema):

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/09-own-annotation (master)
$ javac *.java
MyClass.java:1: error: annotation @SimpleAnnotation is missing a default value for the element 'attribute'
@SimpleAnnotation
^
MyClass.java:4: error: annotation @SimpleAnnotation is missing a default value for the element 'attribute'
        @SimpleAnnotation
        ^
MyClass.java:7: error: annotation @SimpleAnnotation is missing a default value for the element 'attribute'
        @SimpleAnnotation
        ^
3 errors

Błędów możemy się pozbyć na dwa sposoby. Pierwszy z nich, to ustawienie wartości przy wykorzystaniu adnotacji. Czyli poprawienie klasy Main:

@SimpleAnnotation(attribute = "klasa")
public class MyClass {
    
    @SimpleAnnotation(attribute = "pole")
    public int field;
    
    @SimpleAnnotation(attribute = "metoda")
    public void myMethod() {
        System.out.println("FOO");
    }
}

Albo, tak jak sugeruje komunikat błędu, zdefiniowanie wartości domyślnej dla atrybutu:

@interface SimpleAnnotation {
    String attribute() default "";
}

Wtedy możemy nie definiować tej wartości przy użyciu adnotacji, ale wciąż możemy ją ustawić:

@SimpleAnnotation
public class MyClass {
    
    @SimpleAnnotation(attribute = "pole")
    public int field;
    
    @SimpleAnnotation
    public void myMethod() {
        System.out.println("FOO");
    }
}

Atrybut domyślny

W poprzednich przykładach widziałeś użycie adnotacji bez definiowania jaki atrybut ustawiasz:

@SimpleAnnotation("pole")

W tym momencie, kompilacja takiej klasy:

@SimpleAnnotation
public class MyClass {
    
    @SimpleAnnotation("pole")
    public int field;
    
    @SimpleAnnotation
    public void myMethod() {
        System.out.println("FOO");
    }
}

się nie powiedzie:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/12-own-annotation (master)
$ javac *.java
MyClass.java:4: error: cannot find symbol
        @SimpleAnnotation("pole")
                          ^
  symbol:   method value()
  location: @interface SimpleAnnotation
1 error

Komunikat błędu sugeruje rozwiązanie. Gdy podajemy wartość bez nazwy atrybutu, to tak naprawdę ustawiamy wartość atrybutu o nazwie value. Więc, gdy zaktualizujemy adnotację:

@interface SimpleAnnotation {
    String value() default "";
}

kompilacja klasy Main się powiedzie. Wciąż możemy nazwać atrybut, który ustawiamy, ale w przypadku atrybutu value, nie musimy:

@SimpleAnnotation
public class MyClass {
    
    @SimpleAnnotation("pole")
    public int field;
    
    @SimpleAnnotation(value = "metoda")
    public void myMethod() {
        System.out.println("FOO");
    }
}

Atrybut jako tablica

Jak już wspomniałem, atrybut może być tablicą. Czyli mogę zdefiniować adnotację:

@interface SimpleAnnotation {
    String[] value();
}

z którą będzie działała następująca klasa:

@SimpleAnnotation("klasa")
public class MyClass {
    
    @SimpleAnnotation("pole")
    public int field;
    
    @SimpleAnnotation(value = "metoda")
    public void myMethod() {
        System.out.println("FOO");
    }
}

Ale jakto? Przecież zdefiniowaliśmy atrybut jako listę. Java jest sprytna i traktuje pojedynczy element jak jednoelementową tablicę. Jeżeli chcielibyśmy przekazać rzeczywiście tablicę to możemy użyć następującej składni:

@SimpleAnnotation("klasa")
public class MyClass {
    
    @SimpleAnnotation("pole")
    public int field;
    
    @SimpleAnnotation(value = {"metoda", "bez", "argumentu"})
    public void myMethod() {
        System.out.println("FOO");
    }
}

I teraz wiemy, jak może wyglądać wartość domyślna, takiego atrybutu. Poniżej widzisz, w jaki sposób ustawiamy, że atrybut domyślnie jest pustą tablicą.

@interface SimpleAnnotation {
    String[] value() default {};
}

Dygresja o refleksji

Tytuł sekcji zabrzmiał dość filozoficznie. Nie zniechęcaj się. W Javie, w czasie uruchomienia programu, możesz wiele się dowiedzieć na temat obiektów, które występują w Twojej aplikacji. Na przykład możesz poznać wszystkie pola (z prywatnymi włącznie), ich typy i ich wartości. Wynika to z tego, w klasie Object jest zdefiniowana metoda getClass() (dodatkowo każda klasa ma statyczne pole o nazwie class), która zwraca obiekt klasy Class. Klasa ta pozwala poznać dokładną budowę klasy. Pokażę Ci to na przykładzie. W tym momencie adnotacja nie jest istotna, ale zamieszczę ją tutaj dla porządku:

@interface SimpleAnnotation {
    String value() default "";
    String first() default "";
    int second() default 0;
}

Tę adnotację kładziemy na elementach (i samej klasie) klasy:

@SimpleAnnotation
public class MyClass {
    
    // @SimpleAnnotation("pole", first = "Pawel") - compilation error
    @SimpleAnnotation(value = "pole", first = "Pawel")
    private int field;
    
    @SimpleAnnotation(second = 7)
    public void myMethod() {
        System.out.println("FOO");
    }
}

I teraz spróbujemy poznać sekrety tej klasy:

public class Main {
    public static void main(String [] args) {
        Class<?> clazz = MyClass.class;
        // or:
        // MyClass object = new MyClass();
        // Class<?> clazz = object.getClass();
        
        System.out.println("Nazwa klasy: " + clazz.getName());
        Field[] fields = clazz.getFields();
        
        for (Field f: fields) {
            System.out.println("Pole:");
            System.out.println("    nazwa: " + f.getName());
            System.out.println("    typ: " + f.getType());
        }
        
    }
}

Po kompilacji możemy program uruchomić, ale jego wynik może rozczarować:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/16-annotation-usage (master)
$ javac *.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/16-annotation-usage (master)
$ java Main
Nazwa klasy: MyClass

Ale wystarczy zamienić linijkę

Field[] fields = clazz.getFields();

na

Field[] fields = clazz.getDeclaredFields();

I działanie programu staje się dużo bardziej interesujące:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/16-annotation-usage (master)
$ java Main
Nazwa klasy: MyClass
Pole:
    nazwa: field
    typ: int

Za pomocą podobnego mechanizmu możemy uzyskać informację o adnotacjach. Spróbujmy:

public class Main {
    public static void main(String [] args) {
        Class<?> clazz = MyClass.class;
        // or:
        // MyClass object = new MyClass();
        // Class<?> clazz = object.getClass();
        
        System.out.println("Nazwa klasy: " + clazz.getName());
        Field[] fields = clazz.getDeclaredFields();
        
        for (Field f: fields) {
            System.out.println("Pole:");
            System.out.println("    nazwa: " + f.getName());
            System.out.println("    typ: " + f.getType());
            
            Annotation[] annotations = f.getAnnotations();
            for (Annotation a: annotations) {
                System.out.println("    adnotacja: " + a.annotationType().getName());
            }
        }
        
    }
}

Niestety program niczego nie pokaże… Ale to jest zgodne z oczekiwaniami. Dlaczego?

Zakres widoczności adnotacji

Każda adnotacja ma ustawiony pewien zakres widoczności. Są trzy zakresy zdefiniowane w enumie:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

Tak więc domyślnie adnotacja jest widoczna w pliku z rozszerzeniem class (wyniku kompilacji), ale nie będzie widoczna w trakcie uruchomienia aplikacji. Zakres ten możemy zwiększyć – czyli ustawić ją widoczną w czasie działania aplikacji. Możemy tez zakres zmniejszyć – czyli ustawić adnotację widoczną tylko dla kompilatora.

Sprawdziliśmy, że domyślnie adnotacja nie jest widoczna w czasie uruchomienia programu. Możemy teraz sprawdzić, czy adnotacja jest widoczna w pliku z rozszerzeniem class. Możemy zaryzykować i wyświetlić zawartość pliku class jako pliku tekstowego. Ale bardziej elegancko będzie, gdy wykorzystamy narzędzie javap (dissasembler dostarczany razem z JDK – to taki program, który pozwala zajrzeć w sensowny sposób do wnętrza pliku class):

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/16-annotation-usage (master)
$ javap -v -p MyClass.class
Classfile /C:/Users/pawel/code/java-annotations-basics/16-annotation-usage/MyClass.class
  Last modified 29 pač 2020; size 557 bytes
  SHA-256 checksum e2dffdad39ee58566a2a6b41a8bfa958fb0ca14d29385ebec028e89f711b0490
  Compiled from "MyClass.java"
public class MyClass
  minor version: 0
  major version: 59
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #21                         // MyClass
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 2
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Class              #10            // java/lang/System
   #9 = NameAndType        #11:#12        // out:Ljava/io/PrintStream;
  #10 = Utf8               java/lang/System
  #11 = Utf8               out
  #12 = Utf8               Ljava/io/PrintStream;
  #13 = String             #14            // FOO
  #14 = Utf8               FOO
  #15 = Methodref          #16.#17        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #16 = Class              #18            // java/io/PrintStream
  #17 = NameAndType        #19:#20        // println:(Ljava/lang/String;)V
  #18 = Utf8               java/io/PrintStream
  #19 = Utf8               println
  #20 = Utf8               (Ljava/lang/String;)V
  #21 = Class              #22            // MyClass
  #22 = Utf8               MyClass
  #23 = Utf8               field
  #24 = Utf8               I
  #25 = Utf8               RuntimeInvisibleAnnotations
  #26 = Utf8               LSimpleAnnotation;
  #27 = Utf8               value
  #28 = Utf8               pole
  #29 = Utf8               first
  #30 = Utf8               Pawel
  #31 = Utf8               Code
  #32 = Utf8               LineNumberTable
  #33 = Utf8               myMethod
  #34 = Utf8               second
  #35 = Integer            7
  #36 = Utf8               SourceFile
  #37 = Utf8               MyClass.java
{
  private int field;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE
    RuntimeInvisibleAnnotations:
      0: #26(#27=s#28,#29=s#30)
        SimpleAnnotation(
          value="pole"
          first="Pawel"
        )

  public MyClass();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0

  public void myMethod();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String FOO
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
    RuntimeInvisibleAnnotations:
      0: #26(#34=I#35)
        SimpleAnnotation(
          second=7
        )
}
SourceFile: "MyClass.java"
RuntimeInvisibleAnnotations:
  0: #26()
    SimpleAnnotation

gdy zagłębisz się w wyjście z programu javap, to powinieneś zobaczyć, że w trzech miejscach pojawia się informacja o RuntimeInvisibleAnnotations.

Zmiana zakresu widoczności

Jak zmienić zakres widoczności? Wystarczy położyć adnotację @Retention na naszej adnotacji – taka trochę incepcja.

@Retention(RetentionPolicy.SOURCE)
@interface SimpleAnnotation {
    String value() default "";
    String first() default "";
    int second() default 0;
}

Gdy resztę plików zostawimy bez zmian i ponownie zdesemblujemy klasę MyClass, to dostaniemy:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/17-annotation-retention (master)
$ javap -v -p MyClass.class
Classfile /C:/Users/pawel/code/java-annotations-basics/17-annotation-retention/MyClass.class
  Last modified 29 pač 2020; size 410 bytes
  SHA-256 checksum 9ba49d23161b3d72b31128cdbe8b065e729a9853685371ec91871d4f9fc03cf8
  Compiled from "MyClass.java"
public class MyClass
  minor version: 0
  major version: 59
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #21                         // MyClass
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Class              #10            // java/lang/System
   #9 = NameAndType        #11:#12        // out:Ljava/io/PrintStream;
  #10 = Utf8               java/lang/System
  #11 = Utf8               out
  #12 = Utf8               Ljava/io/PrintStream;
  #13 = String             #14            // FOO
  #14 = Utf8               FOO
  #15 = Methodref          #16.#17        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #16 = Class              #18            // java/io/PrintStream
  #17 = NameAndType        #19:#20        // println:(Ljava/lang/String;)V
  #18 = Utf8               java/io/PrintStream
  #19 = Utf8               println
  #20 = Utf8               (Ljava/lang/String;)V
  #21 = Class              #22            // MyClass
  #22 = Utf8               MyClass
  #23 = Utf8               field
  #24 = Utf8               I
  #25 = Utf8               Code
  #26 = Utf8               LineNumberTable
  #27 = Utf8               myMethod
  #28 = Utf8               SourceFile
  #29 = Utf8               MyClass.java
{
  private int field;
    descriptor: I
    flags: (0x0002) ACC_PRIVATE

  public MyClass();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0

  public void myMethod();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String FOO
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
}
SourceFile: "MyClass.java"

No to teraz, spróbujmy zwiększyć zakres widoczności:

@Retention(RetentionPolicy.RUNTIME)
@interface SimpleAnnotation {
    String value() default "";
    String first() default "";
    int second() default 0;
}

I gdy resztę pozostawimy bez zmian i uruchomimy nasz program, to zobaczymy informację o adnotacji, którą położyliśmy na polu:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/18-annotation-retention (master)
$ javac *.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/18-annotation-retention (master)
$ java Main
Nazwa klasy: MyClass
Pole:
    nazwa: field
    typ: int
    adnotacja: SimpleAnnotation

Możemy też poznać wartości atrybutów, które ustawiliśmy w momencie położenia adnotacji. Wystarczy zrzutować obiekt typu Annotation na typ naszej adnotacji i wywołać odpowiednie metody:

public class Main {
    public static void main(String [] args) {
        Class<?> clazz = MyClass.class;
        // or:
        // MyClass object = new MyClass();
        // Class<?> clazz = object.getClass();
        
        System.out.println("Nazwa klasy: " + clazz.getName());
        Field[] fields = clazz.getDeclaredFields();
        
        for (Field f: fields) {
            System.out.println("Pole:");
            System.out.println("    nazwa: " + f.getName());
            System.out.println("    typ: " + f.getType());
            
            Annotation[] annotations = f.getAnnotations();
            for (Annotation a: annotations) {
                System.out.println("    adnotacja: " + a.annotationType().getName());
                if (a.annotationType() == SimpleAnnotation.class) {
                    SimpleAnnotation sa = (SimpleAnnotation) a;
                    System.out.println("        value: " + sa.value());
                    System.out.println("        first: " + sa.first());
                    System.out.println("        second:  " + sa.second());
                }
            }
        }
        
    }
}

Taki zaktualizowany program wypisze na ekran:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/18-annotation-retention (master)
$ java Main
Nazwa klasy: MyClass
Pole:
    nazwa: field
    typ: int
    adnotacja: SimpleAnnotation
        value: pole
        first: Pawel
        second:  0

Żeby nie dało się gdzieś położyć adnotacji

Jak widziałeś do tej pory, zdefiniowaną adnotację można kłaść na wszystko. Ale czasami położenie jakiejś adnotacji na jakimś elemencie może nie mieć sensu. Więc przez odpowiednią adnotację położoną na zdefiniowanej przez nas adnotacji, możemy wybrać gdzie daną adnotację będzie można położyć:

public @interface Target {
    ElementType[] value();
}

Elementy są zdefiniowane w enumie ElementType. Nie będę tu wszystkiego wymieniał, bo pozycji jest 12. Zainteresowanych odsyłam do dokumentacji. Więc zdefiniujmy, że naszą adnotację można położyć na polu i na metodzie:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@interface SimpleAnnotation {
    String value() default "";
    String first() default "";
    int second() default 0;
}

(to jest jednocześnie przykład, że można kłaść wiele adnotacji na danym elemencie)

Wtedy kompilacja klasy:

@SimpleAnnotation
public class MyClass {
    
    // @SimpleAnnotation("pole", first = "Pawel") - compilation error
    @SimpleAnnotation(value = "pole", first = "Pawel")
    private int field;
    
    @SimpleAnnotation(second = 7)
    public void myMethod() {
        System.out.println("FOO");
    }
}

zakończy się niepowodzeniem:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/19-annotation-target (master)
$ javac *.java
MyClass.java:1: error: annotation type not applicable to this kind of declaration
@SimpleAnnotation
^
1 error

Adnotacje przy dziedziczeniu

A co się dzieje, jeżeli do tego worka włożymy jeszcze dziedziczenie? Rozważmy adnotację:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface SimpleAnnotation {
    String value() default "";
}

I połóżmy ją na nad klasie:

@SimpleAnnotation
public class SuperClass {
    public int a;
}

Zdefiniujmy też podklasę:

public class SubClass extends SuperClass {
    public int b;
}

I sprawdźmy, które klasy będą miały położoną adnotację:

public class Main {
    
    private static void printClassMetadata(Class<?> clazz) {
        System.out.println("Klasa: " + clazz.getName());
        Annotation[] annotations = clazz.getAnnotations();
        
        for (Annotation a: annotations) {
            System.out.println("    adnotacja: " + a.annotationType().getName());
        }
    }
    
    public static void main(String[] args) {
        printClassMetadata(SuperClass.class);
        printClassMetadata(SubClass.class);
        
    }
}

Uruchomienie tego programu wypisze na ekran:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/20-annotation-inheritance (master)
$ java Main
Klasa: SuperClass
    adnotacja: SimpleAnnotation
Klasa: SubClass

Możemy z tego wyciągnąć wniosek, że adnotacje klasy się nie dziedziczą i tak rzeczywiście jest. Możemy to zmienić, przez położenie adnotacji @Inherited – wtedy adnotacja zostanie odziedziczona.

Powtarzanie adnotacji

Okazuje się, że nie można położyć jednej adnotacji kilka razy na tym samym elemencie programu. Przy podobnej próbie dostaniemy błąd kompilacji:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/20-annotation-inheritance (master)
$ javac *.java
SuperClass.java:2: error: SimpleAnnotation is not a repeatable annotation type
@SimpleAnnotation
^
1 error

Błąd kompilacji już nam coś sugeruje. Okazuje się, że adnotację możemy oznaczyć jako możliwą do powtórzenia stosują adnotację:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    /**
     * Indicates the <em>containing annotation type</em> for the
     * repeatable annotation type.
     * @return the containing annotation type
     */
    Class<? extends Annotation> value();
}

Jak więc widać, naszą adnotację możemy oznaczyć jako powtarzalną, ale musimy w tym celu stworzyć adnotację-kontener na wielokrotne występowanie naszej adnotacji. Na przykład:

@Retention(RetentionPolicy.RUNTIME)
@interface SimpleAnnotations {
    SimpleAnnotation[] value();
}

Gdy mamy już tą agregującą adnotację możemy uzupełnić:

@Repeatable(SimpleAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
@interface SimpleAnnotation {
    String value() default "";
}

I mimo, że położymy dwie adnotacje SimpleAnnotation

@SimpleAnnotation()
@SimpleAnnotation()
public class Foo {
    void bar() {}
}

to program będzie widział jedną adnotację agregującą. Następujący program

public class Main {
    public static void main(String [] args) {
        Class<Foo> clazz = Foo.class;
        for (Annotation a : clazz.getAnnotations()) {
            System.out.println(a.annotationType().getName());
        }
    }
}

wydrukuje:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/21-annotation-repeatable (master)
$ java Main
SimpleAnnotations

Kolejna dygresja – pliki JSON

Zawsze, jak myślę o JSONie, to widzę oczami wyobraźni ten filmik:

Ale do rzeczy. JSON to skrót od JavaScript Object Notation. To jest sposób tekstowej reprezentacji obiektów w postaci zbioru par klucz-wartość. Znowu pobawię się w definiowanie przez przykład (zgapiony z polskiej Wikipedii):

{
  "menu": {
    "id": "file",
    "value": "File",
    "popup": {
      "menuitem": [
        {"value": "New", "onclick": "CreateNewDoc()"},
        {"value": "Open", "onclick": "OpenDoc()"},
        {"value": "Close", "onclick": "CloseDoc()"}
      ]
    }
  }
}

Bez znajomości formatu, jesteś w stanie zrozumieć, że mamy zbiorczy obiekt menu, który ma trzy własności: id, value, popup. Wartością własności id jest file, wartością dla klucza value to File, ale wartością cechy popup, jest już zagnieżdzony obiekt menuitem, który ma tablicę trzech małych obiektów.

Gdy połączymy naszą znajomość refleksji, możemy napisać prosty program, który będzie tworzył JSONa dla obiektu dowolnej klasy. My weźmiemy sobie prostą klasę (pola są publiczne tylko z takiego powodu, żeby mieć mnie pisania – refleksja bez problemu radzi sobie z polami prywatnymi):

public class Student {
    public String firstName;
    public String lastName;
    public String address;
    public int age;
    public double height;

    public Student(String firstName, String lastName, String address, int age, double height) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.address = address;
        this.age = age;
        this.height = height;
    }
}

I napiszemy prosty konwerter dowolnej klasy na JSON:

public class Main {

    public static String serializeToJSON(Object o) throws IllegalAccessException {
        Class<?> clazz = o.getClass();
        Field[] fields = clazz.getDeclaredFields();

        StringBuilder result = new StringBuilder("{");
        result.append(System.lineSeparator());

        for (Field f : fields) {
            String fieldName = f.getName();
            f.setAccessible(true);
            Object value = f.get(o);
            result.append(String.format("'%s' : '%s'", fieldName, value.toString())).append(System.lineSeparator());
        }
        result.append("}");
        return result.toString();
    }

    public static void main(String[] args) throws IllegalAccessException {
        Student s = new Student("Doktor", "Ziel", "Zielony Backlog 13", 44, 1.56);

        System.out.println(serializeToJSON(s));
    }
}

Możemy wzbogacić ten program, przez zdefiniowanie kilku adnotacji. Taką którą będziemy kłaść na klasę:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JSONObject {
}

I taką, którą będziemy kłaść na pola:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JSONField {
    String name() default "";
}

Teraz możemy dodać adnotację do klasy.

@JSONObject
public class Student {
    @JSONField("Imie")
    public String firstName;
    @JSONField("Nazwisko")
    public String lastName;
    public String address;
    @JSONField
    public int age;
    @JSONField
    public double height;

    public Student(String firstName, String lastName, String address, int age, double height) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.address = address;
        this.age = age;
        this.height = height;
    }
}

Teraz możemy zmienić nasz konwerter, tak aby konwertował obiekty z klas z adnotacją: @JSONObject i żeby wybierał tylko pola z adnotacją @JSONField. A jeżeli ustawiliśmy atrybut, to niech zmieni nazwę pola na wartość atrybutu:

public class Main {

    public static String serializeToJSON(Object o) throws IllegalAccessException {
        Class<?> clazz = o.getClass();
        if (!clazz.isAnnotationPresent(JSONObject.class)) {
            throw new IllegalArgumentException("Not supported class");
        }
        Field[] fields = clazz.getDeclaredFields();

        StringBuilder result = new StringBuilder("{");
        result.append(System.lineSeparator());

        for (Field f : fields) {
            Annotation a = f.getAnnotation(JSONField.class);
            if (a != null) {
                JSONField jf = (JSONField) a;
                String fieldName = f.getName();
                if (!jf.value().isBlank()) {
                    fieldName = jf.value();
                }
                f.setAccessible(true);
                Object value = f.get(o);
                result.append(String.format("'%s' : '%s'", fieldName, value.toString())).append(System.lineSeparator());
            }
        }
        result.append("}");
        return result.toString();
    }

    public static void main(String[] args) throws IllegalAccessException {
        Student s = new Student("Doktor", "Ziel", "Zielony Backlog 13", 44, 1.56);

        System.out.println(serializeToJSON(s));
    }
}

Ten program wypisze na standardowe wyjście:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/java-annotations-basics/23-json (master)
$ java Main
{
'Imie' : 'Doktor'
'Nazwisko' : 'Ziel'
'age' : '44'
'height' : '1.56'
}

Podsumowanie

W tym dłuższym-niż-się-spodziewałem wpisie przedstawiłem krótki wstęp do adnotacji. Dziękuję, że przebrnąłeś przez ten tekst razem ze mną.

Warto wspomnieć, że korzystanie z refleksji spowalnia program. Jednakże bez refleksji nie da się korzystać z adnotacji. A adnotacje są fundamentem wielu współczesnych bibliotek. Więc uważam, że warto wiedzieć o adnotacjach coś więcej i być w stanie, jeżeli zdarzy się taka potrzeba, stworzyć i oprogramować swoją własną.

Już wkrótce kolejny wpis, który połączy temat CLI (ComandLine Interface) z tematem adnotacji.

 

Aspirujący twórca internetowy, który zna się na programowaniu i chce się dzielić wiedzą

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Scroll to top