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 » 8.Clonazione di oggetti. Confronto tra oggetti.


Clonazione di oggetti

Per “clonazione” si intende la creazione di un oggetto uguale ad uno esistente, ma indipendente da esso.
Ovvero, solitamente si desidera che le modifiche eventualmente apportate al clone non abbiano effetto sull’oggetto clonato.
Il linguaggio Java suggerisce un modo standard di clonare gli oggetti: il metodo clone della classe Object

protected Object clone() throws CloneNotSupportedException

Tale metodo effettua una cosiddetta “copia superficiale” dell’oggetto sul quale viene chiamato:

  • cioè, ogni campo dell’oggetto in questione viene copiato nel nuovo oggetto tramite una semplice assegnazione;
  • quindi, nel caso ci sia un campo x di tipo riferimento (o array), nel clone il campo x farà riferimento allo stesso oggetto (o array) cui faceva riferimento nell’oggetto clonato;
  • il più delle volte, questo comportamento non è quello desiderato, in quanto contrasta con il principio che il clone abbia vita indipendente dall’originale;
  • a tale scopo, è utile ridefinire il metodo clone nelle proprie classi, realizzando una copia “profonda” anziché “superficiale”;
  • la copia profonda consiste nel clonare ricorsivamente gli oggetti puntati dai propri campi.

Visibilità di clone

Si dice che un membro protected è visibile nel pacchetto e nelle sottoclassi. In realtà, la regola di visibilità è più complicata.
Un membro protected di una classe A è visibile in una classe B se:

  1. B si trova nello stesso pacchetto di A, oppure;
  2. B estende (anche indirettamente) A, e l’accesso al membro in questione viene fatto tramite un riferimento di tipo dichiarato B o suo sottotipo.

Questo fa si che una sottoclasse non possa accedere ai membri protected degli oggetti della sua superclasse.
Ovvero, le funzionalità protected sono disponibili solo per gli oggetti su cui la sottoclasse ha effettivamente responsabilità, cioè per le sue istanze.
Nota: un membro statico protected è effettivamente accessibile nel pacchetto e in tutte le sottoclassi, senza la restrizione di sopra.

Consideriamo una classe A, che non ridefinisce il metodo clone, e un’altra classe Test.

  • Supponiamo che la classe Test voglia clonare un oggetto di tipo A.
  • Naturalmente, Test estende Object.
  • Tuttavia, Test non può chiamare il metodo clone (di Object) su un oggetto di tipo A.
  • Test potrebbe chiamare il metodo clone di Object solo su oggetti di tipo Test, o suoi sottotipi.

Quindi, perché una classe sia effettivamente clonabile, essa deve ridefinire il metodo clone, rendendolo pubblico.

Clausola “throws” di clone

L’eccezione CloneNotSupportedException (abbreviando, CNSE), che clone dichiara di lanciare, è verificata.
Come vedremo, clone lancia questa eccezione se l’oggetto this non implementa l’interfaccia Cloneable.
Il fatto che l’eccezione sia verificata serve ad attirare l’attenzione del programmatore sulla delicatezza della clonazione.
L’eccezione verificata, insieme alla corrispondente interfaccia e alla visibilità protected, fa si che attivare correttamente la clonazione richieda una serie di passi che vanno accuratamente programmati.

Se, nel ridefinire il metodo clone, eliminiamo dalla nuova versione la dichiarazione “throws CNSE”, per le regole dell’overriding nessuna futura sottoclasse potrà ridefinire il metodo clone in modo da lanciare quell’eccezione.
Così facendo, impediamo alle nostre future sottoclassi di disabilitare la clonazione.

D’altra parte, mantenere la clausola “throws” comporta un fastidio per i client, che devono gestire l’eccezione anche se essa non verrà lanciata.
Sta al progettista decidere di volta in volta quale aspetto far prevalere.

L’interfaccia Cloneable

Il metodo clone di Object controlla preliminarmente che l’oggetto su cui è stato chiamato implementi l’interfaccia Cloneable
in caso negativo, viene lanciata l’eccezione verificata CloneNotSupportedException.

L’interfaccia Cloneable è un’interfaccia “di tag” (tag interface), ovvero è completamente vuota.
Le interfacce di tag servono solo a specificare che una certa classe gode di una certa proprietà.
In questo caso, essa serve a indicare che una classe prevede che i suoi oggetti vengano clonati.

Un’altra interfaccia di tag che incontreremo più avanti è Serializable.

Copia superficiale e copia profonda

Consideriamo una classe Employee, con campi nome (Stringa), salario (int) data di assunzione (java.util.Date) e capoufficio (un altro Employee).
Come va clonato un oggetto Employee?

  • in realtà, la risposta dipende dal contesto applicativo;
  • delineiamo comunque una proposta plausibile di clonazione.

Il campo salario non presenta alcuna difficoltà:

  • essendo di un tipo base, è sufficiente copiarlo tramite assegnazione;
  • quindi: copia superficiale.

Il campo nome è di tipo riferimento:

  • siamo tentati di effettuare una copia profonda;
  • tuttavia, gli oggetti String sono immutabili;
  • non c’è alcun rischio nel condividere lo stesso oggetto String tra clone e oggetto originale;
  • quindi: copia superficiale.

Copia superficiale e copia profonda (segue)

Il campo “data di assunzione” punta ad un oggetto di tipo java.util.Date:

  • uno sguardo alla documentazione di quella classe ci dice che i suoi oggetti sono mutabili;
  • cioè, la classe offre dei metodi modificatori per cambiare una data;
  • per essere indipendente dall’originale, l’oggetto clonato dovrà avere una copia della data di assunzione dell’originale;
  • quindi: copia profonda.

Il campo capoufficio punta ad un altro Employee:

  • supponiamo che gli Employee siano mutabili;
  • siamo tentati di effettuare una copia profonda;
  • tuttavia, il capoufficio dell’oggetto clonato dovrebbe essere effettivamente la stessa persona che è capoufficio dell’originale;
  • inoltre, sarebbe assurdo che la classe Employee offrisse metodi per modificare i dati del proprio capoufficio;
  • più probabilmente, essa offre un metodo per cambiare integralmente il proprio capoufficio, assegnando l’impiegato ad un altro supervisore;
  • per questi motivi, e in mancanza di ulteriori informazioni sul contesto, è maggiormente indicata la copia superficiale.

Copia superficiale e copia profonda (segue)

In definitiva, otteniamo la seguente implementazione Mostra codice

Notiamo che, da Java 1.5, è possibile specializzare il tipo restituito da clone, passando da Object a Employee.
Iniziamo chiamando il metodo clone di Object, che effettua una preliminare copia superficiale.
Il cast è necessario e sicuro, perché sappiamo che this è di tipo Employee.
Poi, procediamo con la copia profonda per quei campi che, in base all'analisi precedente, lo richiedano.
In questo caso, l'unico campo che richiede copia profonda è la data di assunzione.

Un rapido controllo al manuale della libreria standard ci informa che la classe java.util.Date è clonabile, cioè implementa Cloneable ed è dotata di un metodo clone pubblico;
tuttavia, il metodo clone di Date restituisce un semplice Object, e quindi necessita di un cast.

Confronto tra oggetti: l’interfaccia Comparable

Esaminiamo il problema di ordinare un insieme di oggetti secondo un criterio di ordinamento.
Innanzitutto, vediamo qual è il modo standard di impostare un criterio di ordinamento per gli oggetti di una classe.
La libreria standard Java fornisce due interfacce a questo scopo.
La prima è l’interfaccia Comparable

public interface Comparable {
public int compareTo(Object x);
}

Quando si voglia dotare una classe di un criterio di ordinamento, si può far implementare alla classe l’interfaccia Comparable.
Il contratto del metodo compareTo è il seguente:

  • dato un oggetto x, restituisce:
    • un valore negativo se this è minore di x;
    • 0 se this è uguale a x;
    • un valore positivo se this è maggiore di x;
  • il metodo dovrebbe lanciare l’eccezione (non verificata) ClassCastException se riceve un oggetto che non è confrontabile con this, a causa del suo tipo effettivo.

L’uso dell’interfaccia Comparable è indicato quando la classe da ordinare possiede un unico criterio di ordinamento naturale.

Confronto tra oggetti: l’interfaccia Comparator

In alternativa, si può realizzare una seconda classe che implementa l’interfaccia Comparator

public interface Comparator {
public int compare(Object x, Object y);
}

Il contratto del metodo compare di Comparator è il seguente

  • dati due oggetti x e y, restituisce:
    • un valore negativo se x è minore di y;
    • 0 se x è uguale a y;
    • un valore positivo se x è maggiore di y.

L’uso di Comparator è indicato quando:

  • la classe da ordinare non ha un unico criterio di ordinamento naturale, oppure
  • la classe da ordinare è già stata realizzata e non può essere modificata.

Quella presentata qui è la versione “grezza” delle interfacce Comparable e Comparator, l’unica presente nella libreria standard fino a Java 1.4.
Come si vedrà in seguito, a partire da Java 1.5 queste interfacce sfruttano il meccanismo della programmazione generica.

Esempio

Consideriamo la classe Employee, con campi nome (String) e salario (int).
Se siamo certi che gli Employee saranno sempre confrontati alfabeticamente per nome, faremo in modo che Employee implementi l’interfaccia Comparable, come segue.

public class Employee implements Comparable {

private int salary;
private String name;
public int compareTo(Object x) {

if (!(x instanceof Employee)) throw new ClassCastException();
Employee e = (Employee) x;
return name.compareTo(x.name);
}

}

Come si vede, sfruttiamo il fatto che la classe String implementi a sua volta Comparable.

Esempio (segue)

Supponiamo adesso di voler offrire due diversi criteri di ordinamento per gli Employee: per nome e per salario.
Per farlo, dobbiamo utilizzare l’interfaccia Comparator, come segue.

Mostra codice

Si noti l'uso di classi anonime per l'inizializzazione di campi statici.

Proprietà dell’ordinamento

Affinché l’implementazione di Comparable o Comparator definisca effettivamente un criterio di ordinamento tra oggetti, essa dovrà rispettare le seguenti proprietà.
Dati due oggetti x e y, appartenenti ad una classe che implementa Comparable, deve valere:

  1. x.compareTo(x) == 0 (riflessività);
  2. x.compareTo(y)<0 se e solo se y.compareTo(x)>0;
  3. x.compareTo(y)==0 se e solo se y.compareTo(x)==0 (insieme alla precedente, rappresenta una forma di antisimmetria);
  4. se x.compareTo(y)<0 e y.compareTo(z)<0 allora x.compareTo(z)<0 (transitività).

Condizioni simili devono valere per le implementazioni di Comparator.

  • 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