Annotazioni di Tipo in Java

Concetti Chiave
  • Le annotazioni di tipo in Java consentono di specificare metadati sui tipi, migliorando la documentazione e la verifica del codice.
  • Le annotazioni di tipo possono essere utilizzate su vari elementi, inclusi i metodi, i parametri e i tipi generici.
  • È importante utilizzare le annotazioni di tipo in modo appropriato per garantire la qualità e la manutenibilità del codice.

Annotazioni di Tipo

Come menzionato nelle lezioni precedenti, le annotazioni erano originariamente consentite solo sulle dichiarazioni.

Tuttavia, per le versioni moderne di Java, le annotazioni possono anche essere specificate nella maggior parte dei casi in cui viene utilizzato un tipo.

Questa nuova funzionalità delle annotazioni è chiamata annotazione di tipo.

Ad esempio, è possibile annotare il tipo di ritorno di un metodo, il tipo di this all'interno di un metodo, un cast, i livelli di array, una classe ereditata e una clausola throws.

È inoltre possibile annotare tipi generici, inclusi i limiti dei parametri di tipo generico e gli argomenti di tipo generico. I tipi generici, o Generics, li affronteremo nelle prossime lezioni.

Le annotazioni di tipo sono importanti perché consentono agli strumenti di sviluppo (come IDE e editor) di eseguire controlli aggiuntivi sul codice per aiutare a prevenire errori. È importante capire che, come regola generale, il compilatore javac non eseguirà questi controlli da solo. Per questo scopo viene utilizzato uno strumento separato, anche se tale strumento potrebbe operare come un plug-in del compilatore.

Un'annotazione di tipo deve includere ElementType.TYPE_USE come target. Ricordiamo che i target di annotazione validi sono specificati utilizzando l'annotazione @Target, come descritto nella lezione precedente.

Un'annotazione di tipo si applica al tipo che l'annotazione precede. Ad esempio, assumendo di aver implementato un'annotazione di tipo chiamata @AnnotazioneTipo, la seguente riga di codice è legale:

void mioMetodo() throws @AnnotazioneTipo NullPointerException {
    // ...
}

Qui, @AnnotazioneTipo annota NullPointerException nella clausola throws.

È inoltre possibile annotare il tipo di this (chiamato il ricevitore). Come sappiamo, this è un argomento implicito a tutti i metodi di istanza e si riferisce all'oggetto invocante. Per annotare il suo tipo è necessario l'uso di un'altra caratteristica che non faceva originariamente parte di Java. A partire da JDK 8, è possibile dichiarare esplicitamente this come primo parametro di un metodo. In questa dichiarazione, il tipo di this deve essere il tipo della sua classe; ad esempio:

class UnaClasse {

    // Dichiara esplicitamente this come primo parametro.
    int mioMetodo(UnaClasse this, int i, int j) {
        // ...
    }

}

Qui, poiché mioMetodo() è un metodo definito da UnaClasse, il tipo di this è UnaClasse. Utilizzando questa dichiarazione, ora è possibile annotare il tipo di this. Ad esempio, assumendo di nuovo che @AnnotazioneTipo sia un'annotazione di tipo, la riga seguente è legale:

int mioMetodo(@AnnotazioneTipo UnaClasse this, int i, int j) {
    // ...
}

È importante capire che non è necessario dichiarare this a meno che non lo si stia annotando. Se this non è dichiarato, viene comunque passato implicitamente, come è sempre stato.

Inoltre, dichiarare esplicitamente this non cambia alcun aspetto della firma del metodo perché this è dichiarato implicitamente, per impostazione predefinita. Ancora una volta, dichiareremo this solo se vogliamo applicare un'annotazione di tipo ad esso. In ogni caso, se si dichiara this, deve essere il primo parametro.

Il seguente programma mostra una serie di luoghi in cui può essere utilizzata un'annotazione di tipo. Definisce diverse annotazioni, di cui diverse sono per l'annotazione di tipo. I nomi e i target delle annotazioni sono mostrati qui:

Annotazione Target
@AnnotazioneTipo ElementType.TYPE_USE
@LunghezzaMax ElementType.TYPE_USE
@NonLunghezzaZero ElementType.TYPE_USE
@Unico ElementType.TYPE_USE
@Cosa ElementType.TYPE_PARAMETER
@VuotoOK ElementType.FIELD
@Raccomandato ElementType.METHOD
Tabella 1: Annotazioni di Tipo create dall'esempio e loro Parametri Target

Notiamo che @VuotoOK, @Raccomandato e @Cosa non sono annotazioni di tipo. Sono incluse per scopi di confronto. Di particolare interesse è @Cosa, che viene utilizzata per annotare una dichiarazione di parametro di tipo generico. I commenti nel programma descrivono ogni uso.

// Dimostra diverse annotazioni di tipo.

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

// Un'annotazione marcatore che può essere applicata a un tipo.
@Target(ElementType.TYPE_USE)
@interface AnnotazioneTipo {
}

// Un'altra annotazione marcatore che può essere applicata a un tipo.
@Target(ElementType.TYPE_USE)
@interface NonLunghezzaZero {
}

// Ancora un'altra annotazione marcatore che può essere applicata a un tipo.
@Target(ElementType.TYPE_USE)
@interface Unico {
}

// Un'annotazione parametrizzata che può essere applicata a un tipo.
@Target(ElementType.TYPE_USE)
@interface LunghezzaMax {
    int value();
}

// Un'annotazione che può essere applicata a un parametro di tipo.
@Target(ElementType.TYPE_PARAMETER)
@interface Cosa {
    String description();
}

