Annotazioni Built-in di Java
Le principali annotazioni built-in di Java: @Override, @Deprecated, @SuppressWarnings, @SafeVarargs, @FunctionalInterface.
La libreria standard include un piccolo insieme di annotazioni nel pacchetto java.lang che il compilatore tratta in modo speciale. Nessuna di esse aggiunge comportamento al codice a runtime; sono tutte indicazioni per javac (o, nel caso di @Deprecated, per l'intera toolchain). Capire cosa promette ciascuna — e cosa non promette — è la parte pratica del lavoro quotidiano con le annotazioni.
Le cinque che incontrerai più spesso:
@Override— "questo metodo sovrascrive uno di un supertipo."@Deprecated— "non usare questo; verrà rimosso."@SuppressWarnings— "silenzia queste specifiche avvertenze in questo scope."@SafeVarargs— "il mio metodo varargs non inquina l'heap."@FunctionalInterface— "questa interfaccia ha esattamente un metodo astratto."
@Override
Inserire @Override su un metodo dice al compilatore che il metodo intende sovrascrivere un metodo della superclasse o implementare un metodo di un'interfaccia. Se non sovrascrive effettivamente nulla (perché il nome è scritto male, la firma è errata, o il tipo padre ha rimosso il metodo), javac interrompe la compilazione.
class Animal {
public String speak() { return "?"; }
}
class Dog extends Animal {
@Override
public String speak() { return "woof"; } // OK
@Override
public String spaek() { return "oops"; } // compile error: nothing to override
}Intercetta il tipo di bug molto difficile da trovare a runtime: un metodo che sovrascrive la cui firma si discosta da quella della superclasse. Scrivi sempre @Override sui metodi che intendi sovrascrivere. Ha retention SOURCE, quindi scompare nel momento in cui la compilazione termina.
@Deprecated
@Deprecated contrassegna qualcosa — classe, metodo, campo, costruttore — come sconsigliato. Il compilatore avverte in ogni punto di utilizzo. Da Java 9 l'annotazione accetta due elementi:
@Deprecated(since = "1.4", forRemoval = true)
public void oldApi() { ... }sincedocumenta quando è iniziata la deprecazione.forRemoval = trueè un segnale più forte: una versione futura prevede di eliminare l'API. Il compilatore emette un removal warning (tipicamente più evidente di un semplice avviso di deprecazione) e lo strumento Javadoc lo segnala in modo diverso.
A differenza di @Override, @Deprecated ha retention RUNTIME — strumenti bytecode, IDE e reflection possono vederla. Il tag Javadoc associato @deprecated (minuscolo, in un commento doc) porta la spiegazione; l'annotazione attiva i meccanismi degli strumenti.
@SuppressWarnings
Quando il compilatore ha ragione quasi sempre ma sbaglia qui, @SuppressWarnings silenzia una categoria di avvertenze all'interno dell'elemento annotato:
@SuppressWarnings("unchecked")
List<String> strings = (List<String>) raw; // raw cast intentional
@SuppressWarnings({"unchecked", "rawtypes"})
public void uglyButNeeded() { ... }L'elemento string indica una categoria di avvertenza; le più comuni sono unchecked, rawtypes, deprecation, serial, unused, removal. I compilatori possono accettarne altre. Due regole per mantenere pulito l'uso:
- Annota lo scope più piccolo possibile. Sopprimi su una variabile locale o su un singolo metodo, mai su una classe, a meno che ogni riga non ne abbia davvero bisogno.
- Abbinala a un commento che spieghi il perché. Un
@SuppressWarnings("unchecked")nudo accanto a un cast lascia il prossimo lettore a chiedersi se il cast sia effettivamente sicuro.
@SafeVarargs
Un metodo varargs il cui tipo di parametro contiene un parametro di tipo ha un problema sottile: nel punto di chiamata, il compilatore potrebbe dover creare un array di un tipo generico, il che non è sicuro. Il compilatore avverte con il messaggio "possible heap pollution". Se l'autore ha verificato che il corpo non fa trapelare l'array né scrive tipi errati al suo interno, @SafeVarargs silenzia l'avviso:
@SafeVarargs
public final <T> List<T> listOf(T... items) {
return java.util.List.of(items); // only reads the items, never stores other types
}Regole:
- Valida solo su metodi che non possono essere sovrascritti —
static,final, oprivate, più i costruttori in stile record. - L'annotazione è una promessa. Se il corpo scrive effettivamente nell'array varargs con un tipo errato, il cast nel punto di chiamata può fallire in seguito con un
ClassCastExceptionconfuso.
Ha retention SOURCE.
@FunctionalInterface
Un'interfaccia funzionale è un'interfaccia con esattamente un metodo astratto — la forma condivisa da Runnable, Callable, Comparator e Function. Le espressioni lambda e i riferimenti a metodi si rivolgono alle interfacce funzionali. L'annotazione rende esplicita l'intenzione e chiede al compilatore di applicare la regola del singolo metodo astratto:
@FunctionalInterface
public interface StringMapper {
String map(String input); // the single abstract method
default StringMapper andThen(StringMapper next) { // default methods are allowed
return s -> next.map(map(s));
}
}Se in seguito aggiungi un secondo metodo astratto, la compilazione fallisce immediatamente. Senza l'annotazione, l'interfaccia smetterebbe silenziosamente di essere utilizzabile come target di una lambda — cosa che un utente noterebbe solo nel punto di chiamata.
Come @Override, ha retention SOURCE.
Un esempio pratico: vedere l'applicazione del compilatore
Questo programma dimostra le quattro annotazioni applicate e mostra cosa il runtime può vedere in seguito. Le parti interessanti: il metodo @SafeVarargs compila effettivamente dove quello non annotato stampa un avviso; @FunctionalInterface si riflette tramite le regole di conteggio SAM; i valori dell'elemento @Deprecated arrivano a runtime.
Cosa ricavare dall'esecuzione:
@FunctionalInterfaceha svolto il suo lavoro a compile-time garantendo cheStringMappersia un tipo SAM: siaString::toUpperCaseche la lambdas -> s + \"!\"si sono legate correttamente ad essa. Se qualcuno avesse aggiunto un secondo metodo astratto all'interfaccia, la compilazione sarebbe fallita e queste espressioni avrebbero smesso di risolversi.- La riga
@Overrideè stata il meccanismo contabile che ha garantito cheChild.describe()sovrascrivesse effettivamenteParent.describe(). La chiamata polimorfica che atterra su\"child\"lo conferma; se la firma si fosse discostata (nome diverso, tipo di ritorno diverso), la compilazione avrebbe fallito invece di produrre un comportamento errato a runtime. @Deprecatedè l'unica annotazione qui che sopravvive alla compilazione. La reflection ha estratto con successosince=1.4eforRemoval=truedal file class. Il metodo stesso ha continuato a funzionare —@Deprecatedavverte, non disabilita.@SafeVarargsha rimosso l'avviso "possible heap pollution" a compile-time mantenendo la chiamata type-safe. Nota che il metodo èstatic, quindi soddisfa la regola "non può essere sovrascritto". Rimuovere l'annotazione compilerebbe ma genererebbe avvisi durantejavac; aggiungerla su un metodo non-static, non-final, non-private sarebbe un errore di compilazione.@SuppressWarningsnon ha lasciato tracce a runtime — l'array di annotazioni stampato perparseOrZeroè vuoto. Questo è l'intero scopo della retentionSOURCE: l'annotazione svolge il suo lavoro durante la compilazione e poi scompare, mantenendo il file class non sovraccaricato.
Altre annotazioni built-in da conoscere
Un gruppo di annotazioni meno comuni dalla libreria standard, brevemente:
@SuppressWarnings("preview")— per codice che usa funzionalità linguistiche in anteprima (Java 14+).@Native(java.lang.annotation.Native) — contrassegna una costante che potrebbe essere referenziata dal codice nativo; usata da strumenti che generano header JNI.@Generated(javax.annotation.processing.Generated, Java 9+) — aggiunta dai generatori di codice sui file che emettono.@Documented,@Retention,@Target,@Inherited,@Repeatable— queste sono meta-annotazioni; trattate nel prossimo capitolo.
Raramente scriverai le prime tre a mano. Le meta-annotazioni sono la porta d'accesso alla scrittura di tipi di annotazione personalizzati, e la reflection è il modo in cui le annotazioni con retention runtime come @Deprecated vengono rilette — come mostra l'esempio pratico sopra.