Home > Java > Effective Java Madde 8: equals() Metodunu Geçersiz Kılarken Sözleşmeye Uyun

Effective Java Madde 8: equals() Metodunu Geçersiz Kılarken Sözleşmeye Uyun

Java dilinde bütün sınıfların atası olan Object içerisinde, eşitlik kontrolü amacıyla kullanılan equals() metodunu geçersiz kılmak (override) çok zor bir iş gibi görünmese de bunu yanlış yapmak aslında çok daha kolay. Yanlış şekilde geçersiz kılınmış bir equals() metodu uygulamada ciddi sorunlar çıkmasına yol açabilir. Bu tarz problemlerden kaçınmanın en kolay yolu bu metodu geçersiz kılmamaktır. Bu durumda her nesne sadece kendisine eşit olacaktır. Object içerisindeki equals() metodu çok basit bir şekilde aşağıdaki gibi tanımlanmıştır:

public boolean equals(Object obj) {
    return (this == obj);
}

Gördüğünüz gibi Object sınıfında tanımlanan equals() metoduna göre her nesne sadece kendisine eşittir. Aşağıdaki durumlardan birisi söz konusuysa bu metoda dokunmamak en doğrusudur:

  • Bir sınıfın her nesnesi doğası gerekir tektir. Bu durum Thread gibi aktif varlıkları temsil eden sınıflar için doğrudur. Object sınıfından gelen equals() metodu bu tür sınıflar için doğru davranışı yansıtmaktadır.
  • Sınıfın mantıksal eşitlik testi sağlamak gibi bir amacı yoktur. Mesela, java.util.Random metodu equals() metodunu geçersiz kılarak, iki Random nesnesinin aynı değeri üretip üretmeyeceğini test edebilirdi. Ancak bu sınıfı yazanlar böyle bir teste kimsenin ihtiyacı olmayacağını düşünmüşler ve dolayısıyla equals() metoduna dokunmamışlardır. Sizin sınıfınız için de böyle bir durum geçerliyse dokunmamak en iyisi.
  • Bir ata sınıf equals() metodunu zaten geçersiz kılmıştır ve o davranış sizin için de uygundur. Mesela, birçok Set gerçekleştirimi equals() metodunu AbstractSet sınıfından alırlar, aynı şekilde List gerçekleştirimleri AbstractList, Map gerçekleştirimleri de AbstractMap sınıfından gelen equals() metodunu kullanırlar.

Peki o zaman equals() metodunu geçersiz kılmamız gereken durumlar nelerdir? Basit olarak söylemek gerekirse, nesneleri arasında mantıksal eşitlik uygulanabilen sınıflar için equals() metodu geçersiz kılınabilir. Bu tür sınıflar genellikle Date, Integer gibi değer sınıflarıdır. Bir programcı equals() metodunu kullanarak iki Integer nesnesini karşılaştırıyorsa, o iki nesnenin aynı nesne olup olmadığından ziyade aynı değeri taşıyıp taşımadığını test ediyordur. Bu durumlarda equals() metodunu geçersiz kılmak sadece programcının beklentisini karşılamakla kalmaz, aynı zamanda bu nesnelerin Map için anahtar (key) veya Set elemanı olarak doğru bir biçimde kullanılabilmesini sağlar.

Enum türü bir değer sınıfı olmasına rağmen bu sınıf için equals() metodunu geçersiz kılmaya gerek yoktur çünkü aynı değere sahip birden fazla Enum nesnesi oluşturulması mümkün değildir. Bu durumda Object sınıfından gelen equals() metodu mantıksal eşitliği karşılayacaktır.

Object sınıfı içerisinde equals() metodunu geçersiz kılarken uyulması gereken bir “sözleşme” belirtilmiştir. Bu sözleşme aşağıdaki şekildedir:

equals() metodu bir eşitlik ilişkisi tanımlar. Bu ilişki aşağıdaki şartları sağlamalıdır:

  • Dönüşlülük: Null olmayan bir x referansı için, x.equals(x) true değerini üretmelidir.
  • Simetriklik: Null olmayan x ve y referansları için, y.equals(x) true değerini üretiyor ise x.equals(y) de mutlaka true üretmelidir.
  • Geçişlilik: Null olmayan x, y ve z referansları için, x.equals(y) ve y.equals(z) true üretiyorsa, x.equals(z) de mutlaka true üretmelidir.
  • Tutarlılık: Null olmayan x ve y referansları için, equals() metodu içerisinde kullanılan alanlar değiştirilmediği sürece, x.equals(y) tutarlı bir biçimde her zaman true veya false değerini üretmelidir.
  • Null olmayan bir x referansı için, x.equals(null) false üretmelidir.

