Effective Java Madde 5: 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 pogramı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.

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. 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.

Yukarıdaki örneklere 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. Aşağıdaki örnekte yine yanlış bir kullanım görülmektedir. Bu kodda değiştirilebilir (mutable) Date nesneleri yaratılmakta ancak bu nesneler daha sonra hiç değiştirilmemektedir. Örnek kod kişilerin “baby boomer” olup olmadılarını, başka bir deyişle 1946-1964 yılları arasında doğup doğmadıklarını belirlemektedir.

public class Person {

    private final Date birthDate;

    // YANLIS KULLANIM
    public boolean isBabyBoomer() {
        // Gereksiz yere Calendar, Date ve TimeZone nesneleri yaratılmaktadır
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomEnd = gmtCal.getTime();
        return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
    }
}

Bu örnekte, isBabyBoomer() metodu her çağrıldığında aynı Calendar, Date ve TimeZone nesneleri gereksiz yere yaratılmaktadır. Aşağıdaki gibi bir kod kullanarak bu durumu düzeltebiliriz:

public class Person {
    private final Date birthDate;

    // "baby boomer" için başlangıç ve bitiş tarihleri    
    private static final Date BOOM_START;
    private static final Date BOOM_END;
    
    // static kod bloğu kullanarak nesnelere ilk ve son değerlerini veriyoruz
    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
    }
}

Yukarıdaki iyileştirilmiş “Person” sınıfı, Calendar, Date ve TimeZone nesnelerini isBabyBoomer() metodu her çağrıldığında değil, sadece bir kere yaratır ve metot içerisinde bu sabit değerler kullanılır. Bu iyileştirme kodu ciddi derecede hızlandıracaktır. (Joshua Bloch kitapta, metot 10 milyon kez işletildiğinde iki versiyon arasında 250 kat hız farkı olduğunu gözlemlemiş, ben test etmedim) Sadece hızlandırmakla kalmayıp aslında kodu daha okunabilir bir hale de getirdik, çünkü BOOM_START ve BOOM_END değerleri sabit değerlerdir ve bunları sınıf değişkeni (static) olarak tanımlamak daha anlaşılır olacaktır.

Eğer iyileştirilmiş “Person” sınıfı JVM tarafından yüklenirse ama isBabyBoomer() metodu hiç çağrılmazsa, o zaman statik kod bloğu içerisinde yaptığımız ilk değer atamalarını boşuna yapmış oluruz. Bundan kurtulmak için de kodu değiştirip isBabyBoomer() metodu ilk defa çağrıldığı zaman ilk değer atamalarını yapabiliriz, ancak bu kodu karmaşıklaştıracaktır ve karşılığında sağladığımız fayda çok da büyük olmadığından kodu bu şekilde bırakmak daha iyi olacaktır.

Başka bir örnek adaptör tasarım deseni üzerinden verilebilir. 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.

Son olarak bir örnek daha verip yazıyı bitirmek istiyorum. 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: 5. 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 5. 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 istediğim şey nesne yaratmanın kötü birşey olması değil. 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 pahalı olan nesnelerden bahsetmiyorsak, fazladan nesne yaratmamak amacıyla nesne havuzları (object pool) oluşturup bunların yönetmeye kalkmak da son derece gereksiz ve yanlıştır. Özet olarak bu yazıda bahsedilen tuzakların farkında olup bunlardan kaçınmak daha stabil ve yüksek performanslı uygulamalar yazmanıza yardımcı olur, bunun dışında uç noktalara kayıp olabildiğince az nesne yaratmak mantığıyla uygulama yazmaya da gerek yok, JVM’ye güvenin 🙂

Share

3 Replies to “Effective Java Madde 5: Gereksiz Nesne Yaratmaktan Kaçının”

  1. Java’da bildiğim kadarıyla stringler equals() metoduyla karşılaştırılır.== ile yapmanın çok temel bir hata olduğunu biliyorum.Yanılıyormuyum?

  2. @Hasan
    Haklisin string karsilastirma equals() ile yapilir ancak bu makaledeki ilk ornekte string nesnelerine bellekte nasil yer ayrildigini inceledigimiz icin == ile referans karsilastirmasi yaptik.

Bir Cevap Yazın