Last updated on September 21, 2019
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 gelenequals()
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
metoduequals()
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ıylaequals()
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ştirimiequals()
metodunuAbstractSet
sınıfından alırlar, aynı şekildeList
gerçekleştirimleriAbstractList
,Map
gerçekleştirimleri deAbstractMap
sınıfından gelenequals()
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.
Singleton gibi nesne sayısının sınırlandırıldığı sınıflar söz konusu olduğunda (Madde 1) equals()
metodunu geçersiz kılmaya gerek yoktur, çünkü aynı mantıksal değere sahip birden fazla nesnenin yaratılamayacağı bellidir. Enum
türü (Madde 34) bu kategoriye girmektedir, 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 isex.equals(y)
de mutlaka true üretmelidir. - Geçişlilik: Null olmayan x, y ve z referansları için,
x.equals(y)
vey.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<>();
list.add(cis);
Bu durumda list.contains(s)
ne döndürecektir? Kim bilir? Şu anda güncel olan OpenJDK, bu durumda 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. (Madde 18) 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.compare(float, float)
ve double
için Double.compare(double, double)
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. Float.equals
dokümantasyonuna bakarak bu konuda daha detaylı bilgi alabilirsiniz. float
ve double
türleri karşılaştırmak için Float.equals
ve Double.equals
kullanmak mümkün olsa da, autoboxing devreye gireceği için performansı düşük olacaktır. Dizi türleri için (array), buradaki önerileri her bir elemana ayrı ayrı uygulayın. Eğer dizideki bütün alanlar anlamlıysa, Arrays.equals
metotlarından birini kullanabilirsiniz.
Bazı nesne referansları null değerini meşru bir biçimde alabilirler, bu durumda olası bir NullPointerException
hatasını engellemek için Objects.equals(Object, Object)
kullanabilirsiniz:
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.
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. Eğer AutoValue kullanarak equals()
metodunu otomatik oluşturduysanız, birim testleri yazmanıza gerek olmayabilir. 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 aşağıda verilmektedir:
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
... // Sınıfın geri kalanı çıkartılmıştır
}
equals()
metodunu geçersiz kılıyorsanız mutlakahashCode()
metodunu da edin. (Effective Java – Madde 11)
- 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ılarkenObject
sınıfını başka bir türle değiştirmeyin. Aşağıdaki gibi yazılmış birequals()
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ış birequals()
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) {
...
}
equals()
(ve hashCode
) metotlarını yazıp test etmek son derece meşakkatli bir iştir. Kendiniz bu işi yapmak yerine, Google tarafından geliştirilen AutoValue kütüphanesini kullanmak, bu işi çok basitleştirmektedir. Sınıf üzerinde tanımlayacağınız bir annotation ile bu metotların otomatik olarak üretilmesini sağlayabilirsiniz, otomatik olarak üretilen bu metotlar çoğu zaman sizin yazacağınız metotlarla birebir aynı olacaktır.
Kod düzenleyici programlar da (IDE) bu metotları otomatik üreten özelliklere sahiptirler. Ancak ürettikleri kod AutoValue kütüphanesine göre daha az okunabilir olmaktadır ve sınıftaki değişiklikleri otomatik olarak takip etmemektedir. Bu sebeple test edilmesi gerekmektedir. Ancak yine de, IDE tarafından üretilen metotları kullanmak kendiniz yazmaktan daha mantıklıdır, çünkü bu programlar insanlar gibi dikkatsizlik sonucu hata yapmazlar.
Özet olarak, equals()
metodunu mecbur değilseniz geçersiz kılmayın. Çoğu durumda, Object
sınıfından gelen davranış sizin ihtiyacınızı karşılayacaktır. Kendiniz yazıyorsanız da, yukarıda incelenen sözleşmenin bütün kurallarını sağladığınıza emin olun.
[…] sınıfı yazan kişi equals metodunu geçersiz kılmak istemiş (Madde 10) ve hatta hashCode‘u da geçersiz kılması gerektiğini unutmamış (Madde 11). Ancak, […]
Effective Java kitabını okumaya başladım ve sizin yazılarınızı gördüm. Çok faydalı bir çeviri olmuş. Her bir chapter’ı bitirdikten birkaç gün sonra bir de burdan gelip okuyorum ki atladığım hiçbir nokta kalmasın ve başka birisi bu yazıya ve maddelere hangi açıdan yaklaşıyor olduğunu görmek keyifli. Teşekkürler..