Matematiğe çok yatkın değilseniz bu kurallar biraz korkutucu görünebilir ancak asla gözardı etmeyin! Bunları çiğnediğiniz taktirde uygulamanız garip davranıp çökebilir, sorunun kaynağını bulmak da çok zor olabilir. Nesneler sürekli olarak diğer nesnelere geçilirler. Collection sınıfları da dahil olmak üzere birçok sınıf kendisine geçilen nesnelerin equals() metodunun kurallara uygun biçimde geçersiz kılındığını varsayarak işlem yaparlar.

equals() sözleşmesini ihlal etmenin tehlikeli olduğunu anladığımıza göre sözleşmenin maddelerine daha detaylı göz atalım. Tek tek bakıldığı zaman hiç de karmaşık olmayan bu kuralları bir kere anladığınız zaman uygulamak hiç de zor olmayacaktır. Şimdi bu kurallara tek tek bakalım:

Dönüşlülük

Bu kurala göre her nesne kendisine eşit olmalıdır. Bu kuralı ihlal etmek zor gibi görünse de ihlal edildiği taktirde, nesneyi bir listeye ekleyip daha sonra contains() metodunu çağırırsanız eklediğiniz nesneyi bulamayabilirsiniz.

Simetriklik

İkinci kural iki nesnenin birbirleri arasındaki eşitlik ilişkisi konusunda aynı fikirde olmaları gerektiğini söyler. İlk kuralın aksine bu kuralı ihlal etmek çok da zor değildir. Örnek olarak, aşağıdaki büyük/küçük harflere duyarsız stringleri ele alan CaseInsensitiveString sınıfını inceleyelim:

// Bozuk - simetriyi ihlal ediyor!
public final class CaseInsensitiveString {
    
    private final String s;

    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        this.s = s;
    }

    // Bozuk - simetriyi ihlal ediyor!
    @Override 
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        if (o instanceof String) 
            return s.equalsIgnoreCase((String) o);
        return false;
    }
    ... // Sınıfın geri kalanı ihmal edilmiştir..
}

Yukarıdaki sınıf iyi niyetli bir biçimde normal String sınıfıyla da uyumlu çalışmayı amaçlamaktadır. Varsayalım ki aşağıdaki gibi bir normal string bir de CaseInsensitiveString nesnemiz olsun:

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

Beklendiği gibi, cis.equals(s) true döndürecektir. Buradaki problem, CaseInsensitiveString sınıfı normal string nesneleriye çalışabilmesine rağmen, String sınıfının bu durumdan haberinin olmamasıdır. Bu sebeple, s.equals(cis) false döndürecektir ve bu da açık bir biçimde simetri kuralının ihlali demektir. Bir CaseInsensitiveString nesnesini bir listeye eklediğimizi düşünelim:

List<CaseInsensitiveString> list = new ArrayList<CaseInsensitiveString>();
list.add(cis);

Bu durumda list.contains(s) ne döndürecektir? Kim bilir? Sun firmasının geliştirdiği JVM’de bu ifade false döndürmektedir ancak bu tamamen bir tercihtir. Farklı bir JVM true döndürebilir veya bir istisna fırlatabilir. equals() sözleşmesini bir kere ihlal ettiğiniz zaman, diğer nesnelerin sizin nesnenizi nasıl ele alacağını bilemezsiniz.

Bu problemden kurtulmak için, equals() metodu içerisinde normal String nesneleriyle karşılaştırma yapan bölümü çıkarmanız yeterli olacaktır. Bunu yapınca metod aşağıdaki gibi kısalacaktır:

@Override 
public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

Geçişlilik

equals() sözleşmesinin üçüncü maddesine göre bir nesne ikinci bir nesneye eşitse ve ikinci nesne de üçüncü bir nesneye eşitse, birinci nesne de üçüncü nesneye eşit olmalıdır. Bu kuralı da istemeden ihlal etmek son derece mümkündür. Bir çocuk sınıfın ata sınıfa başka bir alan eklediği durumu düşünelim. İki boyutlu bir noktayı temsil eden basit bir Point sınıfı ile çalışmaya başlayalım:

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    @Override 
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return p.x == x && p.y == y;
    }
    ... // Sınıfın geri kalanı ihmal edilmiştir..
}

