Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Marco Faella » 22.Classi enumerate


Il problema dei tipi enumerati

Un’esigenza ricorrente nella programmazione consiste nel creare un tipo di dati che può assumere un numero fisso e limitato di valori. Ad esempio, un tipo di dati che rappresenta un giorno della settimana può assumere solo uno di 7 possibili valori, così come il seme di una carta da gioco può assumere 4 possibili valori. Supponiamo di dover implementare una classe Card, che rappresenta una carta da gioco francese. Ogni carta è caratterizzata da un valore, da 1 a 13, ed uno dei quattro semi (in Inglese, suit). Che tipo di dati utilizzare per questi due attributi? Una soluzione ingenua (e fortemente sconsigliata per motivi che vedremo in seguito) è la seguente:

public class Card {

private int val, suit;
...

}

Un’altra soluzione simile è:

public class Card {

private int val;
private String suit;
...

}

Il problema dei tipi enumerati (segue)

Nelle due implementazioni propopste nella slide precedente, si utilizza un tipo base per rappresentare il seme; in un caso un intero, nell’altro una stringa.
Così facendo, non riusciamo a distinguere una variabile che contenga un seme, da una che contiene un qualsiasi intero, o una stringa.
Ad esempio, se un metodo di Card come setSuit deve accettare un seme come argomento, deve necessariamente accettare un qualsiasi intero (o stringa), almeno in fase di compilazione.
Quindi, il compilatore non può accorgersi che una invocazione come card.setSuit(5) non ha senso.

Sarebbe preferibile disporre di un tipo di dati che preveda un insieme fissato di valori.
I principali linguaggi di programmazione offrono supporto specifico a questa esigenza.
Ad esempio, i linguaggi C/C++ offrono il costrutto enum. Fino alla versione 1.4, Java non offriva un simile supporto.
Tuttavia, anche in Java 1.4 è possibile simulare questo supporto.

Supponiamo di voler implementare la classe Card in Java 1.4.
Invece di utilizzare un tipo base per rappresentare il seme, creiamo una classe Suit apposita.
Il nostro scopo consiste nell’assicurarci che la classe Suit possa avere solo 4 istanze predefinite, e che ciascun riferimento di tipo Suit possa puntare solo ad una di queste 4 istanze (oltre che a null).
La prossima slide illustra la soluzione standard a questo problema, che prende il nome di Typesafe Enum Pattern.

Il Typesafe Enum Pattern

Il pattern suggerisce di implementare una classe, che chiameremo SuitClass per distinguerla dalla successiva versione SuitEnum, secondo il seguente schema:

public class SuitClass {

private SuitClass(String name) { this.name = name; }
public final String name;
public static final HEARTS = new SuitClass("Hearts");
public static final SPADES = new SuitClass("Spades");
public static final CLUBS = new SuitClass("Clubs");
public static final DIAMONDS = new SuitClass("Diamonds");

}

Riassumendo, le regole da seguire sono le seguenti:

  • La classe avrà solo costruttori privati;
  • Ogni possibile valore del tipo enumerato corrisponderà ad una costante pubblica di classe, cioè un campo public static final;
  • Ciascuna di queste costanti viene inizializzata usando uno dei costruttori privati.

Si noti che in questo caso specifico non è necessario fornire un metodo accessore per il campo immutabile “name”.

Osservazioni

Con la classe SuitClass abbiamo raggiunto lo scopo che ci eravamo prefissati: abbiamo creato un tipo di dati che può assumere solo uno di quattro valori possibili.
Infatti, il costruttore privato assicura che non sia possibile creare ulteriori istanze.

Il lettore attento potrebbe chiedersi perché non abbiamo riservato lo stesso trattamento anche all’attributo “valore” delle carte.
Anche in questo caso, si tratta di un attributo che può assumere un numero fisso e piuttosto limitato di valori validi: i numeri da 1 a 13.
Tuttavia, a differenza del seme, il valore di una carta è intrinsecamente un numero, e quindi rappresentarlo come tale presenta dei vantaggi, come la facilità di confrontare o sommare due valori.
Comunque, niente impedisce di trattare anche il valore allo stesso modo, definendo una ulteriore classe enumerata con 13 possibili valori; il tutorial ufficiale sulle enumerazioni Java suggerisce di procedere proprio così.

Le classi enumerate in Java 1.5

A partire dalla versione 1.5 di Java, il linguaggio offre supporto nativo ai tipi enumerati, tramite il concetto di classe enumerata.
Una classe enumerata è un tipo particolare di classe, introdotta dalla parola chiave enum, che prevede un numero fisso e predeterminato di istanze.
Continuando l’esempio dei semi, la corrispondente classe enumerata può essere definita come segue:

public enum SuitEnum {
HEARTS, SPADES, CLUBS, DIAMONDS;
}

Una classe enumerata può consistere semplicemente di un elenco di valori possibili.
Per molti versi, SuitEnum si comporta come SuitClass.
In particolare, i quattro valori dichiarati si utilizzano proprio come costanti pubbliche di classe, come ad esempio in:

SuitEnum s = SuitEnum.HEARTS;

Però, come vedremo, SuitEnum dispone di molte più funzionalità di SuitClass, offerte in modo del tutto automatico e trasparente per il programmatore.
A differenza di SuitClass, l’ordine in cui i valori sono definiti in SuitEnum è significativo.

Potenzialità delle classi enumerate

Una classe enumerata può essere molto più ricca della classe SuitEnum. Può contenere campi, metodi e costruttori, come una classe normale. Le uniche restrizioni riguardano i costruttori:  tutti i costruttori devono avere visibilità privata o default (di pacchetto); non è possibile invocarli esplicitamente con new, neanche all’interno della classe stessa. Ad esempio, consideriamo una classe enumerata che rappresenti i moduli della stazione spaziale orbitante internazionale (ISS). Ogni modulo è caratterizzato dalla sua massa (in kg) e dall’anno di messa in orbita.

public enum Module {

ZARYA(19300, 1998), UNITY(11600, 1998), ...;
private final double m;
private final int y;
private Module(double mass, int year) {

m = mass;
y = year;

}

}

In una classe enumerata, la prima riga deve sempre essere l’elenco dei valori possibili. Se una classe enumerata ha più costruttori, ciascun valore può essere costruito con un costruttore diverso.

Indici dei valori enumerati

Tutte le classi enumerate estendono automaticamente la classe parametrica Enum, quindi, le classi enumerate non possono estendere altre classi.
Precisamente, ogni classe enumerata E estende Enum<E>. Inoltre, le classi enumerate sono automaticamente final.

Ad ogni valore di una classe enumerata è associato un numero intero, chiamato indice, che rappresenta il suo posto nella sequenza dei valori, a partire da zero; nel caso di SuitEnum, a HEARTS è associato 0, a SPADES 1, e così via.
Esistono metodi per passare da indice numerico a valore enumerato, e viceversa.

Per passare da valore enumerato a indice, si usa il seguente metodo della classe Enum

public int ordinal()
Essendo pubblico, questo metodo viene ereditato da tutte le classi enumerate.

Per l’operazione inversa, si usa il seguente metodo statico, che ogni classe enumerata E possiede automaticamente (non appartiene alla classe Enum)

public static E[] values()
Il metodo restituisce un array contenente tutti i possibili valori di E.
Quindi, per ottenere il valore di posto i-esimo, è sufficiente accedere all’elemento i-esimo dell’array restituito da values.

Nomi dei valori enumerati

È anche possibile passare da un valore enumerato alla stringa che contiene il suo nome, come definito nel codice sorgente, e viceversa.

Per passare da valore a stringa, si usa il seguente metodo della classe Enum

public String name()

Restituisce il nome di questo valore enumerato.

Per il passaggio inverso, la classe Enum offre il seguente metodo statico parametrico

public static <T extends Enum <T>> T valueOf(Class<T> enumType, String name)

Restituisce il valore enumerato della classe enumType che ha nome name.
Il parametro di tipo del metodo rappresenta la classe enumerata a cui lo si applica.
Esempio:

SuitEnum x = Enum.<SuitEnum>valueOf(SuitEnum.class, "HEARTS");

Specializzazione dei valori enumerati

È possibile specializzare il comportamento di un valore enumerato rispetto agli altri valori della stessa enumerazione.
In particolare, è possibile che un valore enumerato abbia una versione particolare di un metodo che è comune a tutta l’enumerazione.
Ad esempio, consideriamo una classe enumerata BoolOp, che rappresenta i principali operatori booleani (AND e OR).
Vogliamo dotare la classe di un metodo eval, che accetta due valori booleani e calcola il risultato dell’operatore applicato a questi valori. Per farlo, è possibile utilizzare la seguente sintassi:

public enum BoolOp {

AND { public boolean eval(boolean a, boolean b) { return a && b; } },
OR { public boolean eval(boolean a, boolean b) { return a || b; } };
public abstract boolean eval(boolean a, boolean b);

}

Per specializzare il comportamento di un valore, si inserisce il codice relativo subito dopo la dichiarazione di quel valore, racchiuso tra parentesi graffe.
Le versioni specializzate di eval rappresentano un overriding del metodo astratto eval.
Le enumerazioni possono avere metodi astratti pur non essendo astratte esse stesse.
Si deve considerare questo costrutto equivalente alla creazione di una classe anonima che estende l’enumerazione stessa, come illustrato nella prossima slide.

Specializzazione come classe anonima

Abbiamo già visto che un’enumerazione come: public enum MyEnum { A, B, C; } è molto simile alla seguente classe:
public class MyEnum {

public static final MyEnum A = new MyEnum();
...

}

Un valore specializzato, come ad esempio: public enum MyEnum { A { /* codice specifico per A */ }, B, C; } va interpretato alla stregua di:
public class MyEnum {

public static final MyEnum A = new MyEnum() {

/* codice specifico per A */

};

...

}
Il codice specifico di un valore si comporta come se fosse inserito in una apposita classe anonima. Infatti, nell’ambito del codice specifico di un certo valore è possibile inserire tutti i costrutti che possono comparire in una classe anonima, come campi e metodi.

Collezioni per tipi enumerati

Il Java Collection Framework offre delle collezioni specificamente progettate per le classi enumerate.
Si tratta di EnumSet, versione specializzata di Set, ed EnumMap, versione specializzata di Map.
EnumSet è una classe che implementa Set ed è ottimizzata per contenere elementi di una classe enumerata.
La sua intestazione completa è

public abstract class EnumSet<E extends Enum <E>> extends AbstractSet<E>

implements Cloneable, Serializable

Il limite superiore del parametro di tipo impone che tale tipo estenda Enum di se stesso
Questo requisito è soddisfatto da tutte le classi enumerate.

Internamente, un EnumSet è un vettore di bit (bit vector).
Ovvero, se un EnumSet dovrà contenere elementi di una classe enumerata che prevede n valori possibili, esso conterrà internamente un vettore di n valori booleani.
L’i-esimo valore booleano sarà vero se i-esimo valore enumerato appartiene all’insieme, e falso altrimenti.
Questa rappresentazione permette di realizzare in modo estremamente efficiente (tempo costante) tutte le operazioni base sugli insiemi previste dall’interfaccia Collection (add, remove, contains).
È facile rendersi conto che questa tecnica implementativa non potrebbe funzionare su classi non enumerate, che non hanno un numero prefissato e limitato di valori possibili.

Creare un EnumSet

Il discorso della slide precedente implica che un EnumSet deve conoscere il numero di valori possibili che ospiterà.
Questa necessità comporta che la creazione di un EnumSet segua un percorso un pò tortuoso.

Essendo astratta, la classe EnumSet non ha nessun costruttore pubblico.
Per istanziarla, si utilizzano dei metodi statici, chiamati in gergo metodi factory.
Uno di questi metodi è il seguente

public static <E extends Enum <E>> EnumSet<E> noneOf(Class<E> elemType)

Questo metodo crea un EnumSet vuoto, predisposto per contenere elementi della classe enumerata elemType.
Passare l’oggetto di tipo class corrispondente alla classe enumerata permette all’EnumSet di conoscere il numero di valori possibili che ospiterà.

Ad esempio:

Collection <SuitEnum>c = EnumSet.noneOf(SuitEnum.class);

Possiamo affidarci alla type inference per dedurre il parametro di tipo appropriato (cioè, E = SuitEnum).

EnumMap

EnumMap è una classe che implementa Map, ed è ottimizzata per i casi in cui le chiavi appartengano ad una classe enumerata.
Internamente, una EnumMap con valori di tipo V è semplicemente un array di riferimenti di tipo V.
Le chiavi vengono convertite in indici nell’array.
La sua intestazione completa è

public class EnumMap<K extends Enum <K>,V> extends AbstractMap<K,V>

implements Serializable, Cloneable

A differenza di EnumSet, EnumMap ha costruttori pubblici. Il più semplice costruttore di EnumMap è il seguente

public EnumMap(Class<K> keyType)

Questo metodo crea una EnumMap vuota, predisposta per contenere chiavi della classe enumerata keyType. Ad esempio:

Map<SuitEnum,Integer> m = new EnumMap<SuitEnum,Integer>(SuitEnum.class);

Esercizio (esame 19/6/2009)

Implementare l’enumerazione Cardinal, che rappresenta le 16 direzioni della rosa dei venti, chiamate N (per Nord), NNE (per Nord Nord Est), NE, ENE, E, etc.

Il metodo isOpposite prende come argomento un punto cardinale x e restituisce vero se questo punto cardinale è diametralmente opposto ad x, e falso altrimenti.

Il metodo statico mix prende come argomento due punti cardinali, non opposti, e restituisce il punto cardinale intermedio tra i due. Se i due punti cardinali sono opposti, viene lanciata un’eccezione.

Esempio d’uso:
Cardinal nord = Cardinal.N;
System.out.println(nord.isOpposite(Cardinal.S));
Cardinal nordest = Cardinal.mix(Cardinal.N, Cardinal.E);
assert nordest==Cardinal.NE : "Errore inaspettato!";
Cardinal nordnordest = Cardinal.mix(nordest, Cardinal.N);
System.out.println(nordnordest);

Output dell’esempio d’uso:
true
NNE

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion