Questo articolo fa parte della serie “Stranger things in Java”, dedicata agli approfondimenti del linguaggio che ci permetteranno di padroneggiare anche gli scenari più strani che si possono presentare quando programmiamo. Tutti gli articoli sono ispirati dal contenuto dal libro “Java for Aliens” (in inglese), dal libro “Il nuovo Java” e dal libro “Programmazione Java”.
Questo articolo è un breve tutorial sui tipi enumerazione, detti anche enumerazioni o enum. Sono uno dei costrutti fondamentali del linguaggio Java, al pari di classi, interfacce, annotazioni e record. Sono particolarmente utili per rappresentare insiemi di valori già noti e non modificabili, come i giorni della settimana o i punti cardinali.
Cos’è un enum
Una enum si dichiara con la parola chiave enum e contiene tipicamente una lista di valori, detti elementi (o valori o anche costanti) dell’enumerazione.
Consideriamo, per esempio:
public enum PuntoCardinale {
NORD, SUD, OVEST, EST;
}
Qui abbiamo definito un’enum di nome PuntoCardinale, con quattro elementi: NORD, SUD, OVEST ed EST. Gli elementi definiti nell’enumerazione sono le uniche istanze possibili di tipo PuntoCardinale, e non è possibile istanziare altri oggetti dello stesso tipo. Quindi, se provassimo a istanziare un oggetto dall’enumerazione PuntoCardinale, otterremo un errore in compilazione:
var p3 = new PuntoCardinale(); // ERRORE: non si possono creare nuove istanze
Elementi di un’enumerazione
Utilizzare un’enumerazione, quindi, significa principalmente utilizzare i suoi elementi. Per esempio, il seguente metodo restituisce true se il parametro direzione coincide con l’elemento NORD di PuntoCardinale:
static boolean isNord(PuntoCardinale direzione) {
return direzione == PuntoCardinale.NORD;
}
Nel seguente esempio, invece, assegniamo dei reference agli elementi di PuntoCardinale:
PuntoCardinale p1 = PuntoCardinale.SUD; // p1 punta all'istanza SUD System.out.println(p1 == PuntoCardinale.SUD); // true (stessa istanza) var p2 = PuntoCardinale.EST; System.out.println(p2 == PuntoCardinale.OVEST); // false
Ogni elemento è implicitamente dichiarato public, static e final. Infatti, da questi esempi possiamo notare che:
- Per usare gli elementi di un’enumerazione bisogna sempre referenziarli tramite il nome dell’enumerazione. Infatti, scriviamo
PuntoCardinale.SUDe non direttamenteSUD. Questo dipende dal fatto che tutti gli elementi di un’enum sonopublicestatic. - Possiamo confrontare gli elementi direttamente con l’operatore
==perché sono implicitamentefinale perché sono gli unici elementi dell’enum esistenti. - I nomi degli elementi di un’enumerazione seguono le convenzioni dei nomi delle costanti (uso di caratteri esclusivamente maiuscoli ed eventualmente di caratteri di underscore), perché in effetti sono costanti (
public,staticefinal).
In effetti, durante la compilazione l’enumerazione PuntoCardinale viene trasformata in una classe simile alla seguente per supportare la compatibilità all’indietro*:
public class PuntoCardinale {
public static final PuntoCardinale NORD = new PuntoCardinale();
public static final PuntoCardinale EST = new PuntoCardinale();
public static final PuntoCardinale SUD = new PuntoCardinale();
public static final PuntoCardinale OVEST = new PuntoCardinale();
// ...
}
Inoltre, il compilatore si assicura che non si possano istanziare altri elementi al di fuori di quelli dichiarati nell’enumerazione.
* La compatibilità all’indietro (backward compatibility) è una caratteristica fondamentale di Java che garantisce che il codice scritto per versioni precedenti della piattaforma continui a funzionare anche su versioni più recenti della JVM, senza richiedere modifiche. La compatibilità all’indietro è uno dei motivi principali per cui Java è ampiamente utilizzato in contesti enterprise e in sistemi di lunga durata.
Perché usare enumerazioni
Uno dei principali vantaggi delle enumerazioni è la possibilità di rappresentare in modo sicuro un insieme limitato di valori. Senza un’enum, si rischia di utilizzare stringhe o numeri “magici”, che possono introdurre errori difficili da individuare. Consideriamo questo esempio:
public class Bussola {
public void muovi(String direzione) {
String messaggio;
if (direzione.equals("NORD")) {
messaggio = "Ti muovi verso nord";
} else if (direzione.equals("SUD")) {
messaggio = "Ti muovi verso sud";
} else if (direzione.equals("OVEST")) {
messaggio = "Ti muovi verso ovest";
} else if (direzione.equals("EST")) {
messaggio = "Ti muovi verso est";
} else {
messaggio = "Direzione non valida: " + direzione;
}
System.out.println(messaggio);
}
}
In questo approccio è possibile passare qualunque stringa, anche una non valida. Per esempio il valore del parametro direzione potrebbe essere "nord", oppure "Nord", ma dovrebbe essere "NORD" affinché il metodo funzioni correttamente. Il compilatore non può aiutarci a prevenire errori.
Nel codice seguente utilizziamo l’enumerazione PuntoCardinale per eliminare del tutto i valori arbitrari e delegare al compilatore il controllo sui valori ammessi:
public class Bussola {
public void muovi(PuntoCardinale direzione) {
String messaggio = switch (direzione) {
case NORD -> "Ti muovi verso nord";
case SUD -> "Ti muovi verso sud";
case OVEST -> "Ti muovi verso ovest";
case EST -> "Ti muovi verso est";
};
System.out.println(messaggio);
}
}
In questo modo:
- Il parametro
direzionepuò assumere solo i valori definiti nell’enumerazione. - Non è possibile specificare un valore non valido: un errore sarebbe rilevato in compilazione.
- L’espressione
switchdeve essere esaustiva**, quindi il compilatore richiede che tutte le alternative siano gestite per compilare senza errori.
Le enumerazioni rendono il codice più sicuro e leggibile, poiché evitano l’uso di valori “magici” o stringhe arbitrarie per rappresentare concetti che hanno un numero limitato di alternative.
** Per conoscere il concetto di esaustività che riguarda le espressioni
switchintrodotte nella versione 14 di Java potete consultare l’articolo intitolato “Il nuovo switch”.
Enumerazioni ed ereditarietà
Abbiamo visto che il compilatore trasforma l’enum PuntoCardinale in una classe con i suoi elementi implicitamente dichiarati public, static e final. Non abbiamo ancora detto, però, che tale classe:
- È dichiarata essa stessa
final. Ciò implica che non è possibile estendere enumerazioni. - Estende la classe generica
java.lang.Enum. Di conseguenza non può estendere altre classi (può però sempre implementare interfacce).
In pratica, la dichiarazione della classe PuntoCardinale sarà simile alla seguente:
public final class PuntoCardinale extends Enum<PuntoCardinale> {
// resto del codice omesso
}
Quindi non possiamo creare gerarchie di enumerazioni come facciamo con le classi.
Inoltre, ereditiamo in tutte le enumerazioni:
- I metodi dichiarati nella classe
Enum. - I metodi dichiarati e le proprietà delle interfacce
SerializableeComparableche sono implementate daEnum. - I metodi dalla classe
Object.
Tuttavia, nell’ultimo paragrafo di questo tutorial vedremo come è possibile in qualche modo estendere un’enumerazione.
Metodi ereditati dalla classe Enum
Estendendo Enum, le enumerazioni ereditano alcuni metodi:
name: restituisce il nome dell’elemento sotto forma di stringa (non si può fare override perché è dichiaratofinal).toString: restituisce lo stesso valore diname, ma può essere sovrascritto.ordinal: restituisce la posizione dell’elemento nell’enumerazione partendo dall’indice 0 (è un metodo dichiaratofinal).valueOf: metodo statico che prende in input unaStringe restituisce l’elemento dell’enumerazione corrispondente al nome.values: metodo statico non presente realmente nella classejava.lang.Enum, ma che viene generato dal compilatore per ogni enumerazione. Restituisce un array contenente tutti gli elementi dell’enumerazione nell’ordine in cui sono dichiarati.
Per esempio, il metodo name è definito in modo tale da restituire il nome dell’elemento, per cui:
System.out.println(PuntoCardinale.SUD.name()); // stampa SUD
stamperà la stringa "SUD".
Anche il metodo equivalente toString restituisce il nome della enum, quindi l’istruzione:
System.out.println(PuntoCardinale.SUD); // stampa SUD
produce esattamente lo stesso risultato (visto che il metodo println chiama il metodo toString sull’oggetto in input). La differenza sta nel fatto che il metodo name non si può riscrivere perché dichiarato final, ma possiamo sempre riscrivere il metodo equivalente toString.
Enum dichiara anche un metodo complementare a toString: il metodo statico valueOf. Esso prende in input una String e ritorna il corrispondente valore dell’enumerazione. Per esempio:
PuntoCardinale puntoCardinale = PuntoCardinale.valueOf("NORD");
System.out.println(puntoCardinale == PuntoCardinale.NORD); // stampa true
stamperà "true".
Il metodo speciale statico values restituisce un array contenente tutti gli elementi dell’enumerazione nell’ordine in cui sono stati dichiarati. È possibile sfruttare questo metodo per iterare sui valori di un’enumerazione che non conosciamo. Per esempio, sfruttando un ciclo for migliorato, possiamo stampare i contenuti dell’enumerazione PuntoCardinale, introducendo anche il metodo ordinal:
for (PuntoCardinale pc : PuntoCardinale.values()) {
System.out.println(pc + "\t è in posizione " + pc.ordinal());
}
Notare che il metodo ordinal (anch’esso dichiarato final), ritorna la posizione di un elemento all’interno dell’array restituito dal metodo values. L’output del precedente esempio è quindi:
NORD è in posizione 0 SUD è in posizione 1 OVEST è in posizione 2 EST è in posizione 3
Per completezza, la classe Enum in realtà dichiara altri due metodi meno interessanti come:
getDeclaringClass: che restituisce l’oggettoClassassociato al tipoenumcui appartiene il valore su cui il metodo è invocato.describeConstable: un metodo introdotto in Java 12 per supportare la costantizzazione avanzata delle API di basso livello. Si tratta di un’API specialistica non usata nello sviluppo applicativo tradizionale.
Metodi e proprietà ereditati dalle interfacce Serializable e Comparable
La classe Enum implementa le interfacce Serializable e Comparable, e di conseguenza gli oggetti di tipo enumerazione hanno le proprietà di poter essere serializzati e confrontati.
Mentre l’interfaccia marker Serializable non contiene metodi, l’interfaccia funzionale Comparable fa coincidere l’ordine naturale degli elementi di un’enumerazione con l’ordine in cui gli elementi sono definiti nell’enumerazione stessa. Questo significa che l’unico metodo astratto compareTo dell’interfaccia Comparable stabilisce l’ordine di due oggetti di un’enumerazione basandosi sull’ordine della posizione dell’oggetto all’interno dell’enumerazione.
Notare che il metodo compareTo è dichiarato final nella classe Enum, e quindi non è possibile riscriverlo nelle nostre enumerazioni.
Metodi ereditati dalla classe Object
Ereditiamo nelle enumerazioni tutti gli 11 metodi della classe Object.
In particolare, come abbiamo già detto possiamo fare override del metodo toString.
Gli altri metodi di cui solitamente facciamo override come equals, hashCode e clone, invece, sono dichiarati final e quindi non si possono riscrivere.
Infatti, per confrontare due istanze di tipo enumerazione basta utilizzare l’operatore ==, visto che si tratta di costanti e quindi non occorre ridefinire i metodi equals e hashCode.
Inoltre, le enumerazioni non si possono clonare, dovendo i suoi elementi essere le uniche istanze possibili. Anche in questo caso, la classe java.lang.Enum rende final il metodo clone ereditato da Object. Essendo dichiarato anche protected, non è neanche visibile al di fuori del package java.lang di Enum.
Personalizzazione di una enumerazione
Essendo trasformata in classe, un’enumerazione può dichiarare tutto ciò che possiamo dichiarare in una classe (metodi, variabili, tipi innestati, inizializzatori ecc.) con qualche vincolo sui costruttori. Infatti, essi verranno implicitamente considerati privati, e possono essere utilizzati solo dagli elementi dell’enumerazione con una sintassi speciale. La compilazione invece fallirà se proviamo a creare nuove istanze utilizzando l’operatore new.
Per esempio, con il seguente codice viene ridefinita l’enumerazione PuntoCardinale:
public enum PuntoCardinale {
NORD("nord"), // invoca il costruttore 2
SUD("sud"), // invoca il costruttore 2
OVEST("ovest"), // invoca il costruttore 2
EST; // invoca il costruttore 1
// equivalente a EST()
// variabile d’istanza
private String descrizione;
// costruttore numero 1
private PuntoCardinale() {
this("est"); // chiama il costruttore 2
}
// costruttore numero 2 (implicitamente private)
PuntoCardinale(String direzione) {
setDescrizione("direzione " + direzione);
}
public String getDescrizione() {
return descrizione;
}
public void setDescrizione(String descrizione) {
this.descrizione = descrizione;
}
@Override
public String toString() {
return "Puntiamo in " + descrizione;
}
}
Possiamo notare che:
- Abbiamo dichiarato due costruttori nell’enumerazione: uno esplicitamente privato e l’altro implicitamente privato: non è possibile dichiarare costruttori con
publicoprotected. Per il resto valgono le regole applicate ai costruttori delle classi. Come per le classi anche per le enumerazioni, se non inseriamo costruttori, il compilatore ne aggiungerà uno per noi senza parametri (il costruttore di default). Sempre come per le classi, il costruttore di default non verrà inserito nel momento in cui ne inseriamo noi uno esplicitamente, come nell’esempio precedente. - Gli elementi dell’enumerazione invocano i due costruttori che abbiamo dichiarato con una sintassi speciale. Nella dichiarazione degli elementi
NORD,SUDeOVESTabbiamo aggiunto una coppia di parentesi tonde a cui abbiamo passato un parametro di tipo stringa. Questo fa sì che nell’atto di creazione di queste istanze venga chiamato il costruttore numero 2 che prende come parametro unaString. Invece, l’elementoESTnon usando parentesi va a chiamare il costruttore senza parametri. Notare che avremmo anche potuto aggiungere una coppia di parentesi vuote per ottenere lo stesso risultato. - La dichiarazione degli elementi dell’enumerazione deve sempre precedere tutte le altre dichiarazioni. Se anteponessimo una qualsiasi dichiarazione alla lista degli elementi, otterremo un errore in compilazione. Notare che il punto e virgola dopo la lista degli elementi è facoltativo nel caso non ci siano altri membri dichiarati.
Implementare interfacce nelle enumerazioni
È anche possibile far implementare una o più interfacce a un’enum.
Per esempio consideriamo le seguenti interfacce:
public interface Identificabile {
String getCodice();
}
public interface Orientabile {
boolean isOrizzontale();
}
Modifichiamo l’enumerazione PuntoCardinale implementando le due interfacce appena presentate:
public enum PuntoCardinale implements Identificabile, Orientabile {
NORD, SUD, OVEST, EST;
@Override
public String getCodice() {
return name().substring(0, 1);
}
@Override
public boolean isOrizzontale() {
return this == EST || this == OVEST;
}
}
Ora potremo utilizzare il polimorfismo per dati tramite le interfacce
Identificabile e Orientabile:
Identificabile id = PuntoCardinale.NORD;
Orientabile o = PuntoCardinale.OVEST;
System.out.printf("Codice di %s = %s\n", id, id.getCodice());
System.out.printf("%s orientabile = %s\n", o, o.isOrizzontale());
che stamperà il seguente output:
Codice di NORD = N OVEST orientabile = true
Un’enumerazione può quindi implementare interfacce esattamente come una classe, fornendo un comportamento comune a tutti i suoi elementi.
Come vedremo alla fine di questo tutorial, tale comportamento può anche essere specializzato dai singoli valori dell’enumerazione.
Enumerazioni innestate statiche e import statici
Non è raro creare enumerazioni innestate, che a differenza delle classi innestate, sono sempre statiche. Per esempio, supponiamo di voler creare un’enumerazione che definisce i possibili tipi di account (per esempio "standard" e "premium") di clienti di un negozio online. Siccome il tipo di account è strettamente legato al concetto di account, sembra sensato dichiarare l’enumerazione Type innestata nella classe Account:
package com.online.shop.data;
public class Account {
public enum Type {STANDARD, PREMIUM} // enum innestata statica
// altro codice omesso ...
public static void main(String[] args) {
System.out.println(Type.PREMIUM); // accesso all'enumerazione statica
}
}
Se invece volessimo stampare un elemento dell’enumerazione dall’esterno della classe Account è possibile utilizzare la seguente sintassi:
System.out.println(Account.Type.PREMIUM);
Ovviamente, potremmo anche utilizzare gli import static all’occorrenza, per esempio così:
import static com.online.shop.data.Account.Type; // ... System.out.println(Type.PREMIUM);
O anche:
import static com.online.shop.data.Account.Type.PREMIUM; // ... System.out.println(PREMIUM);
Le enumerazioni e gli import static sono stati introdotti entrambi nella versione 5 di Java. Gli import static, in effetti, ci permettono di ridurre la verbosità quando usiamo le enumerazioni.
Estendere un’enumerazione
Sappiamo che non possiamo estendere un’enumerazione, tuttavia è possibile usare la sintassi delle classi anonime per ogni elemento, andando a ridefinire i metodi presenti nell’enumerazione. Si possono definire metodi nell’enumerazione e fare override di essi con i suoi elementi. Riscriviamo ancora una volta l’enumerazione PuntoCardinale:
public enum PuntoCardinale {
NORD {
@Override
public void test() {
System.out.println("metodo di NORD");
}
},
SUD, OVEST, EST;
public void test() {
System.out.println("metodo dell'enum");
}
}
Dove abbiamo definito un metodo chiamato test che stampa la stringa "metodo dell’enum". Però l’elemento NORD, con una sintassi simile a quella delle classi anonime dichiara anch’esso lo stesso metodo, sottoponendolo a override. Infatti, il compilatore tramuterà l’elemento NORD proprio in un’istanza di una classe anonima, che estenderà PuntoCardinale. Quindi l’istruzione:
PuntoCardinale.NORD.test();
stamperà:
metodo di NORD
mentre l’istruzione:
PuntoCardinale.SUD.test();
stamperà:
metodo dell'enum
perché SUD non ha fatto override di test. Otterremo lo stesso output chiamando il metodo test anche sugli elementi EST e OVEST.
Enumerazioni e polimorfismo
Dopo aver visto il rapporto tra enumerazioni ed ereditarietà, è quindi possibile utilizzare le enumerazioni sfruttando il polimorfismo in maniera più avanzata.
Per esempio, consideriamo la seguente interfaccia Operazione:
public interface Operazione {
boolean esegui(int a, int b);
}
Possiamo implementare tale interfaccia all’interno di un’enumerazione Confronto, personalizzando l’implementazione del metodo esegui per ciascun elemento:
public enum Confronto implements Operazione {
MAGGIORE {
public boolean esegui(int a, int b) {
return a > b;
}
},
MINORE {
public boolean esegui(int a, int b) {
return a < b;
}
},
UGUALE {
public boolean esegui(int a, int b) {
return a == b;
}
};
}
Con questa struttura possiamo scrivere codice come il seguente:
boolean risultato = Confronto.MAGGIORE.esegui(10, 5);
System.out.println("10 maggiore di 5 = " + risultato);
risultato = Confronto.MINORE.esegui(10, 5);
System.out.println("10 minore di 5 = " + risultato);
risultato = Confronto.UGUALE.esegui(10, 5);
System.out.println("10 uguale a 5 = " + risultato);
che produrrà il seguente output:
10 maggiore di 5 = true 10 minore di 5 = false 10 uguale a 5 = false
Implementare un’interfaccia in un’enumerazione permette di associare un comportamento a ciascun valore dell’enum e di sfruttare il polimorfismo, rendendo il codice più estensibile, leggibile e robusto.
Conclusioni
Le enumerazioni sono particolarmente utili quando:
- Il dominio dei valori è chiuso e noto a priori, come punti cardinali, stati di un oggetto, giorni della settimana, livelli di priorità e così via.
- Si vuole rendere il codice più sicuro, eliminando stringhe arbitrarie o numeri “magici”.
- Ogni elemento deve poter avere proprietà o metodi specifici, mantenendo chiarezza e leggibilità.
Sono invece meno adatte quando:
- Gli elementi possono variare dinamicamente nel tempo, per esempio se provengono da database o configurazioni esterne.
- Si vuole modellare una gerarchia estensibile di tipi, per cui classi e interfacce restano soluzioni più flessibili.
In questo articolo abbiamo visto che le enumerazioni non sono semplici liste di costanti, ma vere e proprie classi con istanze predefinite, metodi ereditati da Enum, la possibilità di implementare interfacce e perfino di ridefinire comportamenti per singoli valori attraverso classi anonime.
Questi aspetti rendono le enum uno strumento sorprendentemente potente e in alcuni casi inaspettato: un perfetto esempio delle Stranger Things in Java.
Note dell’autore
Questo articolo è basto su alcuni paragrafi del capitolo 4 e 7 del mio libro “Programmazione Java” e del mio libro in inglese “Java for Aliens”.
