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 8: Finalizer (Sonlandırıcı) ve Cleaner (Temizleyici) Kullanmaktan Kaçının

Java’da sonlandırıcılar (finalizer) çoğu durumda gereksiz, tehlikeli ve tutarsız davranışlar sergileyen metotlardır. Kullandığınız taktirde anlam veremediğiniz hatalı davranışlar, kötü performans ve taşınabilirlik (portability) sorunlarıyla karşılaşabilirsiniz. Sonlandırıcı kullanmak sadece bir iki durumda işe yarayabilir, onları da yazıda göreceğiz. Ancak genel bir kural olarak sonlandırıcı kullanmaktan sakınmalıyız. Java 9 ile beraber, sonlandırıcılar kullanımdan kaldırılmıştır (deprecated) ancak hala Java kütüphaneleri tarafından kullanılmaktadır. Java 9 sonlandırıcılar yerine temizleyicileri (cleaner) kullanıma sunmuştur. Temizleyiciler her ne kadar sonlandırıcılar kadar tehlikeli olmasa da, yine de davranışlarını tahmin etmek zordur, yavaş çalışırlar ve çoğu zaman gereksizdirler.

Öncelikle sonlandırıcı derken tam olarak neyi kastettiğimizi söyleyelim. Java’da bütün sınıfların atası olan Object içerisinde aslında hiçbir iş yapmayan finalize() metodu vardır. Eğer yazılımcı nesne bellekten temizlenmeden önce bir takım kaynakları serbest bırakmak istiyorsa, bu metodu override edebilir. Teorik olarak çöp toplayıcı, bir nesne tamamen erişilemez duruma geldiğinde bu metodu çağıracak ve sistemden silinmeden önce finalize() metodunun içeriği işletilecektir. Ancak pratikte bu yaklaşım çalışmamakta ve uygulamada ciddi sorunlara yol açmaktadır.

C++ programcıları Java’daki sonlandırıcıları C++’taki destructor ile aynı zannedebilirler ancak bu yanlıştır. C++ için destructor kullanılan kaynakları serbest bırakmanın normal yoludur ve gereklidir. Java’da ise çöp toplayıcı mekanizması, bellekte kalmış artık kullanılmayan kaynakları algılar ve serbest bırakır, bunun için yazılımcının birşey yapmasına gerek yoktur. Bellek dışı kaynaklar için – mesela dosyalar – C++’da yine destructor kullanırken Java’da try-finally veya try-with-resources blokları kullanılır. (Madde 9)

Sonlandırıcıların ve temizleyicilerin en büyük dezavantajı ne zaman işletileceklerinin belli olmamasıdır. Bir nesnenin tamamen erişilemez duruma gelmesiyle, sonlandırıcı veya temizleyicinin çalıştırılması arasında uzun bir zaman geçebilir. Bu da demektir ki hemen çalıştırılmasını istediğiniz kodları asla sonlandırıcı veya temizleyici içerisinde kullanmayın! Örneğin, dosyalarla çalıştığınız bir uygulamada sık sık dosya açıyor ve kullandığınız dosyaları kapatmak için sonlandırıcı veya temizleyici kullanıyorsanız, programınız kısa bir süre sonra çökebilir çünkü dosyaların kapanması hemen gerçekleşmez. Çok sayıda dosya açık bırakıldığı zaman sistem yeni dosyalar açmanıza izin vermeyecektir.

Sonlandırıcı ve temizleyicilerin ne zaman çalışacağı tamamen çöp toplayıcı (garbage collector) algoritmasının insiyatifindedir. Bu da farklı JVM’lerde farklı şekillerde ele alınabilir. Sonlandırıcı veya temizleyici kullanarak yazdığınız bir uygulamanın sizin bilgisayarınızda mükemmel çalışıp, en önemli müşterinizin bilgisayarında çökmesi son derece olası bir senaryodur.

Burada anlatılanlar teorik bir problemden ibaret değildir. Bir sınıf için sonlandırıcı tanımlamak, nesnelerinin bellekten temizlenmesini uzun süre geciktirebilir. Bir iş arkadaşım, zaman zaman esrarengiz bir şekilde OutOfMemoryError hatası vererek çöken bir GUI uygulamasındaki hatayı bulmaya çalışırken, uygulama çöktüğü sırada binlerce grafik nesnesinin sonlandırılmak için sırada beklediğini farketti. Sonlandırıcı threadi uygulama threadlerinden daha düşük önceliğe sahip olduğu için, uygulamanın yarattığı nesneler aynı hızla temizlenmiyor, bu da belleğin bir süre sonra dolup uygulamanın çökmesine sebep oluyordu. Java dili, hangi threadin sonlandırıcıları çalıştıracağını belirlemediği için, bu tarz bir problemi engellemek için yapılabilecek tek şey sonlandırıcı kullanmaktan kaçınmaktır. Temizleyiciler bu konuda biraz daha avantajlıdır çünkü temizleyici threadleri sınıfı yazan kişinin kontrolü altındadır. Ancak yine de temizleyiciler arka planda, çöp toplayıcının kontrolünde çalışmaktadırlar. Bu yüzden de ne zaman çalıştırılacakları kestirilemez.

Java dili sonlandırıcıların ve temizleyicilerin ne zaman işletileceğine dair hiçbir garanti vermemenin yanında, işletip işletilmeyeceğini bile garanti etmez. Bir uygulama, sonlandırıcı ve temizleyicileri çalıştırmadan çıkış yapabilir. Bu sebeple, kalıcı olmasını istediğiniz güncellemeler için asla sonlandırıcı veya temizleyicilere güvenmeyin. Örneğin, bir veritabanı kilidini (database lock) serbest bırakmak için sonlandırıcı veya temizleyici kullanmak, dağıtık bir sistemi çalışmaz hale getirmek için çok iyi bir yoldur.

System.gc() ve System.runFinalization() metotları da gözünüzü boyamasın. Bu metotlar sonlarıcıların çalışma ihtimalini artırsa da asla bir garanti veremez. Sonlandırıcıların çalışmasını garanti edebilen iki tane şeytani metot vardır: System.runFinalizersOnExit() ve Runtime.runFinalizersOnExit. Bu iki metodu kullanmak uygulamada daha ciddi sorunlara yol açabileceği için deprecate edilmiştir.

Sonlandırıcılar ile ilgili bir başka problem de şudur: eğer sonlandırma esnasında bir istisna (exception) oluşursa ve bunu yakalamazsanız, bu istisna tamamen görmezden gelinir. Normalde bir istisna oluştuğunda işletim durur ve stack trace basılır, ancak sonlandırıcı kullanıldığında bir uyarı bile göremezsiniz. Temizleyiciler için bu problem söz konusu değildir çünkü yazan kişi bunu kontrol edebilir.

Sonlandırıcı kullanmak uygulama performansını da kötü yönde etkiler. Benim bilgisayarımda, basit bir AutoCloseable nesneyi yaratıp try-with-resources ile kapatmak, sonrasında da çöp toplayıcının bu nesneyi bellekten temizlemesi 12ns sürdü. Sonlandırıcı kullanınca ise 550ns sürdü, yani neredeyse 50 kat daha yavaş çalışıyor. Bunun sebebi, sonlandırıcıların çöp toplayıcının etkin çalışmasını engellemesidir. Temizleyiciler de, eğer sınıfın bütün nesnelerini temizlemek için kullanırsanız neredeyse sonlandırıcılar kadar yavaş çalışır. Ancak aşağıda anlatıldığı gibi sadece bir güvenlik önlemi olarak kullanılırsa, nesneyi yaratmak, temizleyiciyi çalıştırmak ve bellekten temizlemek 66ns sürmektedir, yani 50 kat yerine 5 katlık bir bedel ödemiş olursunuz.

Sonlandırıcılar aynı zamanda ciddi bir güvenlik açığına da sebebiyet verirler. Sonlandırıcı saldırısı (finalizer attack) olarak bilinen bu yöntemin çalışma mantığı çok basittir. Eğer bir sınıfın yapıcı metodu istisna (exception) fırlatıyorsa, bu sınıf kalıtılıp kötü niyetli bir çocuk sınıfta finalize() metodu yazılabilir. Bu finalize() metodu da yapıcı metodu tamamlanamamış olan ata sınıfın referansını statik bir alanda kaydederek nesnenin çöp toplayıcı tarafından bellekten silinmesini engelleyebilir. Böylece, hiç yaratılmaması gereken bu ata sınıfın bir referansını, çocuk sınıfta elde etmiş oluyoruz. Bu referansı kullanarak da yine ata sınıftaki istediğimiz metotları çağırabiliriz. Yapıcı metot içerisinde istisna fırlatmak, bir nesnenin yaratılmasını engellemek için yeterli olmalıdır, ancak sonlandırıcılar işin içine girince bu durum değişebiliyor. Bu saldırıdan korunmak için sınıfı final tanımlamak yeterli olacaktır, çünkü bu durumda hiç kimse sınıfı kalıtamayacaktır. Final olmayan sınıfları korumak istiyorsanız da hiçbir şey yapmayan final bir finalize() metodu yazabilirsiniz. (Bu konuyla ilgili daha geniş bilgi ve kod örnekleri için buraya bakabilirsiniz)

Peki, yazdığımız sınıf veritabanı bağlantısı gibi kaynakları kullanıyorsa ve bunların nesne bellekten silinmeden önce serbest bırakılması gerekiyorsa ne yapmalıyız? Böyle durumlarda yapılması gereken AutoClosable arayüzünü gerçekleştirip istemcilerin işleri bittiğinde close() metodunu çağırmalarını istemektir. Genellikle bu try-with-resources yapılır (Madde 9), böylece bir hata oluşsa bile (exception) close() metodu çağrılarak gerekli kaynaklar serbest bırakılır. Burada önemli bir detaydan bahsedebiliriz. close() metodu çağrıldıktan sonra, bu nesnenin artık geçerli olmadığı bilgisi kaydedilmelidir ve sınıfın diğer metotları da nesne eğer geçerli değilken çağırılırsa IllegalStateException fırlatmalıdır.

Peki o zaman sonlandırıcı veya temizleyici kullanmak ne zaman mantıklı olabilir? Bunun bir örneğini FileInputStream, FileOutputStream, java.sql.Connection sınıflarında görebilirsiniz. Bu sınıflar AutoCloseable olmalarına rağmen, sonlandırıcı da tanımlamışlardır. Bunun sebebi ise sınıfı kullanan istemcilerin close() metodunu çağırmayı unutmaları durumunda, her ne kadar çalışıp çalışmayacağı belli olmasa da ‘hiç yoktan iyidir’ mantığıdır. Eğer siz de böyle bir durum için sonladırıcı veya temizleyici yazmak isterseniz, uzun uzun düşünün çünkü bu aynı zamanda uygulama için bir ek maliyettir.

Temizleyici ve sonlandırıcıların ikinci ve son anlamlı kullanım yeri Java içerisinden C/C++ gibi bir dildeki kodları çalıştırdığınızda ortaya çıkar. Burada ‘native peer’ olarak adlandırılan nesneler normal Java nesneleri olmadığı için çöp toplayıcı bunları temizleyemez. Bu durumda sonlandırıcı veya temizleyici kullanmak işinize yarayabilir.

Sonlandırıcı yazarken dikkat edilmesi gereken kurallar kitabın 2. baskısında var ancak 3. baskıda çıkartılmış. Ben belki birilerine faydası dokunur diye o kısmı da ekledim.

Diyelim ki yukarıdaki iki durumdan birisi bizim için geçerli ve sonlandırıcı yazmamız gerekiyor. Bunu yaparken de dikkat edilmesi gereken noktalar var. Mesela bir ata sınıfta sonlandırıcı yazdıysanız ve çocuk sınıf bunu override ediyorsa, çocuk sınıftan ata sınıftaki sonlandırıcıyı kendiniz çağırmanız lazım. Aksi taktirde çocuk sınıfın sonlandırıcısı çalışacak ancak ata sınıftaki çalışmayacaktır. Ata sınıfın sonlandırıcısını çağırma işini de finally bloğu içerisinde yaparak her durumda çağrılmasını sağlayabilirsiniz.

// Zincirleme sonlandırıcı işletimi
@Override
protected void finalize() throws Throwable {
    try {
        ... // çocuk sınıfı sonlandıracak işlemler buraya..
        } finally {
            super.finalize();
        }
    }
}

Yukarıdaki gibi ata sınıf sonlandırıcısı çocuk sınıftan açıkça çağrılmazsa ata sınıfın sonlandırıcısı asla işletilmeyecektir. Bir dikkatsizlik sonucu kolayca yapılabilecek bu hatadan korunmak için finalizer guardian denilen teknik kullanılabilir. Aşağıdaki kodu inceleyelim:

// Finalizer Guardian tekniği
public class Foo {

    // bu nesnenin tek görevi dışarıdaki Foo nesnesini sonlandırmaktır
    private final Object finalizerGuardian = new Object() {
        @Override
        protected void finalize() throws Throwable {
            ... // dışarıdaki Foo nesnesini burada sonlandırın
        }
    };
    ...  // Foo sınıfının devamı
}

Yukarıdaki kod nasıl çalışmaktadır? Gördüğünüz üzere Foo sınıfının içerisinde bir nesne tanımlayıp finalize() metodunu override ediyoruz ancak Foo sınıfına ait bir sonlandırıcı yok. Bu durumda çöp toplayıcı Foo nesnesini temizleyeceği zaman finalizerGuardian nesnesini de temizlemesi gerekmektedir ve bu yüzden de finalizerGuardian içindeki finalize() işletilecektir. Biz normalde Foo sınıfının sonlandırıcısı içinde yapacağımız sonlandırma işlemlerini finalizerGuardian içerisinde taşırsak aynı işi yapmış oluyoruz, dolayısıyla bu sınıfı başka bir sınıf kalıtırsa çocuk sınıfın super.finalize() ile ata sınıfın sonlandırıcısını çağırmasına gerek kalmayacaktır. Böylece olası bir programlama hatasının önüne geçmiş oluyoruz.

Temizleyicilerin kullanımı biraz daha farklıdır. Farzedelim ki uygulamamızda odaları temsil eden sınıflar olsun (Room) ve odaların bellekten silinmeden önce temizlenmesi gerekiyor olsun. Tıpkı bir FileInputStream ile işimiz bittiğinde sistem kaynaklarını serbest bırakmak için close() metodunu çağırmamız gerektiği gibi. Bu sebeple, aşağıdaki Room sınıfı AutoClosable arayüzünü uygulamaktadır ancak bunun yanında önlem olsun diye bir de temizleyici bulundurmaktadır.

// Önlem amaçlı temizleyici kullanan, AutoCloseble bir sınıf
public class Room implements AutoCloseable {
    
    private static final Cleaner cleaner = Cleaner.create();

    // Temizlenmesi gereken kaynak, Room referansı içermemelidir
    private static class State implements Runnable {
        int numJunkPiles; // temizlenmesi gereken çöp sayısı
        State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }
        // close() metodu veya temizleyici tarafından işletilir
        @Override 
        public void run() {
            System.out.println("Oda temizleniyor");
            numJunkPiles = 0;
        }
    }
    
    // Odanın (Room) o anki durumunu temsil eder
    private final State state;
    
    private final Cleaner.Cleanable cleanable;
    public Room(int numJunkPiles) {
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }
       
    @Override 
    public void close() {
        cleanable.clean();
    } 
}

Burada static State sınıfı temizleyici tarafından temizlenmesi gereken kaynakları tutmaktadır. Bu örnekte çok basit olarak numJunkPiles isminde, temizlenecek çöp sayısını tutan bir int değişkeni tutsa da, daha gerçekçi bir uygulamada final long bir değişken içerisinde, yukarıda bahsettiğimiz gibi bir ”native peer” değişkene gösterge tutabilir.

State sınıfı, Runnable arayüzünü uygulamaktadır. Buradan gelen run() metodu nesnenin temizlenmesi için gerekli kodu çalıştırmaktadır. Room yapıcı metodu içerisinde elde ettiğimiz cleanable referansı üzerinden clean() metodu çağrıldığında, aslında state.run()metodu işletilmektedir. Çünkü cleanable elde etmek için state nesnesini kullandık. (26. satır)

Burada run() metodunu işletmek için iki yol vardır. Birincisi ve daha yaygın olanı, Room nesnesi üzerinden close() metodunu çağırmak olacaktır. Bu sınıf AutoClosable olduğu için try-with-resources ile kullanıldığında bu otomatik olarak gerçekleşecektir. İkinci yol ise, istemci close() metodunu çağırmayı unutursa gerçekleşir. Bu durumda cleaner nesnesi, çöp toplama (garbage collection) yapılırken, run() metodunu işletecektir, ancak uygulama çöp toplanmadan önce çıkış yaparsa hiç çalışmayabilir.

Burada başka önemli bir nokta da, State nesnesinin herhangi bir Room referansı tutmamasıdır. Eğer tutsaydı, ikisi arasında oluşacak döngüsel bağdan ötürü Room nesnesi hiçbir zaman çöp toplayıcı tarafından temizlenemezdi. Bu sebeple, State mutlaka statik bir iç sınıf (static nested class) olmak zorundadır. Statik olmayan iç sınıflar, dışarıdaki sınıfın referanslarını tuttukları için bu durumda kullanılamazlar. (Madde 24) Aynı sebepten ötürü, lambda fonksiyonlarının kullanımı da tavsiye edilmez.

Daha önce de belirttiğimiz gibi, Room içerisindeki cleaner sadece ”en kötü durumda kullanılacak son çare” olmak üzere tasarlanmıştır. Eğer istemciler, Room nesnelerini aşağıdaki gibi try-with-resources blokları içerisinde yaratırlarsa, cleaner hiçbir zaman devreye girmeyecektir.

public class Adult {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.println("Güle güle");
        } 
    }
}

Tahmin edeceğiniz gibi, yukarıdaki sınıf önce "Güle güle" sonra da "Oda temizleniyor" çıktısını üretecektir. Peki ya aşağıdaki sınıf?

public class Teenager {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("Güle güle");
    }
}

Yukarıdaki uygulama benim bilgisayarımda sadece "Güle güle" çıktısını üretip sonlanıyor, "Oda temizleniyor" asla üretilmiyor. Bunun sebebi daha önce de bahsettiğimiz belirsizlikten kaynaklanıyor. Uygulama sonlandığında, temizleyicilerin çalışıp çalışmayacağı tamamen JVM gerçekleştirime bağlıdır. Cleaner dokümantasyonu bunu açıkça belirtmektedir. Benim bilgisayarımda, yukarıdaki main metodu içerisine System.gc() ekleyerek çöp toplama işlemini tetikleyince, "Oda temizleniyor" çıktısını görebiliyorum. Ancak sizin de aynı davranışı göreceğinizin bir garantisi yoktur.

Özet olarak, yukarıda bahsettiğimiz iki durum haricinde sonlandırıcı veya temizleyici kullanmaktan kaçının. Eğer kullanmanız gerekirse de, uymanız gereken kuralları dikkatlice uygulayın ve belirsizliklere, performans kayıplarına karşı bilinçli 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 6: Gereksiz Nesne Yaratmaktan Kaçının

Yazılım geliştirirken var olan bir nesneyi kullanmak, işlevsel olarak aynı işi yapan yeni bir nesne yaratmaktan genellikle daha faydalıdır. Nesnelerin yeniden kullanımı hem uygulamayı hızlandıracak hem de daha okunabilir bir kod yazmanızı sağlayacaktır. Eğer bir nesne değiştirilemez (immutable) ise, o nesne her zaman yeniden kullanılabilir.

Aşağıdaki kod gereksiz nesne yaratılmasına bir örnektir:

String s = new String("merhaba")   // YANLIS KULLANIM!!

Yukarıdaki kod her çalıştırıldığında, içeriği aynı olan yeni bir String nesnesi yaratılacaktır. Tırnak içerisindeki “merhaba”, Java’da zaten bir String nesnesini ifade eder ve gereksiz yere yapıcı metot (constructor) kullanarak yarattığımız bütün nesnelerle işlevsel olarak aynıdır. Dolayısıyla yukarıdaki kodun bir döngüde çalıştırıldığını düşünürseniz çok fazla sayıda gereksiz String nesnesi yaratılmış olur. Bu durumu engellemek için aynı kodu aşağıdaki gibi yazabiliriz:

String s = "merhaba"   // DOGRU KULLANIM!

Bu şekilde bir kullanım String nesnesini sadece bir kere yaratacak ve program içerisinde bu satırı kaç kere çalıştırırsanız çalıştırın aynı nesneyi kullanacaktır. Hatta programın başka yerinde bile aynı “merhaba” stringini kullansanız, yine de yeni bir nesne yaratılmayacaktır. JVM string nesneleri üzerinde böyle bir optimizasyon yapabilmektedir, bunun sebebi ise Java’da stringlerin immutable (değiştirilemez) olmasıdır. Eğer bu okuduklarınız kafanızda bir lamba yakmadıysa Java’da stringlerin nasıl ele alındığını küçük bir örnekle görmekte fayda var. (Not: Bu kısım kitapta yok, bilmeyenler olabilir diye eklemek istedim.)

String a = "abc";
String b = "abc";
System.out.println(a == b);  // True (a ve b referansları aynı nesneye işaret etmektedir)

String c = new String("abc");
String d = new String("abc");
System.out.println(c == d);  // False (c ve d referansları farklı nesnelere işaret etmektedir)

Yukarıdaki kodu incelerseniz, JVM’nin string değerlerini nasıl ele aldığını tahmin edebilirsiniz. Siz kodda “abc” string değerini kullandığınız zaman JVM bu değeri bellekte özel bir yere yazar ve o program çalıştığı sürece ne zaman “abc” kullanılırsa aynı nesneyi size döndürmeye devam eder. Ancak siz new String("abc") şeklinde String yaratırsanız her seferinde gereksiz yere nesne yaratmış olursunuz. Daha fazlasını merak eden varsa bu işleme String interning denmektedir ve buradan detaylıca okuyabilirsiniz.

Gereksiz nesne yaratmaktan kaçınmanın bir diğer yöntemi de statik fabrika metotları kullanmaktır. (Madde 1) Değiştirilemez (immutable) sınıflarla çalışıyorsanız yapıcı metotlar (constructor) yerine statik fabrika metotlarını kullanmak gereksiz nesne yaratmayı engelleyecektir. Örneğin, Boolean sınıfı için yapıcı metot olan Boolean(String) yerine Boolean.valueOf(String) kullanmak her zaman tercih edilir çünkü yapıcı metot her seferinde nesne yaratırken, statik fabrika metodu yaratmayacaktır. Boolean(String) yapıcı metodu zaten Java 9’da deprecate edilmiştir. Bunlara ek olarak, eğer kullandığınız nesnelerin kod içerisinde değiştirilmeyeceğini biliyorsanız, bu nesneler değiştirilebilir (mutable) olsa bile yeniden kullanabilirsiniz.

Bazı nesneleri yaratmak başka nesnelere kıyasla çok daha pahalı olabilir. Böyle bir ”pahalı nesneye” tekrar tekrar ihtiyaç duyduğunuz durumlarda, her seferinde yeniden yaratmak yerine bellekte tutup yeniden kullanmak tavsiye edilir. Ancak bazı durumlarda pahalı bir nesne yaratıyor olsanız da bunun farkında olmayabilirsiniz. Mesela parametre geçtiğimiz stringin bir Roma rakamı olup olmadığını test eden bir metot yazdığımızı düşünelim. Kurallı ifade (regular expression) kullanarak yazılabilecek en basit metot aşağıdaki gibidir:

// Performans büyük oranda artırılabilir!
static boolean isRomanNumeral(String s) {
    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
           + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

Buradaki problem gerçekleştirimin String.matches() metoduna dayanıyor olmasıdır. String.matches() her ne kadar bir string değerinin bir kurallı ifade ile eşleşip eşleşmediğini kolayca test etse de, yüksek performans gerektiren uygulamalarda sık kullanımı uygun değildir. Çünkü kendi içerisinde bir Pattern nesnesi yaratıp sadece bir kez kullanmaktadır, daha sonra bu nesne çöp toplayıcı (garbage collector) tarafından temizlenmeyi bekleyecektir. Bu Pattern nesnesi de kurallı ifadeyi kendi içerisinde bir sonlu durum makinesine (finite state machine) dönüştürdüğü için yaratılması çok pahalı olmaktadır.

Performansı artırmak için, sınıf başlatılırken kurallı ifadeyi kendimiz bir Pattern nesnesine (bu nesne değiştirilemez bir nesnedir) dönüştürüp bellekte tutabiliriz ve isRomanNumeral her çağrıldığında yeniden kullanabiliriz.

// Performans kazanımı için pahalı nesneyi yeniden kullanıyoruz
public class RomanNumerals {
    private static final Pattern ROMAN = Pattern.compile(
               "^(?=.)M*(C[MD]|D?C{0,3})"
               + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    } 
}

Yukarıdaki geliştirilmiş versiyon, çok sayıda çalıştırıldığı zaman ciddi manada performans kazanımı sağlamaktadır. Benim makinemde (Joshua Bloch’un makinesi), orijinal versiyon 8 karakterli bir string için 1.1 μs sürerken, geliştirilmiş kod aynı şartlarda sadece 0.17 μs içinde aynı sonucu üretmektedir. Bu da 6.5 kat daha hızlı olduğunu göstermektedir. Burada sadece performans kazanımı değil aynı zamanda anlaşılırlığı da artırmış oluyoruz. Daha önce görmediğimiz Pattern nesnesine burada bir isim verme şansımız olmaktadır ve bu da kurallı ifadeyi okuyup anlamaktan çok daha kolaydır.

Bu sınıf başlatılırsa (initialize) ancak isRomanNumeral metodu hiç çağrılmazsa, ROMAN statik alanı gereksiz yere oluşturulacaktır. Bunu da ortadan kaldırmak için bu ilklendirme işlemini sınıf başlatılırken değil de isRomanNumeral ilk kez çağrıldığında tetiklemek mümkündür (Madde 83), ancak bu tavsiye edilmez. Bunun sebebi ise çok küçük bir iyileştirme olmasına karşın kodu daha karmaşık bir hale getirmesidir. (Madde 67)

Bir nesne değiştirilemez (immutable) ise, tekrar tekrar güvenle kullanılabileceği açıktır. Ancak bazı durumlar vardır ki bu pek de açık olmayabilir, hatta mantığa aykırı bile gelebilir. Buna örnek olarak adaptör tasarım desenini verebiliriz. Bu desende adaptör sınıflar durum taşımayan (stateless), sadece metot çağrılarını farklı bir formatta başka sınıflara ileten sınıflardır. Başka bir deyişle adaptör sınıfların tek yaptığı iş, kendisine verilen metot parametrelerini kullanarak arka tarafta başka bir nesneyi çağırmak ve ondan gelen sonucu istemciye döndürmektir. Burada adaptör sınıf herhangi bir durum bilgisi (state) taşımadığından, bu sınıftan birden çok sayıda yaratmak yersizdir. Aynı adaptör nesnesini tekrar tekrar kullanabilirsiniz.

Örneğin, Map arayüzünün keySet metodu, Map nesnesinin bütün anahtarlarını (key) içeren bir Set döndürmektedir. Biz keySet metodunu her çağırdığımızda, yeni bir Set nesnesi yaratıldığını zannediyor olabiliririz, ancak gerçekte bütün çağrılar aynı Set nesnesini döndürüyor olabilir ve bunlar işlevsel olarak aynı olacaktır. Bunlardan bir tanesinin içeriğini değiştirirsek, hepsi bu değişimi görecektir çünkü hepsi aynı Map nesnesi üzerinde çalışmaktadır. Burada birden fazla Set nesnesi yaratmak çoğu zaman zararsız olsa da gereksizdir ve hiçbir faydası yoktur.

Bunların haricinde, Java’da kolaylık olması açısından eklenen “autoboxing” özelliği de doğru kullanılmadığı taktirde gereksiz nesne yaratılmasına sebep olabilir. Aşağıdaki kodu inceleyelim:

// Yavaş çalışan program! Gereksiz nesnenin 
// nerede yaratıldığını görebiliyor musunuz?
public static void main(String[] args) {
    Long toplam = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++)    
    {
        toplam += i;
    }
    System.out.println(toplam);
}

Yukarıdaki program 0’dan başlayarak bütün Integer sayıların toplamını bulmaktadır ve çalıştırdığınız zaman doğru sonucu üretecektir, ancak bu program olması gerekenden çok daha yavaş çalışıyor. Bunun sebebi aslında kod içerisindeki tek bir karakterin yanlış yazılması. Toplam değişkenini long yerine Long olarak tanımladığımız için 2 üzeri 31 tane gereksiz Long nesnesi oluşturuyoruz. Bunun sebebi de şudur: 7. satırdaki toplama işlemi toplam = toplam + i; şekline çevrilerek işletilecektir. Ancak toplama operatorü (+) Long nesnesi üzerinde işlem yapamayacağı için, önce onu ilkel long türüne çevirip toplama yapacaktır. Daha sonra da toplamdan yeni bir Long nesnesi yaratmak durumunda kalacaktır. Yani bu satır aslında aşağıdaki gibi işletilecektir:

toplam = new Long(toplam.longValue() + i);

Gördüğünüz gibi her toplama işlemi otomatik olarak devreye giren autoboxing mekanizması sebebiyle her seferinde yeni nesne oluşturmaktadır. Long olarak tanımlanan toplam değişkenini long ilkel türüne çevirmek bütün sorunları çözecektir. Buradan çıkarılacak ders şudur: kullanabildiğiniz yerlerde ilkel türleri tercih edin ve autoboxing sistemini siz istemeden devreye sokacak kodlar yazmaktan kaçının.

Bu yazıda anlatmak istenilen şey fazladan nesne yaratmanın her durumda kötü ve kaçınılması gereken bir şey olması değildir. Bazı durumlarda eğer kodun okunabilirliğini ve anlaşılabilirliğini artırıyorsa yaratması kolay küçük nesneleri fazladan yaratabilirsiniz. JVM sizin için bunu optimize edecektir. Veritabanı bağlantısı gibi yaratması ve yok etmesi çok pahalı olan nesnelerden bahsetmiyorsak, fazladan nesne yaratmamak amacıyla nesne havuzları (object pool) oluşturup bunların yönetmeye kalkmak da son derece gereksizdir, çünkü bu hem uygulamanın okunurluğunu azaltır hem de bellek kullanımı artırarak performansı düşürür. Modern JVM gerçekleştirimleri, son derece iyi optimize edilmiş çöp toplayıcılar (garbage collector) sayesinde, object pool gibi yapıları büyük ölçüde gereksiz hale getirmiştir.

Madde 50 bu yazıda anlatılanlara ters gibi görünmektedir. Bu maddenin ana teması ”Var olan bir nesnesi kullanmanız gerekirken yenisini yaratmayın” iken, madde 50 savunma amaçlı kopyalama (defensive copying) tekniği gereği ”Yeni bir nesne yaratmanız gerektiği zaman var olanı kullanmayın” demektedir. Unutmayın ki, savunma amaçlı kopyalama gerektiği zaman bunu yapmak yerine var olan bir nesneyi kullanmak, gereksiz yere bir nesne yaratmaktan çok daha zararlıdır. Gerektiği yerde kopyalama yapmamak, çok sinsi uygulama hatalarına ve güvenlik açıklarına yol açabilir. Gereksiz nesne yaratmak ise yalnızca performans kayıplarına sebebiyet verir ve bazı durumlarda okunabilirliği azaltabilir.

Share

Effective Java Madde 2: Çok Sayıda Parametreyle Karşılaştığınızda Builder Kullanın

Daha önce birinci maddesini işlediğimiz Effective Java kitabının ikinci maddesiyle devam ediyoruz.

Sınıf yapıcılar (constructor) ve statik fabrika metotlarının (static factory methods) paylaştığı ortak bir kısıt vardır: çok sayıda parametre geçmeniz gerektiği zaman kullanışsız bir duruma gelirler. Örneğin, paket gıdaların üzerinde yer alan ve besin değerlerini gösteren bir etiketi Java’da bir sınıf kullanarak ifade etmeye çalıştığımızı düşünelim. Bu etiketin üzerinde kalori, kolesterol, protein, karbonhidrat, yağ, doğmamış yağ, kalsiyum, demir ve daha birçok besin türü görürüz. Ancak besinlerin bir çoğu için bu değerlerin büyük bir kısmı opsiyonel olacaktır, yani bu alanlara hiçbir atama yapmamıza gerek olmayacaktır.

Böyle bir sınıf yazmamız gerektiği zaman nasıl bir sınıf yapıcı veya statik fabrika metodu kullanırız? Genelde yazılımcıların aklına gelen ilk yöntem iç içe geçmiş yapıcı metotlar (telescobing constructors) kullanmak olur, yani sadece zorunlu parametreleri içeren bir yapıcı metot, ardından zorunlu parametreleri ve sadece bir opsiyonel parametre içeren başka bir yapıcı metot, ardından iki opsiyonel parametreli üçüncü bir yapıcı metot ve bu şekilde devam eder. Şimdi örnek kodu inceleyelim, kolaylık olması açısından sadece 2 tane zorunlu 4 tane de opsiyonel değişkenimiz olsun.

Read more “Effective Java Madde 2: Çok Sayıda Parametreyle Karşılaştığınızda Builder Kullanın”

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

Birim Test Nedir? Niçin Yapılır? Nasıl Yapılır?

Yıl olmuş 2014 hala birim test makalesi mi yazıyorsun diye düşünüyor olabilirsiniz. Ancak, birim testi uzun zamandır kullanılan bir yöntem olmasına rağmen tam olarak ne işe yaradığı, neden yapıldığı ve nasıl yapılması gerektiği konusunda açıklayıcı bir Türkçe yazı bulmak malesef zor. Bu yüzden, yazılım mühendisi adaylarına ve kariyerine yeni başlayan arkadaşlara faydalı olabilmek adına bu yazıyı yazmak uygun olur diye düşündüm.

Birim Testi Nedir?

Birim testi adından anlaşıldığı üzere yazılım birimlerinin test edilmesidir. Burada yazılım birimi dediğimiz şey ise test edilebilen en küçük yazılım bileşenidir. Nesneye yönelik programlama yaklaşımını ele alacak olursak, yazılım birimleri sınıflardır diyebiliriz. Yapılan şey basit olarak sınıf davranışlarının (metodlar) belirli girdiler sağlandığı zaman doğru bir şekilde çalışıp, istediğimiz sonucu üretip üretmediğini kontrol etmektir. Bu şekilde yazılımın küçük birimleri test edildiği zaman, bütünü oluşturan parçaların en azından kendi içlerinde çalıştığından emin olmuş oluruz. Buraya kadar söylediklerimiz birim testinin genel tanımıdır, ama yazılım geliştiren kişiler olarak asıl anlamamız gereken şey birim testini niçin yaptığımızdır.

Birim Test Niçin Yapılır?

Bu soruyu eminim ki birçok yazılımcı kendi kendine sormuştur. Bir kısmımız tam olarak neye hizmet ettiğini anlamasak da, faydalı olduğunu düşündüğümüz için ve kendimizi daha güvende hissetmek adına birim test yazarız. Bazılarımız ise birim test yazmanın faydalı olduğunu bilmemize rağmen çeşitli bahaneler üreterek birim test yazmaktan kaçarız. Bunun arkasındaki asıl sebep ise birim testlerin ve test odaklı yazılım geliştirme tekniğinin (test-driven development) asıl amacını kavrayamamış olmamızdır. Her şeyden önce şunu söylemek gerekir: Birim testleri yazılımları test etmek için yazılmaz. İsmi “birim test” olan bir yöntem için “asıl amacı yazılımları test etmek değildir” demek ilk başta çok mantıklı gelmeyebilir ama yazıyı okudukça bana hak vereceğinizi düşünüyorum.

Birim testler hata bulmak için değildir

Bir yazılım sistemindeki hataları (bug) bulmak birim testler ile mümkün değildir. Çünkü birim testlerin yaptığı iş yazılımın en küçük parçalarını kendi içerisinde test etmektir. Peki bu küçük parçaların kendi içlerinde çalışıyor olması, yazılımın gerçek kullanıcılar tarafından kullanılmaya başladığı zaman bir bütün olarak çalışacağını gösterir mi? Kesinlikle hayır. Bir yazılım sistemi, onu oluşturan parçaların toplamından çok daha fazlasıdır. Dolayısıyla bu bütünü test etmek için farklı yöntemler kullanmak gerekir. İşlevsel test (functional testing), bütünleştirme testi (integration testing) bunlara örnek verilebilir ancak konumuz birim test olduğu için bunlara değinmeyeceğim.

Hataları bulamıyorsa birim testler ne işe yarıyor?

Birim test yazmanın sağladığı gerçek fayda, bizi kaliteli kod yazmaya teşvik etmesidir. Peki bu nasıl olur? Öncelikle şunu söylemek gerekir ki, birim test yazmanın birinci kuralı test etmekte olduğumuz sınıfı, bağımlı olduğu diğer bütün bileşenlerden izole etmektir. Örnek verecek olursak, test ettiğiniz sınıfın bir Google servisine bağlanarak veri çektiğini düşünün. Ancak birim test esnasında bu sınıfın Google servisine bağlanıp veri çekmesini istemeyiz. Çünkü birim testin amacı yazılımın Google servisleriyle çalışabildiğini kanıtlamak değildir. Birim test yazarken, bağlantılı olduğumuz diğer bütün parçaların sorunsuz biçimde çalıştığını varsayarak yazarız, çünkü odaklandığımız şey sınıfın kendisidir, bağımlı olduğu diğer bileşenler değil. Bu varsayımı yapabilmek için de, mocking dediğimiz tekniği kullanarak test esnasında gerçek Google servisine bağlanmak yerine bizim yarattığımız sahte bir servise (mock object) bağlanıp sınıfın ihtiyacı olan veriyi döndürürüz.  Bu şekilde test ettiğimiz sınıf dışarıda bir servise bağlanmadan ihtiyacı olan veriyi alır ve işletimini tamamlar.

Şimdi test etmekte olduğumuz bu sınıfın dışarıdaki Google servisiyle sıkı sıkıya bağlı (tightly coupled) olduğunu düşünün. Sınıf Google servisiyle ilgili bütün bilgileri içinde barındırıyor ve bağlantıyı yaratıp kullanıyor, veri alışverişini yapıyor. Biz bu sınıfa gerçek Google servisine değil de bizim belirlediğimiz sahte servise (mock object) bağlanmasını nasıl söyleyeceğiz? Bu şekilde birbirine sıkıca bağlanmış yazılım bileşenlerini birbirlerinden bağımsız bir şekilde test etmek mümkün değildir. Ancak bu bileşenler gevşek bağlı (loosely coupled) olsaydı, biz sınıfımıza test esnasında sahte servisi, gerçek işletim esnasında ise Google servisini kullanmasını söyleyebilirdik. Bu şekilde yazılım bileşenlerini birbirlerine gevşek bir biçimde bağlamak Dependency Injection tekniğiyle mümkündür ve gevşek bağlı sistemler çok daha kolay bakım yapılabilen, test edilebilen ve eklemeler yapması çok daha kolay olan sistemlerdir.

Test odaklı yazılım geliştirme yapıyorsak (test-driven development), birim testleri sınıfın kendisinden önce yazmamız gerektiği için bu tarz tasarım detaylarını henüz işin başındayken doğru bir şekilde belirlemiş oluruz. Doğru biçimde birim test yazmak, yazılım bileşenlerini birbirlerine sıkı sıkıya bağlamamızı engelleyerek daha tasarım aşamasındayken daha kaliteli bir yazılım çıkarmamıza yardımcı olur. Özet olarak şunu söylemekte fayda var, bütün bileşenleri birbirinden bağımsız olarak test edilebilen yazılımlar, bakımı nispeten daha kolay olan ve kaliteli yazılımlardır. İşe birim testleri yazarak başlamak da bunu başarmamıza yardımcı olur.

Birim test yazmak kodda iyileştirme yapmayı (refactoring) kolaylaştırır

Birim test yazmanın bir diğer büyük faydası da kodda iyileştirme yaparken (refactoring) ortaya çıkar. Hiçbir kod mükemmel değildir ve iyileştirme her zaman bir ihtiyaçtır. Ancak birçok yazılımcı çalışan sistemi bozmaktan korktuğu için iyileştirme yapmaz. Ancak kapsamlı birim testleriniz varsa, değişiklik yaptığınız sınıfın hala çalışıp çalışmadığını anlamak için birim testlerinizi kullanabilirsiniz. Daha önce birim testlerin hataları bulmak için kullanılmadığını söylemiş olsak da iyileştirme esnasında üzerinde çalıştığımız sınıfı bozup bozmadığımızı anlamak mümkün olabilir. Dolayısıyla birim test yazmak sadece kodu yazarken kaliteli yazmaya teşvik etmekle kalmaz, aynı zamanda ileride kodu iyileştirmemize de yardımcı olur.

Doğru birim test nasıl yazılır?

Birim testin nasıl yazılması gerektiği de çok önemlidir. Doğru yazılmayan birim testler bize hiçbir şey kazandırmayacağı gibi en ufak değişiklikte hatalar vermeye başlayıp başımızı ağrıtırlar. Üstüne bir de testlere bakım yapmakla uğraşmak zorunda kalacağımız için de fayda sağlamanın aksine zararlı olabilirler. O yüzden birim test yazarken aşağıdaki noktalara dikkat etmekte fayda var:

  • Tek bir şeye odaklanınHer testin tek bir şeyi test ettiğinden emin olun. Çok gerekli değilse aynı test içerisine birden fazla assert ifadesi koymayın.
  • Bağımlılıkları (dependency) değil, tek bir sınıfı test edin: Yazıda daha önce de değindiğimiz gibi, bir sınıfı test ederken o sınıfı bağımlı olduğu diğer yazılım bileşenlerinden izole edin, aksi taktirde yazdığınız test birim test değildir.
  • Yazdığınız testler birbirini etkilemesin: Yazdığınız her test birbirinden bağımsız bir şekilde tek başına sorunsuz çalışabilmelidir. Eğer yazdığınız bir birim test başka bir birim testin üreteceği veriye bağımlıysa yanlış yapıyorsunuz demektir.
  • Testlerinizi doğru isimlendirin: Test sayısı arttıkça isimlendirmenin önemi de artar. Kafa karıştırıcı test isimleri kullanmak ileride problemlere yol açar. Açıklayıcı olması için test isimlerini uzun tutmanız gerekiyorsa öyle yapın, uzun isimler yanlış isimlerden daha faydalıdır.
  • Test koduna ikinci sınıf kod muamelesi yapmayın: Testler de yazılımın bir parçasıdır. Dolayısıyla normal program kodunu yazarken ne kadar özen gösteriyorsanız test kodlarına da aynı özeni gösterin, kod tekrarlarından kaçının, okunabilir test kodu yazı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 Gizli (Implicit) ve Açık (Explicit) Parametreler

Programlama dillerinde bir işlev, fonksiyon veya yöntem (metod) çağırırken veri geçişi için parametreler kullanılır. Java dilinde de aynı şey geçerlidir. Aşağıdaki gibi bir yöntemimiz olduğunu düşünürsek;

public void maasArtir(double oran) {
	maas += maas * oran/100;
}

yukarıdaki yöntem parametre olarak double türünde bir değer almakta ve nesne değişkeni olan “maaşı” artırmaktadır. Parametre olan “oran” değişkeni yöntemin imzasında açıkça tanımlandığı için açık (explicit) parametredir. Buraya kadar ilginç birşey yok, bildiğimiz şeyler. Şimdi bu yöntemin Personel isimli bir sınıf içerisinde tanımlı olduğunu düşünelim ve Personel sınıfından yarattığımız personel1 adında bir nesnemiz olsun.

Personel personel1 = new Personel();

Daha sonra personel1 nesnesi üzerinden maasArtir yöntemini çağıralım.

personel1.maasArtir(20);

Bu koda göre personel1 nesnesine ait olan “maas” değişkeni %20 oranında artacaktır. “oran” parametre değişkeni açık parametre ise yazının başlığında belirttiğimiz gizli parametre nerededir? Java dilinde yöntem çağırırken kullandığımız nesne referansı da yönteme parametre olarak gönderilmektedir. Yani nesne referansımız olan personel1, maasArtir yöntemi için gizli (implicit) parametredir. Diğer bir deyişle yöntemi çağırmak için kullandığımız referans, aynı zamanda gizli parametredir. Gizli parametre yöntemin imzasında tanımlanmaz, gizli olması da zaten bu yüzdendir.

Peki, açık parametre olan “oran” değişkenine yöntem içerisinde direk adıyla erişebiliyorsak, gizli parametreye nasıl erişeceğiz? Bunun için de this anahtar kelimesi kullanılmaktadır. Yöntem içerisinde nesne değişkenlerine erişmek istediğimizde this kelimesini kullanabiliriz. Yani en yukarıda yazdığımız yöntemi aşağıdaki gibi de yazabiliriz.

public void maasArtir(double oran) {
	this.maas += this.maas * oran/100;
}

Yukarıdaki gibi bir kullanımda this anahtar kelimesi ile gizli parametreye erişmiş oluyoruz ve böylece personel1 nesnesinin maaş bilgisine erişiyoruz. Biz maas değişkeninin önüne this yazmasak bile bu kelime otomatik olarak eklenmektedir. Bu şekilde açıkça “this” yazmak, programın okunabilirliğini artırmak ve nesne değişkenleri ile yerel değişkenleri bir bakışta ayırt etmek için faydalı olabilir.

Share