// Un'annotazione che può essere applicata a una dichiarazione di campo.
@Target(ElementType.FIELD)
@interface VuotoOK {
}

// Un'annotazione che può essere applicata a una dichiarazione di metodo.
@Target(ElementType.METHOD)
@interface Raccomandato {
}

// Usa un'annotazione su un parametro di tipo.
class DemoAnnotazioneTipo<@Cosa(description = "Tipo di dato generico") T> {

    // Usa un'annotazione di tipo su un costruttore.
    public @Unico DemoAnnotazioneTipo() {
    }

    // Annota il tipo (in questo caso String), non il campo.
    @AnnotazioneTipo
    String str;

    // Questo annota il campo test.
    @VuotoOK
    String test;

    // Usa un'annotazione di tipo per annotare this (il ricevitore).
    public int f(@AnnotazioneTipo DemoAnnotazioneTipo<T> this, int x) {
        return 10;
    }

    // Annota il tipo di ritorno.
    public @AnnotazioneTipo Integer f2(int j, int k) {
        return j + k;
    }

    // Annota la dichiarazione del metodo.
    public @Raccomandato Integer f3(String str) {
        return str.length() / 2;
    }

    // Usa un'annotazione di tipo con una clausola throws.
    public void f4() throws @AnnotazioneTipo NullPointerException {
        // ...
    }

    // Annota i livelli dell'array.
    String @LunghezzaMax(10) [] @NonLunghezzaZero [] w;

    // Annota il tipo dell'elemento dell'array.
    @AnnotazioneTipo
    Integer[] vec;

    public static void mioMetodo(int i) {
        // Usa un'annotazione di tipo su un argomento di tipo.
        DemoAnnotazioneTipo<@AnnotazioneTipo Integer> ob =
            new DemoAnnotazioneTipo<@AnnotazioneTipo Integer>();

        // Usa un'annotazione di tipo con new.
        @Unico
        DemoAnnotazioneTipo<Integer> ob2 =
            new @Unico DemoAnnotazioneTipo<Integer>();

        Object x = Integer.valueOf(10);
        Integer y;
        // Usa un'annotazione di tipo su un cast.
        y = (@AnnotazioneTipo Integer) x;
    }

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

// Usa l'annotazione di tipo con la clausola di ereditarietà.
class UnaClasse extends @AnnotazioneTipo DemoAnnotazioneTipo<Boolean> {
}

Sebbene ciò a cui si riferiscono la maggior parte delle annotazioni nel programma precedente sia chiaro, quattro usi richiedono un po' di discussione.

Il primo è l'annotazione di un tipo di ritorno di un metodo rispetto all'annotazione di una dichiarazione di metodo. Nel programma, si presti particolare attenzione a queste due dichiarazioni di metodo:

// Annota il tipo di ritorno.
public @AnnotazioneTipo Integer f2(int j, int k) {
    return j+k;
}
// Annota la dichiarazione del metodo.
public @Raccomandato Integer f3(String str) {
    return str.length() / 2;
}

Notiamo che in entrambi i casi, un'annotazione precede il tipo di ritorno del metodo (che è Integer). Tuttavia, le due annotazioni annotano due cose diverse. Nel primo caso, l'annotazione @AnnotazioneTipo annota il tipo di ritorno di f2(). Questo perché @AnnotazioneTipo ha il suo target specificato come ElementType.TYPE_USE, il che significa che può essere utilizzata per annotare usi di tipo.

Nel secondo caso, @Raccomandato annota la dichiarazione del metodo stessa. Questo perché @Raccomandato ha il suo target specificato come ElementType.METHOD. Di conseguenza, @Raccomandato si applica alla dichiarazione, non al tipo di ritorno. Pertanto, la specifica del target viene utilizzata per eliminare quella che, a prima vista, appare come ambiguità tra l'annotazione di una dichiarazione di metodo e l'annotazione del tipo di ritorno del metodo.

Un'altra cosa riguardo all'annotazione di un tipo di ritorno di un metodo: non è possibile annotare un tipo di ritorno di void.

Il secondo punto di interesse sono le annotazioni applicate ai campi, mostrate qui:

// Annota il tipo (in questo caso String), non il campo.
@AnnotazioneTipo String str;

// Questo annota il campo test.
@VuotoOK String test;

Qui, @AnnotazioneTipo annota il tipo String, ma @VuotoOK annota il campo test. Anche se entrambe le annotazioni precedono l'intera dichiarazione, i loro target sono diversi, basati sul tipo di elemento target. Se l'annotazione ha il target ElementType.TYPE_USE, allora il tipo è annotato. Se ha ElementType.FIELD come target, allora il campo è annotato. Quindi, la situazione è simile a quella appena descritta per i metodi, e non esiste ambiguità. Lo stesso meccanismo disambigua anche le annotazioni su variabili locali.

Successivamente, notiamo come this (il ricevitore) è annotato qui:

public int f(@AnnotazioneTipo DemoAnnotazioneTipo<T> this, int x) {

Qui, this è specificato come primo parametro ed è di tipo DemoAnnotazioneTipo (che è la classe di cui f() è membro). Come spiegato, una dichiarazione di metodo di istanza può specificare esplicitamente il parametro this allo scopo di applicare un'annotazione di tipo.

Infine, guardiamo come i livelli di array sono annotati dalla seguente istruzione:

String @LunghezzaMax(10) [] @NonLunghezzaZero [] w;

In questa dichiarazione, @LunghezzaMax annota il tipo del primo livello e @NonLunghezzaZero annota il tipo del secondo livello. In questa dichiarazione

@AnnotazioneTipo Integer[] vec;

il tipo dell'elemento Integer è annotato.