Java Lambda İfadelerinde Checked Exception Yönetimi
Yakın zamanlarda okuma imkanı bulduğum güzel bir kitaptan kısa bir bölüm aktarmak istiyorum. Başlangıç ve orta seviyedeki yazılımcılar için kitabın faydalı olacağını düşünüyorum. A Functional Approach to Java kitabını şuradan satın alabilirsiniz. Faydalı olması dileğiyle.
Lambdalara geçmeden önce, Java’daki hatalara(exceptions, istisnalar da diyebiliriz) bakalım. Genel olarak üçe ayrılır.
- Checked Exceptions(İşaretli Hatalar): Uygulama tarafında yakalanması gereken veya metod tanımlanırken belirtilmesi gereken hatalardır. Bunları ya
try-catch
ile yakalarız ya dathrows
ile metod tanımında belirtiriz.Exception
sınıfından türerler ve derleme zamanında belli olan hatalardır. - Unchecked Exceptions(İşaretsiz Hatalar): Uygulama tanımında belirtilmeyip, JVM tarafından çalışma zamanında yakalanan hatalardır.
RuntimeException
sınıfından türerler. Yine detry-catch
ile yakalayabiliriz. - Errors(Yönetilemeyen Hatalar):
Throwable
sınıfından doğrudan türeyen ve yakalanamayan hatalardır. Genelde JVM’in yeniden başlatılmasına yol açar.
Java’daki hata yönetimi, lambdalardan 18 yıl kadar önce tasarlandığı için Checked Hatalar ile lambdaların kullanımında sıkıntılar olabilir.
Örneğin, bir dosyanın içeriğini java.util.Files.readString()
metodu ile okumaya çalışalım. Metodun imzası şuna benzerdir.
public static String readString(Path path) throws IOException {
// ...
}
Metod tanımına bakarsak, IOException
ile checked hatanın fırlattığını görürüz. Bu yüzden, metod referans olarak lambda ifadelerinde kullanamayız.
Stream.of(path1, path2, path3)
.map(Files::readString)
.forEach(System.out::println);
Kodu derlemek istersek, hata alırız. Çünkü, readString()
metodunu map functional interface içinde kullanmak isteriz ve o da metod tanımında checked hataları belirtmez.
Bariz bir çözüm, try-catch
kullanmaktır.
Stream.of(path1, path2, path3)
.map(path -> {
try {
return Files.readString(path);
} catch (IOException e) {
return null;
}
})
.forEach(System.out::println);
Böyle yapınca da, kodda okunurluk çok kötü oldu. Sanki, antipattern bir yapı kullanmış gibi oldu. Bu durumu 3 şekilde çözebiliriz.
- Güvenli metod ile dışarı çıkarma
- Checked hataları Unchecked hatalara çevirme
- Sinsi(Sneaky) throws kullanımı
Güvenli metod ile dışarı çıkarma
try-catch
içine aldığımız kod bloğunu farklı bir metod olarak aşağıdaki gibi dışarı çıkarabiliriz. Bu şekilde derleyici hatasından kurtuluruz.
String safeReadString(Path path) {
try {
return Files.readString(path);
} catch (IOException e) {
return null;
}
}
Stream.of(path1, path2, path3)
.map(this::safeReadString)
.filter(Objects::nonNull)
.forEach(System.out::println);
Checked hataları Unchecked hatalara çevirme
Kendi geliştireceğimiz bir arayüzle(veya Spring gibi yardımcı kütüphanelerden gelen) checked hataları, unchecked hatalara çevirebiliriz.
@FunctionalInterface
public interface ThrowingFunction<T, R> extends Function<T, R> {
R applyThrows(T elem) throws Exception;
@Override
default U apply(T t) {
try {
return applyThrows(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T, R> Function<T, R> uncheck(ThrowingFunction<T, R> fn) {
return fn::apply;
}
}
Bu arayüz içinde, applyThrows(T)
ile gelen checked hatayı, apply(T)
ile unchecked hataya çeviririz. uncheck()
static metodundan faydalanırız.
Kullanımına bakalım.
Stream.of(path1, path2, path3)
.map(ThrowingFunction.uncheck(Files::readString))
.filter(Objects::nonNull)
.forEach(System.out::println);
Bu şekilde derleyici hatasından yine kurtuluruz.
Sinsi(Sneaky) throws kullanımı
Sneaky throws deyimi, checked bir hatayı, metodun imzasındaki throws
anahtar sözcüğüyle bildirmeden fırlatmak için kullanılan bir hack’tir.
Örnek kullanıma bakalım.
String sneakyRead(File input) {
// ...
if (fileNotFound) {
sneakyThrow(new IOException("File '" + file + "' not found."));
}
// ...
}
Hatanın fırlatılması, sneakyThrow()
metoduna ihale edilir. Bunun için Java 8'de gelen Generics ve Exceptions’daki tip çıkarımından faydalanırız. Daha basitçe, throws E
gibi generic bir ifade kullanılan metodda, E olan tipi belirten bir üst veya alt sınır yoksa, derleyici E tipini RuntimeException
olarak kabul ediyor.
<E extends Throwable> void sneakyThrow(Throwable e) throws E {
throw (E) e;
}
Derleyici bundan şikayet etmese de, çok problemli bir yaklaşımdır. sneakyRead()
ile checked hataların yakalanması veya metod tanımında belirtilmesi gereken public sözleşmesini ihlal etmiş olduk. Kodu okuyan için de anlaşılması zor bir yapıdır. En azından return
ifadesi ile uygun bir çıkış noktası belirlenebilirdi.
Çok istisna durumlar harici bunu kullanmamakta fayda var.
Yazımızın sonuna geldik. Yeni yılınız kutlu olsun.