Java kütüphaneleri yazılımcının close
metodunu çağırarak kapatması gereken birçok kaynak barındırır. Bunlara örnek olarak InputStream
, OutputStream
ve java.sql.Connection
verilebilir. Bu kaynakları kapatmak (serbest bırakmak), istemciler tarafından sıklıkla unutulur ve bu durum ciddi performans sorunlarına yol açar. Bu kaynaklar her ne kadar finalizer
(sonlandırıcı) kullanarak istemci tarafında yapılacak hatalara bir önlem almaya çalışsalar da, pratikte biz sonlandırıcıların iyi çalışmadığını biliyoruz. (Madde 8)
Önceleri, try-finally
ifadesi kullandığımız kaynakların bir istisna (exception
) fırlatıldığında dahi doğru biçimde kapatılmasını garanti eden en iyi yoldu.
// try-finally - Artık kaynakları kapatmanın en iyi yolu değil!
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
Bu çok da kötü görünmüyor, ancak ikinci bir kaynak daha eklediğimizde durum kötüleşiyor.
// try-finally birden fazla kaynakla kullanılınca kod çirkinleşiyor
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
İnanması zor olabilir ama, iyi programcılar bile çoğu zaman bunu yanlış kullanmaktadır. Örneğin, Java Puzzlers kitabının 88. sayfasında ben de hata yaptım ve yıllarca hiç kimse fark etmedi. Dahası, 2007 yılında Java kütüphanelerindeki close
metodu kullanımlarının üçte ikisi yanlıştı.
try-finally
blokları doğru kullanılarak kod yazıldığı durumlarda bile (yukarıdaki iki örnek gibi), ortada küçük de olsa bir sorun bulunmaktadır. Hem try
hem de finally
bloklarındaki kodların istisna fırlatma ihtimali bulunmaktadır. Örneğin, ilk örnekteki firstLineOfFile
metodunda, try
bloğundakireadLine
çağrısı fiziksel aygıttaki olası bir problemden dolayı istisna fırlatabilir, finally
içerisindeki close
çağrısı da aynı sebepten dolayı başarısız olabilir. Bu durumda, ikinci fırlatılan istisna birinciyi tamamen görünmez kılacaktır. Stack trace içerisinde birinci istisna görülmeyecektir, bu da problemi çözmeye çalışan kişileri çok zor durumda bırakacaktır. Çünkü genellikle bir sorunu çözmeye çalışırken ilk hatanın nereden kaynaklandığını görmek önemlidir. Bu sorunu çözmek için yukarıda verilen kodları geliştirmek mümkündür, ancak sonuçta oluşan kod daha karmaşık olduğu için bunu genellikle kimse yapmaz.
Bütün bu problemler Java 7 ile birlikte gelen try-with-resources
ifadesiyle çözülmüştür. Bu ifadeyi kullanabilmek için kapatılması gereken kaynakların, void
geri döndüren tek bir close
metodu içeren AutoClosable
arayüzünü uygulaması gerekmektedir. Java kütüphanelerindeki birçok sınıf ve arayüz bugün bu arayüzü kullanmaktadır. Eğer siz de kapatılması gereken bir kaynağı temsil eden bir sınıf yazarsanız, AutoClosable
arayüzünü mutlaka uygulamalısınız.
Yukarıda verdiğimiz birinci örneğin try-with-resources
ile kullanımı aşağıdaki gibi olacaktır.
// try-with-resources - kaynakları kapatmanın en iyi yolu!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
Ve bu da ikinci örneğimizin try-with-resources
ile yazılışı:
// try-with-resources birden fazla kaynakla kullanımı
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
}
}
Burada gördüğünüz gibi, istemcinin ayrıca close
metodunu çağırmasına gerek kalmamaktadır. try-with-resources
kullanıldığında AutoClosable
arayüzünden gelen close
metodu otomatik olarak çağrılmakta ve istemci tarafında yapılacak hatalara karşı önlem alınmaktadır.
try-with-resources
ifadesiyle yazılan kodlar daha kısa ve okunabilir olmasının yanında, hataların teşhisini de kolaylaştırmaktadır. firstLineOfFile
metodunu düşünecek olursak, eğer hem readLine
metodu hem de görünmez close
metodu istisna fırlatırsa, ikinci istisna birinciyi görünmez kılmaz, tam tersine birinci görülebilsin diye ikinci gizlenir (suppressed). Hatta ikiden daha fazla istisna fırlatıldığı durumlarda birden çok istisna da gizlenebilir. Çünkü yazılımcının ilk oluşan hatayı görebilmesi hatanın teşhisi açısından önemlidir. Burada saklanan istisnalar da aslında kaybolmazlar, stack trace içerisinde yine görülebilirler ancak gizlendiklerine dair bir ibare yer alır. Hatta bunlara Java 7’de Throwable
sınıfına eklenen getSuppressed
metoduyla programatik olarak da erişmek mümkündür.
try-with-resources
kullanırken aynen try-finally
kullanırken olduğu gibi catch
blokları eklemek de mümkündür. Bu, sizlere iç içe katmanlar eklemenize gerek kalmadan kodunuzda oluşabilecek istisnaları ele almanızı sağlar. Pratikte kullanımı pek mantıklı gözükmese de, ilk örneğimizi aşağıdaki gibi değiştirerek istisna fırlatmak yerine, veri okumada bir hata oluştuğunda varsayılan bir değeri döndürecek şekilde yazabiliriz.
// try-with-resources catch bloğuyla kullanımı
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
catch (IOException e) {
return defaultVal;
}
}
Buradaki ders çok net: kapatılması gereken kaynaklarla çalışırken try-finally
yerine try-with-resources
ifadesini kullanmalıyız. Bu sayede ortaya çıkan kod çok daha kısa ve anlaşılır olacaktır, oluşabilecek hatalar sonucunda da teşhis yapmamız kolaylaşacaktır. Bu yeni yöntem, try-finally
ile yazılması çok zor ve zahmetli olan kod parçalarının doğru biçimde yazılmasını son derece kolaylaştırmaktadır.