1.4-Software-Development-Principles

Copy vs. Clone

Inleiding

Zorgen dat je data veilig en correct blijft is een belangrijk onderdeel van jouw programma. Daarom gebruiken we private variabelen en constructors om een correcte staat te garanderen. Een tweede eis is dat onze data beschikbaar is voor anderen, maar daarmee brengen we de eerste eis in gevaar. Wie wat wat die “Gevaarlijke Anderen” met onze precious data gaan doen?

In plaats van onze data voor ons zelf te houden gaan we er aan werken omandere een kopie van onze data te geven.

Kopieren

Hier zijn twee lijsten met data. Nou ja, eigenlijk verwijzen beide variabelen naar de zelfde lijst. Beide variabelen op de stack wijzen naar het zelfde stuk geheugen op de heap.

int[] numbers = { 2, 3, 4, 5};
int[] numbersCopy = numbers;

Dus als we dit doen:

numbersCopy[2] = 0;

Dan wijzen beide variabelen naar een referentie waarvan element 2 een waarde van 0 heeft.

Dit is waarom een methode zoals hier onder beschreven heel gevaarlijk kan zijn als je niet goed nadenkt over de consequenties:

public int[] getList() {
	return myPreciousss;
}

Clonen

Om deze problemen te voorkomen maken we niet alleen een kopie van de lijst referentie, maar ook van de gehele lijst. Dit heet cloning:

  int[] numbers = { 2, 3, 4, 5};
  int[] numbersClone = (int[])numbers.clone();

In eerste instantie bevatten beide lijsten dezelfde data, maar aangezien ze nu naar twee verschillende referenties wijzen in de heap, kan de kloon gewijzingd worden zonder het origineel te veranderen.

Deep clone

In het bovenstaande voorbeeld gebruikten we een array met integer waarden. Dus de array zelf is het enige referentie type, maar wat gebeurt er als we meer complexe objecten opslaan in de array:

class Person {
	private String name;
	
	public Person(String name) {
		this.name = name;
    }
	
	public String getName() {
		return this.name;
    }
    
	public void setName(String name) {
		this.name = name;
    }
}

Nu kunnen we dus een array met Personen maken:

Person[] persons = new Person[3];
persons[0] = new Person("Dick");
persons[1] = new Person("Jan Jaap");
persons[2] = new Person("Peter"); 

We kunnen daar ook een kloon van maken:

Person[] personsClone = (Person[])persons.clone();

Maar toch kan een onguur karakter nog steeds het volgende doen:

personsClone[0] = null; // Only affects the cloned array.
personsClone[1].setName("Marcel"); // Changes the name of the same Person reference.

De gekloonde array van personen is een aparte referentie die nog steeds referenties naar dezelfde Person objecten op de heap bevat. Dat betekent dat de array onafhankelijk gewijzigd kan worden, maar de referenties beinvloeden dezelde instanties.

Om dit op te lossen moeten we ook iedere Person in de array klonen om zo twee compleet nieuwe onafhankelijke datastructuren te bouwen:

public class Person implements Cloneable {
	// ...
	@Override
	public Person clone() {
		return new Person(getName());
	}
}

Eerst implementeren we de Cloneable interface, en bouwen een manier om een kopie van Personen te maken.

Het maken van een “deep clone” van de Person array houdt in dat iedere referentie in de array gekopieerd wordt. (Enzovoort voor alle betrokken datastructuren.)

Person[] deepClone = new Person[original.length];
for (int i = 0; i < deepClone.length; i++) {
    deepClone[i] = original[i].clone();
}

Conclusie

Het maken van een volledige kopie van jouw datastructuur betekent dat alle data nu twee keer opgeslagen wordt in het geheugen. Dit is het offer dat je moet maken om er voor te zorgen dat onbetrouwbare externe partijen (zijn er andere?) alleen een kopie van jouw data kunnen gebruiken.

Ongeacht wat zij met hun kopie (kloon) van de data doen, jouw origineel blijft intact.