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

3 Replies to “Effective Java Madde 6: 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