Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
L’utilizzo di oggetti immutabili in Java si rivela essere un utile tecnica di ottimizzazione in svariati
contesti, oltre ad offrire un concreto utilizzo di meccanismi come il cloning e lo sharing di risorse.
Oggetti immutabili
di Michele Arpaia
Come è stato ben chiarito già ai primordi del movimento dei pattern di design [1], la scelta del
linguaggio di programmazione che si andrà ad utilizzare per “codificare” i pattern è una forza del
contesto che va bilanciata. Come dire, lo stesso pattern ha diverse incarnazioni perché deve dar
conto ai costrutti che il linguaggio offre. Un pattern realizzato mediante un linguaggio di
programmazione prende il nome di idioma. In questa articolo e nel prossimo prenderò in esame un
idioma molto famoso presentando varie tecniche per realizzarlo, tecniche che di volta in volta ne
daranno una caratterizzazione diversa in maniera tale da avere un campo di applicabilità molto
vasto.
Definizione
Un oggetto è detto immutabile se lo stato che lo rappresenta non può essere cambiato dopo la sua
creazione. Un noto esempio di oggetto immutabile è la classe java.lang.String. Una volta
inizializzata la classe String non subirà variazioni di stato. Così ad esempio se String
s=”immutabile”; il metodo seguente non altera il valore originale di s:
Al metodo viene passata una copia del reference ad s, ma la classe String non ha metodi che
alterino lo stato interno (mutators methods). Più precisamente i metodi che lavorano sullo stato
della stringa restituiscono un oggetto dello stesso tipo con lo stato aggiornato.
Un altro esempio di oggetti immutabili presenti nelle API Java sono le classi wrapper per i tipi
primitivi.
Contesto
Generalmente gli oggetti immutabili sono piccoli, hanno poche associazione con altri oggetti e
pongono l’accento più sul contenuto (value based objects) che sull’identità dell’oggetto
consentendo un potenziale resource sharing. Inoltre simulano in maniera semplice il passaggio per
valore, utile in molti casi per migliorare l’occultamento dei dati.
Un altro campo di applicabilità è dato dagli ambienti multithread ove gli oggetti immutabili sono la
garanzia di thread-safe. Dato che non è possibile cambiare lo stato dell’oggetto, uno o più thread
possono solo leggere lo stato evitando di sincronizzare le risorse con un notevole risparmio e
guadagnando in performance. Come sempre è il progettista che pesa opportunamente la
convenienza o meno di una siffatta soluzione, tenendo anche presente che alcune tecniche per
realizzare l’immutability possono risultare pesanti (come ad esempio il cloning, che vedremo
successivamente).
Un altro vantaggio di avere oggetti immutabili è dato dal loro utilizzo nelle collezioni ordinate, ad
esempio il TreeSet. Se inseriamo oggetti Account in questa collezione (avendo opportunamente
implementato l’interfaccia Comparable) questi saranno mantenuti ordinati secondo il criterio scelto,
che in questo caso semplice potremmo considerare l’ordinamento sul nome. Dato che Account è un
oggetto mutabile, provando a modificare lo stato di un oggetto già inserito nella collection
(attraverso il metodo setNome(), ad esempio), questa ultima non si accorgerà del nuovo stato che
potrebbe richiedere un riordino degli elementi. Il problema si risolverebbe alla radice se Account
fosse immutabile.
Soluzione
Possiamo stendere delle condizioni necessarie per la costruzioni di oggetti affinché questi risultino
immutabili. Queste sono:
E’ chiaro che Resource pur costruita per essere immutabile di fatto si trova a diventare mutabile a
causa di Account. Il problema è che Resource riceve in input la copia del reference all’oggetto
Account e non la copia dell’oggetto. Giacché ci siamo, è utile notare che neppure qualificando final
il parametro Account del costruttore di Resource risolverebbe il problema.
Il cloning ci viene di aiuto e dato che ogni buon sviluppatore Java conosce bene la differenza tra
shallow copy e deep copy tratterò questi due casi separatamente senza soffermarmi troppo sul loro
significato (si veda comunque la Figura 1)
Shallow Copy
Riprendiamo la classe Resource e modifichiamo il costruttore come segue:
La “nativa” shallow copy effettua una copia bit a bit dei campi dell’oggetto in esame. Naturalmente
per sfruttare questo meccanismo nativo dobbiamo rendere Cloneable la classe Account e
implementare il metodo clone come segue:
Stiamo assumendo che la classe Account utilizzi (per il suo stato) oggetti tutti clonabili.
Riprenderemo questo punto più avanti quando parleremo del deep copy. In alcuni casi potrebbe
essere più pratico mettere a fattor comune il metodo clone() in una apposita classe astratta per poi
estenderla.
Prima di chiudere con questa tecnica credo sia utile richiamare un altro aspetto spesso sottovalutato
e causa di sottili errori. Consideriamo di aggiungere alla classe Resource un ulteriore costruttore che
prenda in input un insieme di account ad esso associato (naturalmente esisterà il metodo
getAccounts() che restituisce la lista degli account associati). Potremmo facilmente utilizzare un
ArrayList, dunque:
L’output risulta:
User
Guest
Non proprio secondo le nostre aspettative. Cosa è successo? Semplicemente il clone di ArrayList
funziona secondo shallow copy dunque è stata fatta una copia dei reference degli oggetti contenuti
(questo vale anche per gli array). Nel prossimo paragrafo risolveremo il problema attraverso il deep
copy.
Deep Cloning
Un modo semplice per risolvere il problema precedente è quello di utilizzare l’ereditarietà. Si
costruisce una nuova classe derivata da ArrayList e si implementa il metodo clone che costruisce
una nuova lista di elementi clonati. E’ un approccio poco percorribile (esempio di cattivo uso di
ereditarietà) in quanto costringe a creare una classe artificiale utile solo per il cloning. Inoltre la
classe Resource cambia interfaccia e questo impatta su tutti i client (violazione del Open/Closed
Principle), e infine la sottoclasse non è sostituibile nei contesti in cui appare la superclasse poiché
aggiunge metodi ad hoc.
Un approccio migliore è quello del deep coping sulla classe Resource. Quando questa prende in
input (dai costruttori) il parametro ArrayListt, si preoccupa di farne una copia “profonda” in un
metodo private. Questo metodo potrebbe avere la seguente forma:
Questo metodo è invocato al momento della costruzione della classe Resource e nei metodi getter.
Da notare che la classe Account potrebbe aver implementato un deep copy.
1. Definire una interfaccia con tutti i query metodi pubblici della classe Account
Il punto tre va applicato con molta cautela poiché non stiamo trasformando una classe da mutabile
ad immutabile, stiamo semplicemente aggiungendo una vista immutabile a tale classe. Quindi
questo significa che all’interno del sistema ci saranno parti di codice che utilizzano Resource come
mutabile ed altri che necessitano ora di averla come immutabile. Ovviamente è questo ultimo caso
da prendere in considerazioni nella sostituzione dei riferimenti suggeriti dal passo tre.
Riconsideriamo l’esempio visto sopra e applichiamo questa tecnica in luogo del cloning.
L’interfaccia sarà così definita:
Applicando il passo tre nella classe Resource modifichiamo il costruttore in questo modo:
Ora Resource - che era già immutabile – conserva questa caratteristica poiché lavora solo con la
versione immutabile di Account. Un tipico esempio di utilizzo potrebbe essere:
(Account)acc.setName(“User”);
che viola palesemente il principio di immutabilità che si vuole raggiungere. Resta comunque utile
poiché non introduce – come il cloning – un eccessivo overhead ed è molto più semplice da
realizzare aiutando anche alla costruzione di qualche astrazione eventualmente utile al design. Si
rivela particolarmente pratica nel caso dei metodi query che in questo caso, restituendo una versione
immutabile degli oggetti che rappresentano il proprio stato, aiuta a raggiungere un buon grado di
“information hiding”[2]
Considerazioni su ROI
Questa tecnica si offre a molte utili estensioni. Un enhancement è quello di combinare questa
tecnica con quella del cloning. E’ sufficiente aggiungere nell’interfaccia ROI la firma del metodo
clone ed obbligare la sottoclasse ad implementarlo. In questo modo si ha la libertà di scegliere se un
oggetto passato immutabile ci serve comunque utilizzarlo come mutabile.
In alcuni casi risulta più conveniente affiancare all’oggetto immutabile uno mutabile, avere così
due versioni concrete di una stessa entità.Un modo elegante è quello di definire una classe che
implementa il comportamento standard, quindi mutabile. Questo oggetto delega il comportamento
dei metodi query alla classe immutabile tramite l’interfaccia ROI. Ad esempio, potrebbe esistere
l’interfaccia ROI che chiamiamo ImmutabileAccount implementata da Account, cosicché questa
classe è immutabile per definizione. Volendo definire un account mutabile, si costruisce una classe
AccountMutable che delega, in questo semplice caso, l’esecuzione del metodo getName() alla classe
immutabile tramite l’interfaccia ROI. Naturalmente questo tipo di associazione potrebbe
completamente non esistere (si pensi a String e StringBuffer). Ad ogni modo abbiamo due reali
versioni di un oggetto e questo evita il downcasting che era il tallone di Achille della tecnica ROI.
Si noti che lo stesso risultato si raggiunge attraverso l’ereditarietà, definendo cioè le due classi
(mutabile ed immutabile) come sottoclassi di ROI. Lascio al lettore l’esplorazione di questi casi.
.
Conclusioni
Le due tecniche esaminate in questo articolo consentono di costruire e/o trattare con oggetti
immutabili laddove questo si rende necessario. Si è accennato ad un potenziale utilizzo del resource
sharing, tecnica utilizzata anche dalla classe String. Nel prossimo articolo vedremo come gli oggetti
immutabili siano i naturali candidati per il resource sharing mostrando le due principali tecniche
associate.
Bibliografia
[1] Gamma E., Helm R., Johnson R. & Vlissides J., Design Patterns: Elements of Reusable
Design, Addison-Wesley,1995
[2] Holub Allen – “Why getter and setter methods are evil”, JavaWorld, Settembre 2003
Michele Arpaia, laureato in Scienze dell’informazione, attualmente lavora come Team Leader per
Accenture Technology Solutions. Svolge soprattutto attività di mentoring, recuitment, gestione
progetti e supporto tecnico alle fasi di analisi, design ed implementazione su tecnologia Java e
C++.