Şimdi bu sınıfı kalıtıp renk alanı eklediğimizi düşünelim:

public class ColorPoint extends Point {
    
    private final Color color;
    
    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
    ... // Sınıfın geri kalanı ihmal edilmiştir..
}

Burada equals() metodu nasıl yazılmalıdır? Hiç yazmazsanız metod Point sınıfından alınacaktır ve renk (color) bilgisi eşitlik karşılaştırmasında gözardı edilecektir. Bu davranış equals() sözleşmesine aykırı olmasa da doğru davranış değildir. Aşağıdaki gibi bir equals() metodu yazdığımızı düşünelim:

// Bozuk - simetriyi ihlal ediyor!
@Override 
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint))
        return false;
     return super.equals(o) && ((ColorPoint) o).color == color;
}

Bu gerçekleştirime göre Point ve ColorPoint nesnelerini karşılaştırırken farklı sonuçlar görmeniz mümkün. Örnekle inceleyelim:

Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);

Burada p.equals(cp) true döndürecektir çünkü Point içerisindeki equals() metodu renk değerini yok saymaktadır. cp.equals(p) ise false döndürecektir çünkü argüman olarak geçilen Point nesnesi ColorPoint türünde değildir. Bu sorunu çözmek için ColorPoint içerisindeki equals() metodunu aşağıdaki gibi yazmayı deneyebilirsiniz:

// Bozuk - geçişliliği ihlal ediyor!
@Override 
public boolean equals(Object o) {

    if (!(o instanceof Point))
        return false;
    
    // Eğer 'o' normal Point ise, renk değerini ihmal et
    if (!(o instanceof ColorPoint))
        return o.equals(this);
    
    // 'o' ColorPoint türünde; tam karşılaştırma yap
    return super.equals(o) && ((ColorPoint)o).color == color;
}

Bu yaklaşım simetri kuralını sağlasa da geçişlilik kuralını ihlal ediyor:

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

Burada p1.equals(p2) ve p2.equals(p3) true değerini üretirken p1.equals(p3) false döndürecektir ve bu da geçişlilik kuralını açıkça ihlal anlamına gelir. İlk iki karşılaştırma renk değerini görmezden gelirken üçüncüsü rengi de hesaba katmaktadır.

Peki çözüm nedir? Bu aslında nesne tabanlı dillerde eşitlik ilişkisinin temel bir problemidir. Somut (abstract olmayan) bir sınıfı kalıtıp yeni bir alan ekleyerek equals() sözleşmesini muhafaza etmek mümkün değildir. Kalıtım yerine komposizyon kullanarak bu dertten kurtulabilirsiniz. Bu durumda ColorPoint, Point sınıfını kalıtmak yerine private bir Point referansı tutacaktır.

// equals() sözleşmesini ihlal etmeden sınıfa alan ekleme
public class ColorPoint {

    private final Point point;
    private final Color color;
    
    public ColorPoint(int x, int y, Color color) {
        if (color == null)
            throw new NullPointerException();
        point = new Point(x, y);
        this.color = color;
    }

    public Point asPoint() {
        return point;
    }

    @Override 
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }    
}

Java kütüphanelerindeki bazı sınıflar somut bir sınıfı kalıtıp alan eklemektedirler. Örneğin, java.sql.Timestamp sınıfı java.util.Date sınıfını kalıtır ve nanoseconds alanını ekler. Timestamp sınıfındaki equals() metodu simetriklik ilkesini ihlal etmektedir ve Timestamp ile Date sınıfları aynı Collection içerisinde veya başka bir şekilde birlikte kullanıldıklarında hatalara yol açmaktadır. Timestamp sınıfı programcıları bu konuda uyaran uyarılar içermektedir. Date ve Timestamp nesnelerini birbirlerinden ayrı tuttuğunuz sürece sorun yaşamazsınız ancak bunları birlikte kullanmanızı engelleyen bir mekanizma da yok. Timestamp sınıfının bu davranışı hatalıdır ve örnek alınmamalıdır.

Şunu da unutmayın ki soyut (abstract) sınıfları kalıtarak equals() kurallarını ihlal etmeden alan eklemek mümkündür. Örneğin, hiçbir alan içermeyen Shape isimli bir soyut sınıfınız varsa, bunu kalıtan Circle adlı bir çocuk sınıf yaratıp radius gibi bir alan ekleyebilirsiniz. Aynı şekilde Rectangle diye bir çocuk sınıf yaratıp width and height gibi alanlar ekleyebilirsiniz. Bu durumda yukarıdaki gibi problemler karşınıza çıkmayacaktır çünkü ata sınıf soyut olduğu için direk olarak ata sınıftan nesne yaratmanız mümkün olmayacaktır.

Tutarlılık

Dördüncü kurala göre eğer iki nesne birbirine eşitse, bu nesnelerden en az birisi değişmediği sürece hep eşit kalmalıdırlar. Başka bir deyişle, kararsız (mutable) nesneler değişik zamanlarda değişik nesnelere eşit olabilirken, kararlı (immutable) nesneler olamazlar. Bu yüzden bir sınıf yazarken kararlı olup olmaması gerektiğini iyi düşünün, kararlı olması gerektiğine karar verirseniz equals() metodunun bu kuralı sağladığından emin olun.

Bir sınıf kararlı da kararsız da olsa, equals() metodunun sonucu güvenilir olmayan kaynaklara asla bağlı olmamalıdır. Bu kuralı ihlal ettiğiniz zaman tutarlılık prensibini sağlamamanız çok zorlaşacaktır. Mesela, java.net.URL sınıfındaki equals() metodu, URL ile ilişkili olan IP adreslerini de dikkate alarak bir sonuç üretmektedir. Bu IP adreslerini üretmek ağ bağlantısı gerektirebilir ve üstelik zaman içerisinde aynı sonucu üreteceğinin de bir garantisi yoktur. Maalesef bu durum URL sınıfının equals() metodunun tutarsız çalışmasına sebep olmaktadır ve pratikte bir takım problemlere yol açmıştır. Çok ekstrem durumlar haricinde equals() metodu dış kaynaklara bağımlı olmadan her zaman aynı sonucu üretebilecek bir durumda olmalıdır.

Null Değerler

equals() sözleşmesinin son maddesine göre hiçbir nesne null değerine eşit değildir. o.equals(null) gibi bir metod çağrısından yanlışlıkla true döndürmek zor bir ihtimal gibi görünse de NullPointerException fırlatmak gibi bir hata yapabiliriz. Birçok sınıf bu hatayı önlemek için açık bir biçimde null testi yapmaktadır.

@Override 
public boolean equals(Object o) {
    if (o == null)
        return false;
    ...
}

Bu test gereksizdir. Geçilen argüman eşitlik için kontrol edilmesi gerektiğinde ilk başta doğru türe dönüştürülmelidir ki o nesnenin alanlarına erişilebilsin. Tür dönüştürme yapılmadan önce ise instanceof operatörü kullanılarak doğru türde bir nesnenin geçildiğinden emin olunması gerekmektedir.

@Override 
public boolean equals(Object o) {
    if (!(o instanceof MyType))
        return false;
    MyType mt = (MyType) o;
    ...
}

Eğer instanceof operatörüyle tür kontrolü yapmasaydık 5. satırdaki tür dönüştürme (cast) işlemi ClassCastException ile sonuçlanırdı ve bu da equals() sözleşmesine aykırı olurdu. instanceof operatörü eğer solundaki değer null ise false döndürecek şekilde tasarlanmıştır, bu durumda sağındaki tür değerinin bir önemi yoktur. Dolayısıyla metoda null geçildiği taktirde zaten false sonuç üretecektir, fazladan bir null kontrolü yapmaya gerek yoktur.

Bütün bu söylediklerimizi toparlayacak olursak, doğru bir equals() metodunun tarifi şu şekildedir:

1. == operatörünü kullanarak geçilen argümanın bu nesneye bir referans olup olmadığını kontrol edin. Eğer öyleyse true döndürün. Bu iyileştirme, eğer karşılaştırma işlemi pahalı bir işlemse performans artışı sağlayabilir.
2. instanceof operatörünü kullanarak geçilen argümanın doğru türde olup olmadığını kontrol edin. Eğer aynı türden değilse false döndürün. Tipik olarak doğru tür, equals() metodunun yazılı olduğu sınıftır. Ancak bazı durumlarda sınıfın gerçekteştirdiği bir arayüz türü de olabilir. Set, List, Map gibi sınıflarda bu şekilde bir kullanım görmek mümkündür.
3. Argümanı doğru türe dönüştürün. Tür dönüşümü instanceof testinden sonra geleceği için her zaman başarılı olacaktır.
4. Eşitlik kontrolü yaparken sınıf içerisindeki bütün anlamlı alanları karşılaştırın. Eğer bütün alanlar eşitse o zaman true, değilse false döndürün. Karşılaştırma yaparken float veya double olmayan ilkel türler için == operatörünü, nesne referansları için ise özyineli olarak equals() metodunu kullanın. float için Float.equals() ve double için Double.equals() metotlarını kullanın. float ve double türleri için yapılan bu özel karşılaştırmanın sebebi Float.NaN, -0.0f ve bunlara karşılık gelen double değerleridir. Bu metotların dokümantasyonuna bakarak daha detaylı bilgi alabilirsiniz.

Bazı nesne referansları null değerini alabilirler, bu durumda olası bir NullPointerException hatasını engellemek için aşağıdaki yöntemi kullanabilirsiniz:

(field == null ? o.field == null : field.equals(o.field))

equals() metonunun performansı yapılan karşılaştırmaların sırasına göre değişebilir. En iyi performans için, farklı olma ihtimali yüksek olan alanlar ve karşılaştırması ucuz olan alanları tercih edin. Nesnenin o anki mantıksal durumunu yansıtmayan, Lock gibi senkronizasyon işlemleri için kullanılan alanları karşılaştırmayın. Sınıfın içerisinde başka alanlar kullanılarak türetilen alanlar varsa bunları da karşılaştırmanın bir gereği yoktur.

5. equals() metodunuzu yazmayı bitirdikten sonra kendinize 3 soru sorun: Simetrik oldu mu? Geçişli oldu mu? Tutarlı oldu mu? Bu soruları sadece kendinize de sormayın, birim testler yazarak gerçekten bu kuralları sağladınızı garanti altına alın. Tabiki de diğer iki kuralı da (dönüşlülük ve null değerler) sağlamanız gerekmekte, ancak diğer üçünü sağladığınız zaman geri kalanlar da genellikle sağlanmış oluyor.

Bütün bu kuralların nasıl uygulandığını gösteren somut bir örnek arıyorsanız Effective Java 9. maddede yer alan PhoneNumber.equals() metoduna bir göz atabilirsiniz. Son birkaç uyarı:

  • equals() metodunu geçersiz kılıyorsanız mutlaka hashCode() metodunu da edin. (Effective Java – Madde 9)
  • Zekanıza çok güvenmeyin: Sınıfın alanlarını test ederek eşitlik testi yaptığınız sürece equals() sözleşmesine uymak son derece basit. Olayı abartıp eşitliği başka yerlerde ararsanız başınızı belaya sokma ihtimaliniz çok yüksek.
  • equals() metodunu geçersiz kılarken Object sınıfını başka bir türle değiştirmeyin. Aşağıdaki gibi yazılmış bir equals() metodunu sıklıkla görebilirsiniz:
    public boolean equals(MyClass o) {
       ...
    }
    

    Buradaki problem, bu metot Object sınıfındaki equals() metodunu geçersiz kılmaktan (override) ziyade aşırı yüklemektedir (overload) ve bu ikisi tamamen farklı şeylerdir. Geçersiz kılınmış bir equals() metodunun yanında ek olarak aşırı yüklenmiş bir versiyon yazılması, her ne kadar çok faydalı olmasa da kabul edilebilir.

    equals() metoduyla birlikte @Override notasyonunun (annotation) kullanılması sizin bu hatayı yapmanıza engel olacaktır. Örneğin, aşağıdaki metod derleme aşamasında hata verecektir ve size neyin yanlış olduğunu söyleyecektir:

    @Override
    public boolean equals(MyClass o) {

    }

Böylelikle 8. maddeyi de bitirmiş olduk. Bir sonraki yazıda görüşmek üzere..

Categories: Java Tags: ,
  1. No comments yet.
  1. No trackbacks yet.

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 647.106 bad guys.

%d blogcu bunu beğendi: