Java’da Enum Türleri

Merhabalar, Java dilinde Enum türlerini detaylarıyla anlatan Türkçe bir kaynak bulamadığım için bu yazıyı yazmaya karar verdim, umarım bu alandaki eksiği bir nebze olsun kapatır.

Java’da Enum türleri önceden tanımlanmış sabit değerleri ifade etmek için kullanılır. Peki bununla neyi kastediyoruz? Mesela en klasik örnek haftanın günleri. Bir haftanın kaç gün olduğu ve hangi günlerden oluştuğu bilindiğine göre bunu aşağıdaki gibi bir Enum türüyle ifade edebiliriz.

public enum Gun {
    PAZARTESI,
    SALI,
    CARSAMBA,
    PERSEMBE,
    CUMA,
    CUMARTESI,
    PAZAR
}

Read more “Java’da Enum Türleri”

Share

Effective Java Madde 10: 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.

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 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<>();
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 &amp;&amp; pn.prefix == prefix
                   &amp;&amp; pn.areaCode == areaCode;
    }
       ... // Sınıfın geri kalanı çıkartılmıştır
}
  • 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) {
...
}

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.

Share

Effective Java Madde 7: Erişilmeyen Nesnelerin Referanslarından Kurtulun

C veya C++ gibi bellek yönetimini yazılımcının yaptığı dilleri kullandıktan sonra Java gibi çöp toplayıcı (garbage collector) mekanizmasına sahip bir dile geçiş yaptığınız zaman, yazılımcı olarak işinizin ne kadar kolaylaştığını farkedersiniz çünkü çöp toplayıcı sizin için bellekte kalmış kullanılmayan nesneleri temizleyecektir. Bu durum size Java ile kodlama yaparken bellek yönetimini düşünmek zorunda olmadığınız izlenimini verebilir ancak bu doğru değildir!

Hemen aşağıdaki yığıt (stack) kodunu inceleyelim:

// Bellek sızıntısını bulabilir misiniz?
public class Stack {

    // Yığıt yazarken generic türler kullanmak daha mantıklı olacaktır
    // ama buradaki amacımız farklı olduğu için üzerinde durmuyoruz
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    /**
    * Dizide boşluk kalmamışsa, bir sonraki elemanın eklenebilmesini
    * sağlamak için diziyi genişletiyoruz
    */
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

İlk bakışta bu programda hiçbir hata yokmuş gibi görünüyor (yalnız daha doğru olan generic versiyonu için Madde 29’a bakın) Bu kodu test ettiğiniz zaman da bütün teslerden geçecektir ve istediğiniz gibi çalışacaktır. Ancak bu kod içerisinde kolay kolay farkedilmeyen bir “bellek sızıntısı” mevcut. Bellek sızıntıları çok sinsi bir şekilde zaman içerisinde etkisini artırarak, uygulamanın giderek daha fazla bellek tüketmesine ve çöp toplayıcının daha sık tetiklenmesine sebep olurlar. Ciddi boyuttaki bellek sızıntıları belirli bir süre sonra bellek yetersizliği yüzünden uygulamanın tamamen çökmesine bile sebep olabilir.

Peki bellek sızıntısı nerededir? Burada yığıtın boyutu önce artıp daha sonra azalırsa, yığıttan çıkartılan elemanlar çöp toplayıcı tarafından temizlenemeyecektir. Çünkü bu kod yığıttan çıkarılan nesnelerin referanslarını hala saklamaktadır. Çöp toplayıcı eğer bir nesneye işaret eden referans varsa, o nesnenin hala erişilebilir olduğunu düşünecek ve nesneyi bellekte bırakacaktır. Aslında yığıttan çıkarttığımız nesneye yukarıdaki sınıfı kullanarak tekrar erişmemiz mümkün değildir, ancak nesne referansı hala saklandığı için bellekten temizlenmeyecektir.

Erişilmediği halde başıboş bir biçimde bellekte kalan nesneler zincirleme bir etki yaratacağı için uygulamaya etkisi tahmin ettiğinizden daha büyük olabilir. Bellekte kalan bu nesneler kendi içerisinde başka nesnelere de referanslar tutacağı için, çok sayıda nesnenin çöp toplayıcı tarafından temizlenmesini engellerler. Yani birkaç tane nesne referansını gözden kaçırmak, aslında çok daha fazla sayıda nesnenin bellekten temizlenmesine engel olabilir, bu da ciddi sorunlara sebebiyet verebilir.

Bu tarz problemleri çözmek için dikkatli olmak ve kullanılmayan referanslara null değerini atamak yeterli olacaktır. Yukarıdaki örneği göz önüne alırsak, pop() metodunun aşağıdaki gibi değiştirilmesi sorunu çözecektir.

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;    
    return result;
}

Burada artık ihtiyacımız olmayan referansa null değerini atadık ve böylece yığıttan çıkartılmış olan nesneye işaret eden referanstan kurtulmuş olduk. Bu durumda çöp toplayıcı bir sonraki sefer tetiklendiğinde bu nesneye ait hiçbir referans kalmadığını görecek ve bellekten silecektir.

Şunu belirtmekte çok büyük fayda var: bir nesne referansını null atayarak işlevsiz hale getirmek genellikle uygulanan bir yöntem değil, tam tersine istisnai bir durumdur. Kullanılmayan nesne referanslarından kurtulmanın en güzel yolu, bu referansların kapsama alanından (scope) çıkmasını sağlamaktır. Her referansı mümkün olan en küçük kapsama alanı (scope) içerisinde tanımlarsanız bu otomatikmen gerçekleşmiş olur. (Madde 57)

Peki o zaman hangi durumlarda nesne referanslarına açık bir şekilde null değerini atamamız gerekir? Yukarıda verdiğimiz yığıt örneğinde neden bunu kendimiz yapmak zorunda kaldık? Bunun sebebi yığıt sınıfının belleği kendisinin yönetmeye çalışmasıdır. Burada yönetilen bellek elements dizisi içerisindeki nesne referanslarıdır. Yığıt içerisinde o anda aktif olarak bulunan referanslar (indeksi 0 ile size değeri arasında kalan referanslar) erişilebilen bellek öğelerine işaret eder, indeksi size değerine eşit veya daha büyük olan referanslar ise artık erişilemeyen, yığıttan çıkartılmış ve dolayısıyla artık başıboş kalmış nesnelere işaret etmektedir. Ancak bu nesnelerin başıboş kaldığını çöp toplayıcının anlamasına imkan yoktur, bu yüzden bizim açıkça bu referanslara null değeri atayarak geçersiz hale getirmemiz gerekir.

Genel olarak konuşmamız gerekirse, eğer bir sınıf kendi içerisinde bellek yönetimi yapıyorsa o sınıfta bellek sızıntılarına karşı dikkatli olmamız gerekir. Böyle bir durumda eğer bir nesne artık erişilemiyorsa, o nesneye işaret eden referansa null değeri atanmalıdır.

Bellek sızıntılarının ikinci bir nedeni önbellekte (cache) unutulan nesne referanslarıdır. Unutulan nesne referansları önbellekteki nesne geçersiz kalsa bile nesnenin sonsuza kadar bellekte kalmasına sebep olacaktır. Bu sorunu aşmak için çeşitli yöntemler kullanılabilir. Bunlardan birincisi eğer önbellek ihtiyacınızı karşılıyorsa normal HashMap yerine WeakHashMap kullanmak olabilir. Bazı durumlarda da önbellekte belirli bir süre erişilmemiş nesneleri temizleyerek yer açmak mümkün olabilir. LinkedHashMap sınıfının removeEldestEntry metodu bu durumda size yardımcı olabilir. Bunların dışında bir önbellek kütüphanesi (Memcache gibi) kullanarak, nesneleri önbelleğe yazarken ne kadar süreyle bellekte kalacağını belirleyebilirsiniz. Belirlediğiniz süre dolduktan sonra nesne bellekten otomatik olarak temizlenecektir.

Bellek sızıntıları programdaki diğer hatalar gibi kolaylıkla kendini göstermeyeceği için yıllar boyunca farkedilmeyebilir. Ancak dikkatli kod okumak ve bir takım performans ölçüm araçlarının yardımıyla bulunabilirler. Bu sebeple bellek sızıntılarının hangi durumlarda ortaya çıkabileceğini bilip ona göre kod yazmak önemlidir.

Share

Effective Java Madde 1: Yapıcı Metot (Constructor) Yerine Statik Fabrika Metotlarını Kullanın

Effective Java kitabını duymuşsunuzdur, Joshua Bloch efsane kitabında madde madde sizlere nasıl kaliteli Java kodu yazacağınızı anlatır. Bu kitapta anlatılanlarla ilgili yüzlerce İngilizce kaynak bulmak mümkün ama ben Türkçe bir kaynak bulamayınca sizlere bu kitabı özetlemeye karar verdim. Direk kitabın tam çevirisi olmasa da kendimce eklemeler çıkarmalar yaparak ve önemli gördüğüm yerleri vurgulayarak kitaptaki bölümlerden daha kısa ama aşağı yukarı aynı şeyi anlatan yazılar paylaşacağım. Başlangıç olarak kitaptaki ilk madde olan statik fabrika metotlarıyla başlıyoruz.

Normal şartlarda bir sınıf kendisinden nesne oluşturulmasını istiyorsa public bir sınıf yapıcı (constructor) tanımlar ve diğer sınıflar bunu kullanarak nesne oluşturabilir. Ancak her yazılımcının bilmesi gereken başka bir nesne yaratma yöntemi daha var. Bu yöntemde sınıf, dönüş değeri kendi nesnesi olan statik bir fabrika metodu (static factory method) tanımlar ve bu sınıftan nesne oluşturmak isteyenler bu metodu kullanırlar.

Read more “Effective Java Madde 1: Yapıcı Metot (Constructor) Yerine Statik Fabrika Metotlarını Kullanın”

Share

Java Servisinin Kendi Kendine Durması Sorunsalı

Çalıştığım şirkette eski de olsa Glassfish 2.1.1 sunucusu üzerinde barındırdığımız bir uygulamamız var. Bütün sistemi daha yeni ve hızlı bir ortama taşımamız gerektiğinde Glassfish uygulama sunucusu yeni donanımlar üzerine yüklendi ve gerekli ayarlamaları yapıldı. Ancak test aşamasında farkettik ki Glassfish sunucusunun yüklü olduğu makinede kullanıcı oturumunu kapattığı anda uygulama yanıt vermeyi kesiyor. Biraz araştırma yapınca farkettik ki kullanıcı makinede oturum kapattığı anda java.exe servisi çalışmayı durduruyor.

Google sağolsun çözümü bulmak çok uzun sürmedi. Meğerse kullanıcı oturum kapattığı anda JVM işletim sisteminden gelen bir sinyalle kendini durduruyormus. Bu davranışı devre dışı bırakmak için JVM parametrelerini biraz kurcalamak gerekti. JVM’yi -Xrs parametresiyle çalıştırdıgımız zaman kullanıcı oturum kapatsa bile JVM arka planda çalışmaya devam edecektir. Bizim sorunumuz Glassfish sunucusu ile olduğu için domain.xml dosyasına -Xrs parametresini de ekleyerek sorunu çözdük.

Ancak dikkat edilmesi gereken bir nokta var. -X ile başlayan JVM parametreleri standart değil yani her JVM gerçekleştiriminde yer almayabilir veya olsa bile aynı şekilde davranmayabilir. Komut satırında java -X komutunu girerseniz sizin JVM’nizin hangi parametreleri desteklediğini görebilirsiniz.

Share

