Introduzione alle Annotazioni in Java

Concetti Chiave
  • Le annotazioni in Java sono un modo per aggiungere metadati al codice senza influenzare la semantica del programma.
  • Le annotazioni sono dichiarate utilizzando la parola chiave @interface e possono contenere metodi che agiscono come membri.
  • Le annotazioni possono essere applicate a classi, metodi, campi, parametri e costanti enum.
  • Le politiche di ritenzione delle annotazioni determinano quando un'annotazione viene scartata: SOURCE, CLASS o RUNTIME.
  • Le annotazioni con ritenzione RUNTIME possono essere interrogate a tempo di esecuzione utilizzando la reflection.
  • La reflection consente di ottenere informazioni su classi, metodi e campi, incluse le annotazioni associate.
  • Le annotazioni possono essere ottenute utilizzando i metodi getAnnotation() e getAnnotations() su oggetti di tipo Class, Method, Field, e altri.
  • Le annotazioni possono essere utilizzate per fornire informazioni aggiuntive al compilatore, agli strumenti di sviluppo e alla JVM.

Cosa sono le Annotazioni

Java fornisce una funzionalità che consente di incorporare informazioni supplementari in un file sorgente.

Queste informazioni, chiamate annotazioni, non cambiano le azioni di un programma. Pertanto, un'annotazione lascia invariata la semantica di un programma.

Tuttavia, queste informazioni possono essere utilizzate da vari strumenti durante lo sviluppo e la distribuzione. Ad esempio, un'annotazione potrebbe essere elaborata da un generatore di codice sorgente. Il termine metadato viene anche utilizzato per riferirsi a questa funzionalità, ma il termine annotazione è più descrittivo e più comunemente usato.

Fondamenti delle Annotazioni

Un'annotazione viene creata attraverso un meccanismo basato sulle interfacce.

Iniziamo con un esempio. Ecco la dichiarazione per un'annotazione chiamata MiaAnnotazione:

// Un semplice tipo di annotazione.
@interface MiaAnnotazione {
    String str();
    int val();
}

Notiamo alcune cose:

  1. Prima di tutto, si noti la peculiare sintassi: la @ che precede la parola chiave interface. Questo dice al compilatore che viene dichiarato un tipo di annotazione.
  2. Successivamente, si notino i due membri str() e val(). Tutte le annotazioni consistono esclusivamente di dichiarazioni di metodi. Tuttavia, non si forniscono corpi per questi metodi. Inoltre, i metodi agiscono molto come campi, come vedremo tra poco. Quindi, i membri di annotazione sono simili ai campi di una classe.

Un'annotazione non può includere una clausola extends. Tuttavia, tutti i tipi di annotazione estendono automaticamente l'interfaccia Annotation.

Quindi, Annotation è una super-interfaccia di tutte le annotazioni. È dichiarata all'interno del package java.lang.annotation. Sovrascrive hashCode(), equals(), e toString(), che sono definiti da Object. Specifica anche annotationType(), che restituisce un oggetto Class che rappresenta l'annotazione invocante.

Una volta che abbiamo dichiarato un'annotazione, possiamo usarla per annotare qualcosa. Inizialmente, le annotazioni potevano essere usate solo sulle dichiarazioni, ed è da qui che inizieremo.

JDK 8 ha aggiunto la capacità di annotare l'uso dei tipi, e questo è descritto più avanti nelle prossime lezioni. Tuttavia, le stesse tecniche di base si applicano a entrambi i tipi di annotazioni.

Qualsiasi tipo di dichiarazione può avere un'annotazione associata ad essa. Per esempio, classi, metodi, campi, parametri, e costanti enum possono essere annotati. Anche un'annotazione può essere annotata. In tutti i casi, l'annotazione precede il resto della dichiarazione.

Quando si applica un'annotazione, si danno dei valori ai suoi membri. Per esempio, ecco un esempio di MiaAnnotazione che viene applicata a una dichiarazione di metodo:

// Annotare un metodo.
@MiaAnnotazione(str = "Esempio di Annotazione", val = 100)
public static void mioMetodo() { // ...

Questa annotazione è collegata al metodo mioMetodo().

Si guardi attentamente la sintassi dell'annotazione. Il nome dell'annotazione, preceduto da una @, è seguito da una lista tra parentesi di inizializzazioni dei membri.

Per dare un valore a un membro, il nome di quel membro viene assegnato a un valore. Quindi, nell'esempio, la stringa "Esempio di Annotazione" viene assegnata al membro str di MiaAnnotazione. Si noti che nessuna parentesi segue str in questa assegnazione. Quando a un membro di annotazione viene dato un valore, viene usato solo il suo nome. Quindi, i membri di annotazione sembrano campi in questo contesto.

Specificare una Politica di Ritenzione

Prima di esplorare ulteriormente le annotazioni, è necessario discutere le politiche di ritenzione delle annotazioni.

Una politica di ritenzione determina a che punto un'annotazione viene scartata. Java definisce tre di tali politiche, che sono incapsulate all'interno dell'enumerazione java.lang.annotation.RetentionPolicy.

Esse sono:

  • SOURCE
  • CLASS
  • RUNTIME

Un'annotazione con una politica di ritenzione di SOURCE è mantenuta solo nel file sorgente ed è scartata durante la compilazione.

Un'annotazione con una politica di ritenzione di CLASS è memorizzata nel file .class durante la compilazione. Tuttavia, non è disponibile attraverso la JVM durante l'esecuzione.

Un'annotazione con una politica di ritenzione di RUNTIME è memorizzata nel file .class durante la compilazione ed è disponibile attraverso la JVM durante l'esecuzione. Pertanto, la ritenzione RUNTIME offre la maggiore persistenza dell'annotazione.

Si noti che un'annotazione su una dichiarazione di variabile locale non è mantenuta nel file .class.

Una politica di ritenzione è specificata per un'annotazione utilizzando una delle annotazioni integrate di Java: @Retention. La sua forma generale è mostrata qui:

@Retention(politica_di_ritenzione)

Qui, politica_di_ritenzione deve essere una delle costanti di enumerazione precedentemente discusse. Se nessuna politica di ritenzione è specificata per un'annotazione, allora viene utilizzata la politica predefinita di CLASS.

La seguente versione di MiaAnnotazione utilizza @Retention per specificare la politica di ritenzione RUNTIME. Pertanto, MiaAnnotazione sarà disponibile alla JVM durante l'esecuzione del programma.

@Retention(RetentionPolicy.RUNTIME)
@interface MiaAnnotazione {
    String str();
    int val();
}

Accedere alle Annotazioni a Tempo di Esecuzione

Anche se le annotazioni sono progettate principalmente per l'uso da parte di altri strumenti di sviluppo o distribuzione, se specificano una politica di ritenzione di RUNTIME, allora possono essere interrogate a tempo di esecuzione da qualsiasi programma Java attraverso l'uso della reflection.

La reflection è la funzionalità che consente di ottenere informazioni su una classe o un oggetto a tempo di esecuzione. L'API di reflection è contenuta nel package java.lang.reflect. Ci sono diversi modi per utilizzare la reflection, e non li esamineremo tutti qui. Tuttavia, esamineremo alcuni esempi che si applicano alle annotazioni. Tratteremo approfonditamente la reflection nelle prossime lezioni.

Il primo passo per utilizzare la reflection è ottenere un oggetto Class che rappresenta la classe le cui annotazioni si desidera ottenere. Class è una delle classi integrate di Java ed è definita in java.lang. La studieremo in dettaglio nelle prossime lezioni. Ci sono vari modi per ottenere un oggetto Class. Uno dei più semplici è chiamare getClass(), che è un metodo definito da Object. La sua forma generale è mostrata qui:

final Class<?> getClass()

Restituisce l'oggetto Class che rappresenta l'oggetto invocante.

Si noti il <?> che segue Class nella dichiarazione di getClass() appena mostrata. Questo è relativo alla funzionalità dei generics di Java. getClass() e diversi altri metodi relativi alla reflection discussi in questa lezione fanno uso dei generics. I generics saranno analizzati nelle prossime lezioni. Tuttavia, una comprensione dei generics non è necessaria per comprendere i principi fondamentali della reflection.

Dopo aver ottenuto un oggetto Class, si possono utilizzare i suoi metodi per ottenere informazioni sui vari elementi dichiarati dalla classe, incluse le sue annotazioni. Se si vogliono ottenere le annotazioni associate con un elemento specifico dichiarato all'interno di una classe, si deve prima ottenere un oggetto che rappresenta quell'elemento. Ad esempio, Class fornisce (tra gli altri) i metodi getMethod(), getField(), e getConstructor(), che ottengono informazioni su un metodo, campo e costruttore, rispettivamente. Questi metodi restituiscono oggetti di tipo Method, Field, e Constructor.

Per comprendere il processo, esaminiamo un esempio che ottiene le annotazioni associate con un metodo. Per fare questo, prima ottenete un oggetto Class che rappresenta la classe, e poi chiamate getMethod() su quell'oggetto Class, specificando il nome del metodo. getMethod() ha questa forma generale:

Method getMethod(String nomeMetodo, Class<?> ... tipiParametri)

Il nome del metodo è passato in nomeMetodo. Se il metodo ha argomenti, allora oggetti Class che rappresentano quei tipi devono anche essere specificati da tipiParametri. Si noti che tipiParametri è un parametro varargs. Questo significa che si possono specificare quanti tipi di parametri sono necessari, inclusi zero. getMethod() restituisce un oggetto Method che rappresenta il metodo. Se il metodo non può essere trovato, viene lanciata NoSuchMethodException.

Da un oggetto Class, Method, Field, o Constructor, si possono ottenere un'annotazione specifica associata con quell'oggetto chiamando getAnnotation(). La sua forma generale è mostrata qui:

<A extends Annotation> getAnnotation(Class<A> tipoAnnotazione)

Qui, tipoAnnotazione è un oggetto Class che rappresenta l'annotazione a cui siamo interessati. Il metodo restituisce un riferimento all'annotazione. Utilizzando questo riferimento, si possono ottenere i valori associati con i membri dell'annotazione. Il metodo restituisce null se l'annotazione non viene trovata, che sarà il caso se l'annotazione non ha ritenzione RUNTIME.

Ecco un programma che assembla tutti i pezzi mostrati in precedenza e utilizza la reflection per visualizzare l'annotazione associata con un metodo:

import java.lang.annotation.*;
import java.lang.reflect.*;

// La nostra annotazione
@Retention(RetentionPolicy.RUNTIME)
@interface MiaAnnotazione {
    String str();
    int val();
}

class Meta {

    // Annota un metodo.
    @MiaAnnotazione(str = "Esempio Annotazione", val = 100)
    public static void mioMetodo() {
        Meta ob = new Meta();

        // Ottiene l'annotazione per questo metodo
        // e visualizza i valori dei membri.
        try {
            // Prima, ottiene un oggetto Class che rappresenta
            // questa classe.
            Class<?> c = ob.getClass();

            // Ora, ottiene un oggetto Method che rappresenta
            // questo metodo.
            Method m = c.getMethod("mioMetodo");

            // Successivamente, ottiene l'annotazione per questa classe.
            MiaAnno anno = m.getAnnotation(MiaAnno.class);

            // Infine, visualizza i valori.
            System.out.println(anno.str() + " " + anno.val());
        } catch (NoSuchMethodException exc) {
            System.out.println("Metodo Non Trovato.");
        }
    }

    public static void main(String[] args) {
        mioMetodo();
    }
}

L'output del programma è mostrato qui:

Esempio Annotazione 100

Questo programma utilizza la reflection come descritto per ottenere e visualizzare i valori di str e val nell'annotazione MiaAnnotazione associata con mioMetodo() nella classe Meta. Ci sono due cose a cui prestare particolare attenzione. Prima, in questa riga

MiaAnnotazione anno = m.getAnnotation(MiaAnnotazione.class);

Si noti l'espressione MiaAnnotazione.class. Questa espressione restituisce un oggetto Class di tipo MiaAnnotazione, l'annotazione.

Questo costrutto è chiamato un letterale di classe. Si può utilizzare questo tipo di espressione ogni volta che è necessario un oggetto Class di una classe conosciuta. Ad esempio, questa istruzione avrebbe potuto essere utilizzata per ottenere l'oggetto Class per Meta:

Class<?> c = Meta.class;

Naturalmente, questo approccio funziona solo quando si conosce il nome della classe di un oggetto in anticipo, che potrebbe non essere sempre il caso. In generale, si può ottenere un letterale di classe per classi, interfacce, tipi primitivi e array. Bisogna ricordare che la sintassi <?> è relativa alla funzionalità dei generics di Java.

Il secondo punto di interesse è il modo in cui i valori associati con str e val sono ottenuti quando sono visualizzati dalla seguente riga:

System.out.println(anno.str() + " " + anno.val());

Si noti che sono invocati utilizzando la sintassi della chiamata di metodo. Questo stesso approccio è utilizzato ogni volta che è richiesto il valore di un membro di annotazione.

Esempio

Nell'esempio precedente, mioMetodo() non ha parametri. Quindi, quando getMethod() è stato chiamato, solo il nome mioMetodo è stato passato. Tuttavia, per ottenere un metodo che ha parametri, dobbiamo specificare oggetti class che rappresentano i tipi di quei parametri come argomenti a getMethod().

Ad esempio, ecco una versione leggermente diversa del programma precedente:

import java.lang.annotation.*;
import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@interface miaAnnotazione {
    String stringa();
    int valore();
}

class Meta {

    // mioMetodo ora ha due argomenti.
    @miaAnnotazione(stringa = "Due Parametri", valore = 19)
    public static void mioMetodo(String stringa, int i)
    {
        Meta oggetto = new Meta();
        try {
            Class<?> c = oggetto.getClass();

            // Qui, i tipi dei parametri sono specificati.
            Method m = c.getMethod("mioMetodo", String.class, int.class);
            miaAnnotazione annotazione = m.getAnnotation(miaAnnotazione.class);
            System.out.println(annotazione.stringa() + " " + annotazione.valore());
        } catch (NoSuchMethodException eccezione) {
            System.out.println("Metodo Non Trovato.");
        }
    }

    public static void main(String[] args) {
        mioMetodo("test", 10);
    }

}

L'output di questa versione è mostrato qui:

Due Parametri 19

In questa versione, mioMetodo() prende un parametro String e un int. Per ottenere informazioni su questo metodo, getMethod() deve essere chiamato come mostrato qui:

Method m = c.getMethod("mioMetodo", String.class, int.class);

Qui, gli oggetti Class che rappresentano String e int sono passati come argomenti aggiuntivi.

Ottenere Tutte le Annotazioni

È possibile ottenere tutte le annotazioni che hanno ritenzione RUNTIME associate con un elemento chiamando getAnnotations() su quell'elemento. La sua forma generale è mostrata qui:

Annotation[] getAnnotations()

Restituisce un array delle annotazioni. getAnnotations() può essere chiamato su oggetti di tipo Class, Method, Constructor, e Field, tra gli altri.

Ecco un altro esempio di reflection che mostra come ottenere tutte le annotazioni associate con una classe e con un metodo. Dichiara due annotazioni. Poi usa quelle annotazioni per annotare una classe e un metodo.

// Mostra tutte le annotazioni per una classe e un metodo.
import java.lang.annotation.*;
import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@interface MiaAnnotazione {
    String str();
    int val();
}

@Retention(RetentionPolicy.RUNTIME)
@interface Cosa {
    String descrizione();
}

@Cosa(descrizione = "Una classe di test per annotazioni")
@MiaAnnotazione(str = "Meta2", val = 99)
class Meta2 {
    @Cosa(descrizione = "Un metodo di test per annotazioni")
    @MiaAnnotazione(str = "Testing", val = 100)
    public static void mioMeta() {
        Meta2 og = new Meta2();
        try {
            Annotation[] annotations = og.getClass().getAnnotations();
            // Visualizza tutte le annotazioni per Meta2.

            System.out.println("Tutte le annotazioni per Meta2:");
            for(Annotation a : annotations)
                System.out.println(a);
            System.out.println();

            // Visualizza tutte le annotazioni per mioMeta.
            Method m = og.getClass().getMethod("mioMeta");
            annotations = m.getAnnotations();
            System.out.println("Tutte le annotazioni per mioMeta:");
            for(Annotation a : annotations)
                System.out.println(a);
        } catch (NoSuchMethodException exc) {
            System.out.println("Metodo Non Trovato.");
        }
    }

    public static void main(String[] args) {
        mioMeta();
    }

}

L'output è mostrato qui:

Tutte le annotazioni per Meta2:
@Cosa(descrizione="Una classe di test per annotazioni")
@MiaAnnotazione(str="Meta2", val=99)

Tutte le annotazioni per mioMeta:
@Cosa(descrizione="Un metodo di test per annotazioni")
@MiaAnnotazione(str="Testing", val=100)

Il programma usa getAnnotations() per ottenere un array di tutte le annotazioni associate con la classe Meta2 e con il metodo mioMeta().

Come spiegato, getAnnotations() restituisce un array di oggetti Annotation. Ricordiamo che Annotation è una super-interfaccia di tutte le interfacce di annotazione e che sovrascrive toString() in Object. Quindi, quando un riferimento ad una Annotation è emesso in output, il suo metodo toString() è chiamato per generare una stringa che descrive l'annotazione, come mostra l'output precedente.

L'Interfaccia AnnotatedElement

I metodi getAnnotation() e getAnnotations() utilizzati dagli esempi precedenti sono definiti dall'interfaccia AnnotatedElement, che è definita in java.lang.reflect.

Questa interfaccia supporta la reflection per le annotazioni ed è implementata dalle classi Method, Field, Constructor, Class e Package, tra le altre.

Oltre a getAnnotation() e getAnnotations(), AnnotatedElement definisce diversi altri metodi. Due sono disponibili sin da quando le annotazioni sono state inizialmente aggiunte a Java. Il primo è getDeclaredAnnotations(), che ha questa forma generale:

Annotation[] getDeclaredAnnotations()

Restituisce tutte le annotazioni non ereditate presenti nell'oggetto invocante. Il secondo è isAnnotationPresent(), che ha questa forma generale:

default boolean isAnnotationPresent(Class<? extends Annotation> tipoAnnotazione)

Restituisce true se l'annotazione specificata da tipoAnnotazione è associata all'oggetto invocante. Restituisce false altrimenti.

A questi, JDK 8 ha aggiunto getDeclaredAnnotation(), getAnnotationsByType() e getDeclaredAnnotationsByType(). Di questi, gli ultimi due funzionano automaticamente con un'annotazione ripetuta. Vedremo le annotazioni ripetute nelle prossime lezioni.