Home > Java > Effective Java Madde 7: Finalizer (Sonlandırıcı) Kullanmaktan Kaçının

Effective Java Madde 7: Finalizer (Sonlandırıcı) 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.

Ö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 desctructor 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 blokları kullanılır.

Sonlandırıcıların en büyük dezavantajı ne zaman işletileceğinin belli olmamasıdır. Java’da çöp toplama mekanizması belli aralıklarla devreye girerek bellekteki artık nesneleri temizler, bunu yaparken de nesnelerin finalize() metodunu çağırır. Ancak sorun şudur ki biz çöp toplayıcının ne zaman devreye gireceğini bilemeyiz, bu JVM tarafından tamamen dinamik olarak karar verilerek yapılır. Yani bir nesnenin tamamen erişilemez duruma gelmesiyle gerçekten temizlenmesi arasında uzun bir zaman geçebilir. Ö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ı 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.

Java dili sonlandırıcıların ne zaman işletileceğine dair hiçbir garanti vermez, hatta daha kötüsü işletileceğine bile garanti vermez. Bu davranış farklı JVM gerçekleştirimleri tarafından farklı şekillerde ele alınabilir, dolayısıyla sonlandırıcı kullanarak yazdığınız bir uygulama bir JVM’de çalışıp öbüründe çalışmayabilir.

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 finalize() içerisinde bir uyarı bile göremezsiniz. Eğer hala ikna olmadıysanız şunu da söyleyelim: sonlandırıcı kullanmak uygulama performansını da kötü yönde etkiler.

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? Sınıfın yazan kişi olarak sonlandırma metotlarını kendimiz istemcilere sunmalıyız ve istemcilerin bu metotlarını çağırmasını sağlamalıyız. Bu kullanımın en bariz örnekleri InputStream, OutputStream, java.sql.Connection gibi sınıflardaki close() metodudur. Yine java.awt içerisindeki Graphics.dispose ve Window.dispose metotları da bu kullanımın bir örneğidir.

Eğer bir sınıf açık bir sonlandırma metodu (explicit termination method) tanımlamışsa, bu metotlar try-finally ve try-with-resources blokları içerisinde kullanılır. finally bloğu içerisinde çağrılan açık sonlandırma metodu bu metodun her durumda çalıştırılacağını garanti eder.

// Java 1.7 öncesi kullanım
// try-finally terminate() metodunun her durumda işletileceğini garanti eder
Foo foo = new Foo(...);
try {
    // Foo ile ilgili işlemler
    ...
} finally {
    foo.terminate();  // açık sonlandırma metodu
} 

Java 1.7 ile gelen try-with-resources özelliği bu işlemi daha da kolaylaştırmaktadır.

// Java 1.7 ve sonrası kullanım
// Foo sınıfı AutoCloseable arayüzünü gerçekleştiriyorsa try bloğu içerisinde kullanılabilir.
try (Foo foo = new Foo(...)){
    // Foo ile ilgili işlemler
    ...
} 

Yukarıdaki kod programcı hatalarına karşı daha dayanıklı. Foo sınıfı AutoCloseable arayüzünü gerçekleştirdiği taktirde, direk olarak try bloğu içerisinde kullanabilirsiniz ve durumda foo.close() metodu sanki finally bloğu varmış gibi her durumda işletilecektir.

Peki o zaman finalize() metodu kullanmak ne zaman mantıklı olabilir? Bunun bir örneğini FileInputStream, FileOutputStream, Timer, ve Connection sınıflarında görebilirsiniz. Bu sınıflar close() gibi açık sonlandırıcı metotlar kullanmalarına rağmen, finalize() metodunu da override etmişlerdir. Bunun sebebi ise sınıfı kullanan kişilerin açık sonlandırıcıları çağırmayı unutmaları durumunda, ‘hiç yoktan iyidir’ mantığıdır. Buradaki finalize() metotlarının da zamanında çalışıp çalışmayacağının bir garantisi yoktur. Eğer siz de böyle bir durum için finalize() yazmak isterseniz, uzun uzun düşünün çünkü bu aynı zamanda uygulama için bir ek maliyettir. Eğer yazmak zorunda kalırsanız, finalize() metodu içerisinde bir uyarı mesajı yazdırarak sınıfınızı kullanan kişiyi açık sonlandırıcıyı kullanmaları konusunda uyarın.

finalize() metodunun 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 finalize() metodunu kullanmak sorunu çözebilir. Bu çok kısıtlı bir kullanım olduğu için detaylara girmeye gerek yok diye düşünüyorum.

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.

Özet olarak, yukarıda bahsettiğimiz iki durum haricinde sonlandırıcı kullanmayın. Eğer kullanmanız gerekirse de ata sınıflar için super.finalize() metodunu çağırmanız gerektiğini unutmayın. Son olarak public ve final olmayan bir sınıfa sonlandırıcı yazıyorsanız, finalizer guardian tekniğini uygulayın ki bu sınıfı kalıtan sınıflar super.finalize() metodunu çağırmak zorunda kalmasın.

  1. Mustafa TABUR
    Nisan 20th, 2015 at 19:36 | #1

    Java aleminde olmazsa olmazlarından olan bir kaynak tercüme edilmesi de hoş olmuş.

    teşekkürler…

  2. Ocak 3rd, 2017 at 20:41 | #2

    İngilizcesini okuyup buraya geldim.Türkçesi bir başka güzel olmuş.Ellerine sağlık

  1. Ağustos 23rd, 2016 at 15:52 | #1

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 647.106 bad guys.

%d blogcu bunu beğendi: