post-images/overriding-clone/cover.jpg

Overriding clone()

Merhaba arkadaşım bu yazı içerisinde Object sınıfının clone() metodunu inceleyeceğim. Öğrendiklerimi sana aktarmayı hedefliyorum. Hadi başlayalım.

Bir nesnenin kopyasını kullanmak istediğinde ne yaparsın? İlk akla gelen yeni nesne oluşturup orjinal nesneyi yeni oluşturduğun nesneye atamak değil mi? Java'nın pass by value özelliğinden dolayı pek işini görmeyeceğini söyleyebilirim. Peki daha önce hiç clone() metodunu duydun mu veya kullandın mı ? Benim kullanmak için bir fırsatım olmamıştı. Bu yazı ile deneyimimizi arttırmayı veya en azından kulağımızda böyle bir şey olduğuna dair bilgi olmasını diliyorum. Konumuza dönecek olursak genel olarak yazmış olduğun nesnenin klonlanmaya izin verildiğini beyan etmek için yazılıyor. Fakat klonlama yöntemini barındırmıyor. Neye göre nesnemi klonlayacağım konusunda bir bilgim ilk aşama için yok. Bu öğe temelde iyi niyetli bir klonlama yöntemini nasıl uygulayacağımı, ne zaman uygulamam gerektiğini tartışmak ve alternatif aramak için var.

Herhangi bir implementasyon olmadan yazmış olduğum nesnenin clone() metodunu çağırırsam ne olur? Klonlamak istediğin nesnenin alanları aynı olacak şekilde kopyasını döndürür. Tabi klonlanabilir ise eğer değil ise CloneNotSupportedException hatası ile karşılaşılacaktır. Klonlanabilirden kasıt burada Cloneable interface'inin implementasyonundan gelir. Normalde interface'leri sınıfın neler yapacağı hakkında bilgi vermesi için kullanırız fakat burada üst sınıfta bulunan bir metodun davranışını değiştirmek için kullanıyoruz. Peki Object sınıfının clone() metodu bizim için yeterli mi? Kısmen evet, kısmen hayır. Diğer Object sınıfı metodlarında olduğu gibi bu metodu uygularken de uyulması gereken kurallar bütünü - sözleşme mevcut. Fakat clone() metodu için bu maddelerin sağlam olduğu söylenemez. Sözleşme maddeler şunlar::

  • x.clone() != x geçerli olmalı.
  • x.clone().getClass() == x.getClass() geçerli olmalı ama mutlak gereklilik yok
  • x.clone().equals(x) geçerli olmalı ama mutlak gereklilik yok

Object sınıfı içerisinden clone() metodunun kullanılabilmesi için super.clone() metodunun çağrılması gerekmektedir ve tüm alt sınıflar bu kurala uymak zorundadır. Bu kural sağlandığında x.clone().getClass() == x.getClass() kuramının gerçekleşmesi beklenir. Sonuç olarak tamamen birbirinden bağımsız iki nesne ortaya çıkmalıdır.

// Clone method for class with no references to mutable state
@Override public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();  // Can't happen
    }
}

Burada dikkat edilmesi gereken diğer bir unsur immutable (değişmez) sınıflardır bu tip sınıfların boşa kopyalama yapılmaması gerekir.

Herhangi bir immutable sınıf içermeyen PhoneNumber sınıfımızı klonlamak istediğimizde Clonable implementasyonu ile Object sınıfının clone() metodunu kullanabilir. Cast işlemi ilede objemizin tipini PhoneNumber olarak belirtebiliriz. Bu işlemin başarılı sonuçlanacağı garantidir.

Herhangi bir immutable sınıf barındıran başka bir sınıfı klonlamak istediğimizde ortalık biraz karışabilir sonuçları felaket olabilir. Aşağıdaki örneği ele aldığımızda söylemek istediğimi daha iyi anlayacaksın.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    
    public Object pop() {
        if (size == 0) throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        // Eliminate obsolete reference
        return result;
    }
    
    // Ensure space for at least one more element. 
    private void ensureCapacity() {
        if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

Gördüğün gibi bir stack yapısında stack içerisine eleman eklendikçe ve stack kapasitesi yeterli olmadığı müddetçe kendisini genişletecektir. Bu yapıyı klonlamak istediğimizi varsayalım. clone() yöntemini super.clone() ile çağıralım. Ne ile karşılaşırız? size değişkenin mutable (değişebilir) olduğundan klonlama işleminde de birebir aynı değerini korur yani klonlamak istediğin stack içerisinde 60 eleman varsa size değişkeni bu değerini yeni oluşacak klonunda da koruyacaktır. Fakat dikkatini vermeni istediğim bir yer var. Stack'in değerlerini tuttuğu element değişkeni klonlandığında orjinal halini refere edecektir. Orjinal refensı değiştirmek klonu, klondaki referansı değiştirmek orjinaldeki değişmezleri bozacaktır. Bu durum klonlama sonrası yanlış sonuçları hatta NullPointerException hatasını görmene neden olacak. Bu durumu çözmek için elements alanının da klonu alınabilir.

// Clone method for class with references to mutable state
@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

Son olarak dikkat etmeni istediğim diğer bir konu iç içe olan yapılardır. Örneğin bir ağaç yapısını düşün ben bu ağaç yapısını kopyalamak istediğim durumda Object sınıfının clone() metoduna başvurursam birebire alanlarını kopyalayacak ve sadece ağacın ilk değerlerini kopyalamış olacağım. Bunun yerine tek tek tüm notlar derinlemesine gezilip kopyalanması gerekecektir. Aşağıdaki örnek ile pekiştirebilirsin.

Recursive yapıda bir HashTable sınıfımız mevcut:

public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

    private static class Entry {
        final Object key;
        Object value;
        Entry  next;
        
        Entry(Object key, Object value, Entry next) {
            this.key   = key;
            this.value = value;
            this.next  = next;  
        }
    }
    ...
}

clone() metodu ile önceki öğrendiklerimizi uygularsak eğer;

// Broken clone method - results in shared mutable state!
@Override public HashTable clone() {
    try {
        HashTable result = (HashTable) super.clone();
        result.buckets = buckets.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

Stack içerisinde olduğu gibi HashTable için de buckets değişkeninin bağımlılığını koparmış olduk. Fakat yeterli değil burada iç içe recursive bir yapı olduğundan bizim tüm bu yapıyı klonlayabiliyor olmamız gerekiyor. Sınıfımızın içerisine recursive bir klonlama yapısı ekleyerek bunu çözebiliriz.

... 
// Recursively copy the linked list headed by this Entry
Entry deepCopy() {
    return new Entry(key, value, next == null ? null : next.deepCopy());
}
... 

Bu metoda bağlı olarak clone() implementasyonunu şu şekilde değiştirebiliriz.

... 
@Override public HashTable clone() {
    try {
        HashTable result = (HashTable) super.clone();
        result.buckets = new Entry[buckets.length];
        for (int i = 0; i < buckets.length; i++)
            if (buckets[i] != null)
                result.buckets[i] = buckets[i].deepCopy();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}
... 

ta daaaa🎉 Tam olarak orjinal sınıfının bir kopyasını oluşturmuş oldun tebrik ederim 👏🏻

Clonable özelinde başlıca bahsetmek istediğim ve önemli gördüklerim bunlardı. Kitap üzerinden devam edip başka konularda da bilgi birikimini arttırmaya devam edeceğim takipte kal 🙋🏻‍♂️

JavaEffective JavaMethods Common to All ObjectsClonableclone()OverridingSoftware Contract