Ocak 25, 2022 · 12 dakikaOverriding equals()
Evveet 🎉 Yeni ünitenin ilk yazısıyla devam ediyoruz. Bu yazıda Effective Java kitabının ikinci ünitesi olan Methods Common to All Objects'in ilk maddesini ele alacağım.
Bu maddede Object sınıfında bulunan equals() metodunun nasıl kendi sınıfımızda override edileceğini, override ederken nelere dikkat etmemiz gerektiğini ve geliştirirken önümüze çıkarabilecek ve dikkat etmemiz gereken kritik hatalara değineceğim.
Object sınıfımızı bir ziyaret edelim ne varmış bu equals() içerisinde.
public boolean equals(Object obj) {
return (this == obj);
}
Ne kadar da basit ve masum görünüyor değil mi? Sanki bir melek... Sınıfım için equals metoduna verilen parametre ile eşitliğini kontrol ediyor. Ancak bu metodu özelleştirmeyi hedeflediğim durumda yanılgıya düşmem çok mümkün. equals() başlıca bir kaç özelliğini yazmam gerekirse;
Sınıfımın her bir instance'ı doğası gereği benzersizdir : Değerler yerine aktif entitity'leri temsil eden Thread'ler için true döner. Object sınıfı için tam olarak doğru davranıştadır.
Eşitliği kontrol edilen iki nesne mantıksal olarak eşit olmak zorunda değildir : Örneğin oluşturulan iki Pattern nesnesinin paternlerinin eşleşmesine veya veya Random() sınıfının aynı sayıyı üretmesine gerek yoktur.
Parent sınıf equals() metodunu override etmişse bu metod child'lar için de geçerlidir : Set sınıfı AbstractSet sınıfından, List sınıfı ise AbstractList sınıfından equals() implementasyonunu almaktadır.
private metodu ile kullanıma kapatılabilir : Kullanılması ihtimaline karşın takıntılı iseniz ve private tanımlamak size yetmiyorsa aşağıdaki şekilde override edebilirsiniz.
@Override private boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
equals() metodunun genel özelliklerinden bahsetim. Peki ben ne zaman bu metodu override etmem gerekiyor?Hangi durumlarda bu metod override ediliyor? Bu soruları cevaplamadan önce Object metodu içerisinde hali hazır bulunan bu metodun hangi mantıkla çalıştığını kavramam gerekiyor. (this == obj) kontrolu aslında bu iki nesnenin eşit olup olmadığını kontrol ederken bellekte aynı yeri temsil edip etmediğine yani referanslarının aynı olup olmadıklarına bakmaktadır. Mesela ben referans değilde nesnelerin mantıksal olarak yani referans değilde içerdiği değerleri karşılaştırmak isteseydim ne yapacaktım? Tabi ki de kendim bu metodu override edip yeniden yazacağım. Burada kitabın ilk maddesine referans vereceğim: Statik factory metod ile oluşturulan bir nesne sürekli tek bir instance kullanılarak referansı değişmeden değeri değişeceğinden Object sınıfından gelen equals() metodu referans eşitliğine değil mantıksal eşitliğe bakacaktır. Bu durum Enum tipleri içinde geçerlidir.
Kendi nesnen içerisinde yapmış olduğun equals() implementasyonunun java kütüphanesindeki sınıflarla veya yazdığın diğer sınıflarla doğru ve hatasız çalışabilmesi için uyulması gereken kurallar bütünü yani bir sözleşme vardır. Bu sözleşmenin maddelerini örneklemeler ile ele alırsak;
- Reflexive (Dönüşlü) :
x.equals(x)isetrue - Symmetric (Simetrik) :
x.equals(y) = trueisey.equals(x)'detrue'dur. - Transitive (Geçişli) :
x.equals(y) = truevey.equals(z) = trueisex.equals(z)'detrue'dur. - Consistent (Tutarlılık) :
equals()metodunun implementasyonu değişmediği sürece her zamanx.equals(y) = true/false'dır (Daha öncetrueise her zamantruegibi). - Non-null (Boş olmama) :
x.equals(null)her zaman içinfalse'dır.
Not: Örneklerde bahsi geçen her bir parametre (x, y, z) non-null olacak şekilde kabul edilmektedir.
Kritik bir konu olarak özenle değinmek istiyorum ki; Sözleşmenin herhangi bir maddesini dahi ihmal etme, ihmal ettiğin aşamada java içerisindeki veya daha sonradan eklenmiş sınıfların senin nesnen karşısında nasıl davranacağından emin olamazsın. Buradaki durumu daha iyi kavrayabilmen amacıyla örneklerle birlikte detaylandırmayı düşünüyorum.
Reflexive (Dönüşlü)
Bir nesne kendisine eşit olmalıdır. Aksi taktirde bir liste içerisine yazdığım nesneden ekleme yapıp sonrasında listenin contains() metodunu çağırdığımda nesnemi listeye eklememe rağmen bulamayacak ve false dönecektir.
Symmetric (Simetrik)
Nesnem başka bir nesneye eşitse bahsi geçen nesne de benim nesneme eşit olmak zorundadır.
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// Broken - violates symmetry!
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
if (o instanceof String) // One-way interoperability!
return s.equalsIgnoreCase((String) o);
return false;
}
}
Yukarıda paylaştığım kod bloğu ile aşağıdaki iki nesneyi eşitlik bakımından karşılaştırsaydım sence sonuç ne olurdu?
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
Eğer kod bloğu içerisinde kullanılan equalsIgnoreCase() metodunu farkettiysen cis.equals(s) kontrolünün true döneceğini sende görmüşsündür. Peki simetrikliğin getirmiş olduğu "x.equals(y) = true ise y.equals(x)'de true'dur" kuralına ne oldu? Şuan yazılmış bu kod bloğunda bariz bir kural ihlali mevcut. cis.equals(s) = true iken s.equals(cis) = false'dır. Çünkü String sınıfı java içerisinde bulunan bir sınıftır ve içerisindeki equals() metodu büyük küçük harfe duyarlıdır. Bu nedenle aşağıdaki durumda da false ile karşılaşılacaktır.
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
...
list.contains(s); //false
Çözüm olarak kod bloğum içerisinden String sınıfı için yaptığım kontrolü kaldırabilirim.
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
Transitivity (Geçişli)
Bu gereksin birinci nesne ikinci nesneye, ikinci nesne üçüncü nesneye eşitse birinci nesne de üçüncü nesneye eşit olmalıdır mantığına dayanır. İki özellikli bir sınıf hayal et bu sınıfı kalıtan child bir sınıf olduğunuda, bu child sınıf içerisinde ekstra bir özellik olduğunu da hayal etmeni rica edeceğim. Kalıttığım parent sınıfın içerisinde override edilmiş bir equals() metodu varsa child içinde geçerli olacaktır. Parent ile aynı özelliklere sahip bir child ile eşitlik kontrol edildiğinde parent içerisinde child'ın özellikleri olmayacağından transitivity ilkesini bozmuş olacağım. Biliyorum böyle hayal etmesi zor, gel biz örnekleme yolundan şaşmayalım.
Parent sınıfım;
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;
}
}
Child sınıfım;
class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
Aşağıdaki iki nesnenin eşitliği true dönecektir. Point içerisindeki equals() metoduna göre doğru çalışmaktadır ama ColorPoint için false dönecektir. Burada ilk olarak symmetric kuralını çiğnemiş olduk.
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
....
p.equals(cp) // true
cp.equals(p) // false
Bu durumu çözebilmek için ColorPoint'im de equals() imlementasyonu yapabilirim.
// Broken - violates transitivity!
@Override
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
// If o is a normal Point, do a color-blind comparison
if (!(o instanceof ColorPoint))
return o.equals(this);
// o is a ColorPoint; do a full comparison
return super.equals(o) && ((ColorPoint) o).color == color;
}
Hımm... Bence oldu gibi artık p.equals(cp) ve cp.equals(p) eşitlik kontrolleri true dönüyor symmetric işini hallettik 👌🏼 Yalnız bu sefer de transitivity'i bozduk 🤦🏻♂️
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
İncelediğimde p1.equals(p2) ve p2.equals(p3) eşitliği true olsa da p1.equals(p3) eşitliği false'dır. Bu da transitivity maddemize karşı geldiğimi gösterir.
Peki çözüm ne? Bunu kalıtım ile yapmak mümkün değildir. Bu durum nesne tabanlı dillerde eşitlik ilişkisinin temel bir problemidir. Bunun çözümü composition yöntemiyle gerçekleşmektedir. Bu konu önümüzde ki maddelerde tekrar karşımıza çıkacak ve detaylı inceleyeceğim. Özetle elimdeki parent ve child'ın aynı seviyede bulunmasını sağladım.
Composition Yöntemi ile Çözüm:
// Adds a value component without violating the equals contract
class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
// Returns the point-view of this color point.
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);
}
}
Point nesnesinde bir özellik eksik olduğundan true gelme ihtimali yoktur. ColorPoint nesnelerinde ise point eşit olsa bile color eşit olmadığında false dönecektir.
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Consistent (Tutarlı)
İki nesne eşitse, biri (veya her ikisi) değiştirilmediği sürece her zaman eşit kalmaları gerekir. Transitivity'nın aksine çok net ve temiz değil mi? Transitivity'i anlat anlat bitiremedim 😅. Burada önemli konu immutable (değişmez) kabul ettiğimiz değerlerin equals() metodlarınında buna göre yazılıyor olmasıdır. Bir değişkeni immutable yaparken aynı zamanda kullanıldığı equals() metodununda implementasyonunu yapacaksan göz ardı etmemelisin.
Non-nullity (Boş-olmama)
Hiç bir nesne null'a eşit olamaz ilkesine dayanır. Çok fazla detayına girmeyeceğim eğer daha önce yazılım geliştirdiysen muhakkak NullPointerException hatasını almışsındır. Temelinde tam olarak bu prensip yatar.
Konuyu sonlandırmadan önce size şöyle güzel bir özelliği aktarayım. Geliştirme yaparken pek çok yerde null değer kontrolü yaparız değil mi?
// Explicit null check - unnecessary!
@Override
public boolean equals(Object o) {
if (o == null) return false;
}
Burada bu kontrol gereksizdir. Evet gereksizdir. Bu işlemi equals() metodunu kullanıyor olsaydık nesnenin tipini belirlemek için instanceof kullanacaktık. instanceof nesne tiğini belirlerken aynı zamanda null olup olmadığı kontrolünüde yapmaktadır.
// Implicit null check - preferred
@Override
public boolean equals(Object o) {
if (!(o instanceof MyType)) return false;
}
Arrivederci 🙋🏻♂️
