Il
problema degli insiemi
Durante la programmazione emerge spesso la necessità
di definire insiemi omogenei di costanti, che
denotano insiemi presenti nella realtà.
Alcuni esempi sono i giorni della settimana (lunedì,
martedì, mercoledì e così
via), i punti cardinali (nord, sud, est, ovest),
le stagioni (autunno, inverno, primavera, estate)
e così via. Dal momento che l'iterazione
è uno dei costrutti fondamentali della
programmazione, non deve stupire che molti programmi
abbiano la necessità di iterare tra simili
insiemi per effettuale calcoli contabili, statistici
e quant'altro.
Fino
ad ora la maniera più usata per definire
insiemi prevedeva l'uso di costanti numeriche:
public
class Stagioni {
public static final int AUTUNNO = 0;
public static final int INVERNO = 1;
public static final int PRIMAVERA = 2;
public static final int ESTATE = 3;
.
}
La
creazione di insiemi mediante l'uso di costanti
numerica prevede una chiamata esplicita alla variabile
statica della classe contenitrice. Nel seguente
esempio si immagina l'uso di una classe Viaggio
che richiede come parametri di costruttore due
costanti di tipo Nazione e uno di tipo Stagioni:
Viaggio
v = new Viaggio(Nazioni.ITALIA,Nazioni.USA,Stgioni.ESTATE);
Questa
implementazione presenta numerosi svantaggi: anzitutto
non è tipizzata, dal momento che alla fine
il valore reale delle costanti è un numero
intero: se nella precedente chiamata si inverte
in qualche modo l'ordine delle costanti, il compilatore
non segnalerà alcun errore, e il programma
avrà un'esecuzione indefinita. In secondo
luogo non è possibile estendere o modificare
l'ordine delle costanti senza essere costretti
ricompilare il codice client, che continuerà
a funzionare, anche se in modo diverso dal previsto,
dal momento che le costanti vengono compilate
come semplici numeri interi. Infine la stampa
dei valori non contiene nessuna informazione riguardo
al valore simbolico della costante rappresentata.
Molti
linguaggi, tra cui Pascal, Visual Basic e C#,
prevedono un costrutto apposito per definire insiemi,
ma la loro reale implementazione non è
poi tanto diversa da quella descritta qui sopra.
Java
5, al contrario, prevede un costrutto enum particolarmente
completo ed interessante, che verrà analizzato
nei prossimi paragrafi.
Le
enums in Java
La parola chiave enum permette di definire insiemi
ricorrendo ad una sintassi molto semplice:
enum
GiorniDellaSettimana {lunedì, martedì,
mercoledì, giovedì, venerdì,
sabato, domenica}
enum PuntiCardinali {nord, sud, est, ovest}
enum Stagioni {autunno, inverno, primavera, estate}
Le
enum appena definite sono dei veri e propri tipi,
che è possibile usare nei parametri di
un metodo, per evitare i problemi descritti nel
paragrafo precedente:
class
Viaggio {
public Viaggio(Destinazione partenza, Destinazione
arrivo, Stagioni stagione) {
}
...
}
Ma
le enums in Java hanno altre proprietà,
legate al fatto che esse vengono implementate
con vere e proprie classi, che supportano la serializzazione
e la comparazione attraverso l'interfaccia Comparable.
La serializzazione garantisce la compatibilità
all'indietro qualora venga modificato l'ordine
o il numero di elementi di una enum. Si provi
a modificare la enum PuntiCardinali descritta
precedentemente nel seguente modo, aggiungendo
elementi e cambiando l'ordine di quelli esistenti:
enum
PuntiCardinali {nord, nordest, est, sudest,sud,
sudovest, ovest,nordovest}
Non
solo i programmi che ne fanno uso continueranno
a funzionare perfettamente senza bisogno di ricompilazione,
ma nemmeno le eventuali occorrenze presenti in
oggetti serializzati perderanno valore.
Ogni
costante è presente in singola copia in
memoria, per cui è possibile comparare
due valori anche usando l'operatore ==, molto
più efficiente del metodo equals(). Il
metodo toString() restituisce il valore simbolico
della costante, che può essere quindi stampato
in report o tabelle. Nei prossimi paragrafi analizzeremo
ulteriori aspetti interessanti delle enums.
Il
metodo values()
Ogni enum presenta un metodo statico values()
che restituisce un array contenente tutti i valori
del tipo enum nell'ordine in cui sono stati definiti.
In questo modo diventa estremamente facile iterare
tra gli elementi di una enum per effettuare una
elaborazione, specie ricorrendo al nuovo costrutto
foreac di Java 5 (cfr [1]):
for(GiorniDellaSettimana
g : GiorniDellaSettimana.values())
System.out.println(g);
Si
noti la comodità di poter stampare direttamente
il valore simbolico della costante. Oltre al metodo
values(), ogni enum presenta i propri elementi
come variabili static final, un fattore che, come
vedremo più avanti, può rivelarsi
utile in molti frangenti.
Enum
provviste di comportamento
E' anche possibile, ricorrendo ad una sintassi
molto simile a quella di una normale classe, definire
una enum i cui elementi posseggono metodi ed attributi:
//
Trenta giorni ha Novembre, con April, Giugno e
Settembre
public enum Mesi {
gennaio(31), febbraio(28), marzo(31),
aprile(30),maggio(31),giugno(30),
luglio(31),agosto(31),settembre(30),
ottobre(31),novembre(30),dicembre(31);
private final int giorni; // attributo
Mesi(int giorni) { // costruttore
this. giorni = giorni;
}
public int getGiorni() { // metodo
return giorni;
}
}
In
questo esempio l'enum Mesi denota un tipo caratterizzato
dai valori gennaio, febbraio, marzo, aprile, maggio,
giugno, luglio, agosto, settembre, ottobre, novembre,
dicembre. Ciascun elemento dell'insieme è
a sua volta caratterizzato da un differente valore
dell'attributo giorni, che può essere letto
attraverso il metodo getGiorni(), comune a tutti
gli elementi dell'insieme:
for
(Mesi m : Mesi.values())
System.out.println("Il mese" + m + "ha
"+m.getGiorni()+" giorni");
In
pratica una enum permette di definire, con un
costrutto compatto, una classe, i suoi attributi,
i suoi metodi e l'insieme di tutte le sue possibili
istanze che, come già precisato, ha cardinalità
finita.
Metodi
constant-specific
In certe circostanze si desidera fare in modo
che ogni costante dell'insieme disponga di un
comportamento specifico di un metodo comune. In
questo caso è sufficiente definire abstract
il metodo e definire una versione del metodo specifica
per ogni elemento.
Ecco un esempio:
enum
MyEnum {
primoElemento
{
metodo() {
// prima implementazione
}
},
secondoElemento {
metodo() {
// seconda implementazione
}
}
...
abstract
void metodo();
}
EnumSet
ed EnumMap
A supporto delle enum sono stati aggiunte due
classi al package java.util: EnumSet ed EnumMap.
EnumSet è un'implementazione dell'interfaccia
Set specifica per fornire elevate prestazioni
con le enum. Internamente lavora su singoli bit:
spesso un'unica variabile long è sufficiente
a tenere traccia di tutti gli elementi di una
enum, dal momento che è difficile che vengano
definiti insiemi con più di 64 valori.
EnumSet viene usata principalmente per iterare
tra un sottoinsieme dei valori di un insieme,
grazie al metodo statico range():
static
<E extends Enum<E>> EnumSet<E>
range(E from, E to)
La
sintassi di questo metodo è un po' complessa,
a seguito dell'uso di tipi parametrici nidificati:
per chiarire le cose ' sufficiente precisare che
il tipo Enum<E> è la classe da cui
discendono tutte le enum. L'uso pratico risulta
abbastanza semplice, dal momento che il tipo in
questione viene dedotto in maniera automatica
dal tipo dei parametri; si provi ad immaginare
un frammento di un programma che deve elaborare
buste paga e che pertanto deve lavorare solo sui
giorni feriali trascurando i giorni festivi:
for(GiorniDellaSettimana
g : EnumSet.range(GiorniDellaSettimana.lunedì,
GiorniDellaSettimana.venerdì))
calcolaBustaPaga(g);
Come
si vede il tipo parametrico viene dedotto dai
parametri, che devono essere due elementi della
stessa enum. Si noti che gli elementi vengono
presi come membri statici direttamente dalla enum
di appartenenza. Un altro metodo interessante
il metodo copyOf(), che restituisce un EnumSet
a partire dagli elementi presenti in una Collection
che a sua volta deve contenere elementi appartenenti
ad una enum preesistente:
static
<E extends Enum<E>> EnumSet<E>
copyOf(Collection<E> c)
La
classe EnumMap è una speciale realizzazione
dell'interfaccia Map, che permette di creare mappe
hash le cui chiavi appartengono ad una enum. L'efficienza
è garantita da un'implementazione interna
che sfrutta adeguatamente un array per effettuare
le dovute associazioni: pertanto ne viene consigliato
l'utilizzo in tutti quei casi in cui sia necessario
creare mappe hash basate su enum.
Conclusioni
Questo mese abbiamo imparato a sfruttare a fondo
il nuovo costrutto enum presente in Java 5. Dopo
aver visto gli usi più banali, abbiamo
illustrato la definizione di enum dotate di comportamento
e alcune classi di supporto utili nella gestione
delle enum stesse. Nel prossimo articolo tratteremo
le annotation, un ulteriore nuovo costrutto di
Java5.
Bibliografia
[1] Andrea Gini, "Java2 Standard Edition
1.5 beta 1", Mokabyte 3/2004
http://www.mokabyte.it/2004/03/jdk1_5.htm
|