Literatur

Interface Cloneable

Ein spezielles Interface ist das Interface Cloneable. Dies dient dazu, eine exakte Kopie von einem Objekt zu erstellen. Normalerweise würde man denken, dass bei einer Zuweisung ebenfalls eine exakte Kopie eines Objektes entsteht. Dies ist aber nur bei einfachen Datentypen der Fall. Bei Objekten allerdings findet bei einer Zuweisung lediglich eine Referenzierung auf den Speicherbereich, in dem das Objekt liegt, statt. Zur Veranschaulichung dieser Referenzierung schauen wir uns mal folgendes Beispiel an.

/* Unsere Klasse Kopie, implementiert hier noch nicht das Interface Cloneable */
class Kopie
{
    // Attribut x
    int x;
 
    // Konstruktor zum Setzen des Attributes
    public Kopie(int x)
    {
        this.x=x;
    }
 
    // Methode zur Ausgabe des Attributes
    public void print()
    {
        System.out.println("x = "+x);
    }
}
 
/* Startklasse KopieTest, um unsere Klasse Kopie mit der Zuweisung zu testen. */
public class KopieTest
{
    // main-Methode
    public static void main (String [ ] args)
    {
        /* Erstellung eines Objektes mit dem new-Operator  */
        Kopie ref1 = new Kopie(1);
 
        /* Der Variablen ref2 wird der Inhalt von ref1 zugewiesen */
        Kopie ref2 = ref1;
 
        /* Aufruf der Methode print */
        ref1.print();
        ref2.print();
 
        /* Attribut x des von ref1 referenzierten Objektes wird der Wert 5 zugewiesen */
        ref1.x=5;
 
        /* Erneuter Aufruf der Methode print */
        ref1.print();
        ref2.print();
    }
}



In der main-Methode erzeugen wir zuerst ein Objekt von der Klasse Kopie und weisen es der Variablen ref1 zu. Direkt danach weisen wir der Variablen ref2 den Wert von ref1 zu. Wenn wir uns nun mit dem Button die Ausgabe anschauen, sehen wir, dass die ersten beiden Ausgaben identisch sind. Dies ist auch nicht verwunderlich, da die ersten beiden Ausgaben direkt nach der Zuweisung (ref2 =ref1) erfolgt sind. Die anderen beiden Ausgaben erstaunen schon eher. Wir haben kurz zuvor das Attribut x von ref1 verändert. Deswegen hätte man vielleicht erwarten können, dass die Ausgabe von beiden Objekten ref1 und ref2 unterschiedlich sind. Aber da wir ja schon vor diesem Beispiel gesagt haben, dass eine Zuweisung bei Objekten keine Kopie des ersten Objektes erzeugt, sondern nur auf dieses verweist, ist diese Ausgabe nur logisch, da wir über beide Variablen dasselbe Objekt referenzieren.

Zur Veranschaulichung sehen Sie sich bitte folgendes Bild an.

Übersicht,clonen und Zuweisung zur Veranschaulichung

Auf der linken Seite des obigen Bildes sehen Sie die Zuweisung von unserem Objekt ref1 zu ref2. Beide "zeigen" auf dasselbe Objekt. Diese Art von Zuweisung geschieht auch bei Methodenaufrufen die Objekte als Übergabeparameter haben. Dadurch kann man mehrere Rückgabewerte in einer Methode realisieren, in dem man die Attribute des Objektes verändert. Dies ist aber nicht immer erwünscht und deswegen muss man sich zuerst überlegen, ob man das existierende Objekt tatsächlich verändern möchte.

Möchten wir hingegen das Objekt tatsächlich kopieren, müssen wir es klonen. Das Klonen ist in der rechten Bildhälfte dargestellt. Hier zeigt jede Variable auf ein eigenes Objekt nach dem Klonvorgang. An dieser Stelle wirkt sich eine Änderung nicht auf die andere Variable aus.

Wir erweitern nun unsere Klasse Kopie und implementieren dort das Interface Cloneable.

/* Unsere Klasse Kopie implementiert hier das Interface Cloneable */
class Kopie implements Cloneable
{
    // Attribut x
    int x;
 
    // Konstruktor zum Setzen des Attributes
    public Kopie(int x)
    {
        this.x=x;    
    }
 
    // Methode zur Ausgabe des Attributes
    public void print()
    {
        System.out.println('x = '+x);
    }
 
    /* Implementierte Methode aus dem Interface Cloneable */
    /* Diese Methode kann eine CloneNotSupportedException werfen */
    public Object clone() throws CloneNotSupportedException
    {
        /* Hier wird die Methode clone der Superklasse(in diesem Falle Object) aufgerufen. */
        return super.clone();
    }
 
}
 
/* Startklasse KopieTest, um unsere Klasse Kopie mit der Methode clone zu testen. */
public class KopieTest
{
    // main-Methode
    public static void main (String [ ] args)
    {
        /* Erstellung eines Objektes mit dem new Operator und den Konstruktor mit dem Wert 1 */
        Kopie ref1 = new Kopie(1);
 
        // Initialisierung von ref2 mit null
        Kopie ref2 = null;
 
        try
        {
            /* Kopierversuch von Objekt ref1 nach ref2 */
            /*    Da die Methode clone den Datentyp Object zurückliefert,
                müssen wir eine Typumwandlung zu unserer Klasse Kopie durchführen
            */
            ref2 = (Kopie) ref1.clone();
        }
        catch(CloneNotSupportedException ex)
        {
            /* Auffangen der Exception CloneNotSupportedException */
            System.out.println("Das Kopieren dieses Objektes wird nicht untersützt");
        }
 
        /* Aufruf der Methode print von beiden Objekten */
        ref1.print();
        ref2.print();
 
        /* Attribut x des von ref1 referenzierten Objektes wird der Wert 5 zugewiesen */
        ref1.x=5;
 
        /* Erneuter Aufruf der Methode print von beiden Objekten */
        ref1.print();
        ref2.print();
    }
}

Wir haben jetzt das Interface Cloneable in unsere Klasse Kopie eingebunden. Aus diesem Grunde implementieren wir auch die Methode clone. In der Methode clone rufen wir an dieser Stelle einfach die Methode clone von der Superklasse, nämlich Object, auf. Anschließend wird das geklonte Objekt zurückgegeben oder es wird die Exception CloneNotSupportedException geworfen. Wenn wir uns nun die Ausgabe ansehen, sehen wir, dass unser geklontes Objekt nicht verändert wurde, nachdem wir die Änderung an dem von ref1 referenziertem Objekt durchgeführt haben. Da die clone-Methode eine Exception werfen kann, müssen wir auch eine Fehlerbehandlung einbauen und diese Exception auffangen.

Geübte Entwickler sehen an dem Quellcode, dass die Fehlerbehandlung nicht gut gelöst wurde. Es können zwei weitere Exceptions, die nicht abgefangen oder durch einfache Überprüfung hätten festgestellt werden können, auftreten. Die erste zusätzliche Exception, die in einem weiteren catch-Block hätte abgefangen werden sollen, wäre die TypeCastException, die bei der Typkonvertierung von Object nach Kopie auftreten kann. Die zweite zusätzliche Exception, die auftreten kann, wäre die NullPointerException. Sollte im try-Block eine Exception auftreten, so bleibt ref2 auf dem Wert null. Bei dem Methodenaufruf ref2.print() würde dann die NullPointerException geworfen.

Man kann die clone-Methode auch selbst implementieren, indem man mit dem new-Operator ein neues Objekt der zu kopierenden Klasse erzeugt und die vorhandenen Attribute, sofern es einfach Datentypen sind, dem neuen Objekt zuweist und anschließend das neu erzeugte Objekt zurückgibt. Je nachdem, welche Superklassen existieren, kann es sein, dass man die Methode selbst implementieren und auf den Aufurf super.clone() verzichten muss. Schauen wir uns nun zu der obigen Klasse Kopie eine eigene Methode clone an, ohne die Superklasse mit einzubeziehen.

/* Implementierte Methode aus dem Interface Cloneable */
/* Diese Methode kann eine CloneNotSupportedException werfen */
public Object clone() throws CloneNotSupportedException
{
    // Eigene Implementierung
    return new Kopie(this.x);
}

Dies ist ein sehr einfaches Beispiel, aber im Prinzip macht der Methodenaufruf von super.clone() nichts anderes. Bei komplexeren Objekten müssen natürlich noch mehr Attribute "geklont" werden, dadurch würde die Methode einige Anweisungen mehr enthalten.