Java’da Final Anahtar Kelimesi ve Kullanımı

Java dilinde detayları çok iyi bilinmeyen ve belki de pek önemsenmeyen, kıyıda köşede kalmış konulardan birisi de “final” anahtar kelimesidir. Değişkenlere, metodlara ve hatta sınıflara uygulanabilen, kullanıldığı zaman çok faydalı olabilecek bu anahtar kelimenin bütün kullanım detaylarını bu yazıda açıklamaya çalışacağım.

Read more “Java’da Final Anahtar Kelimesi ve Kullanımı”

Share

JVM – İçeride Neler Oluyor?

Java ile uğraşan hemen herkes JVM (Java Virtual Machine) hakkında az çok bilgiye sahiptir. Basit olarak söylemek gerekirse JVM, yazdığımız java uygulamalarını çalıştıran sanal bir makinedir. Peki bu sanal makinenin içerisinde arka planda neler döndüğünü hiç merak ettiniz mi? Bu yazıda programcıların yazdığı .java uzantılı bir kod dosyasının derlendikten sonra hangi aşamalardan geçerek çalıştırıldığını anlatmaya çalışacağım. JVM ile ilgili daha basit düzeyde detaylı bilgi almak istiyorsanız bu adresteki yazımı okuyabilirsiniz.

Bir java programcısının yazdığı .java uzantılı dosya, Java derleyicisi tarafından derlenerek çalıştırılmaya hazır .class uzantılı bir “bytecode” dosyasına dönüştürülür. Bu aşamadan sonra programın çalıştırılması işini JVM yapar. JVM bir Java programını çalıştırmadan önce “yükleme”, “bağlama” ve “ilklendirme” olmak üzere 3 aşamadan geçirir. Şimdi sırasıyla bunları inceleyelim.

Read more “JVM – İçeride Neler Oluyor?”

Share

Java’da Static Anahtar Kelimesi ve Kullanımı

Java öğrenmeye çalışanların kafasını karıştıran konulardan birisi de static değişkenler ve metotlardır. Aslında kullanım mantığı çok basit olan static anahtar kelimesi gereksiz yere birçok kişinin kafasını karıştırmaktadır. Bu yazıda Java dilinde static kelimesinin kullanım alanlarını çeşitli örnekler vererek alt başlıklar halinde inceleyeceğiz.

Read more “Java’da Static Anahtar Kelimesi ve Kullanımı”

Share

Java Virtual Machine Nedir?

JVM’nin basit olarak Java programlarını çalıştıran sanal bir makine olduğunu biliyor olabilirsiniz ancak yine de bu yazıyı okumanızı tavsiye ederim. Çünkü JVM hakkında bilinmesi gereken çok ama çok şey var. Bu yazıda bunların bir kısmına değinmeye çalışacağım.

Bildiğiniz gibi Java derleyicisi doğrudan fiziksel bir makinenin çalıştırabileceği makine kodları değil, sadece Java Sanal Makinesinin anlayıp çalıştırabileceği formatta bir kod üretir. Ara bir dil olarak da tanımlanabilen “bytecode”, class uzantılı dosyalar içinde saklanır. Java derleyicisinin işi burada biter ve bundan sonra ikili (binary) formatta kodlanmış olan class dosyalarının çalıştırılması JVM tarafından yapılır.

Read more “Java Virtual Machine Nedir?”

Share

Class Dosyalarının Anatomisi

Java dilinde yazdığımız programların derlendiğinde .class uzantılı dosyalara dönüştüğünü Java ile uğraşan hemen herkes bilir. Ancak class dosyaları bir çoğumuz için bugüne kadar hep içini bilmediğimiz, hakkında bilgi sahibi olmadığımız birer muamma olarak  kaldılar. Bu yazıda class dosyalarının yapısını inceleyip yazdığımız kodların derlendikten sonra nasıl bir şekle dönüştüğünü açıklamaya çalışacağım.

Read more “Class Dosyalarının Anatomisi”

Share