Press "Enter" to skip to content

Effective Java Madde 44: Standart Fonksiyonel Arayüzlerin Kullanımına Öncelik Verin

Java diline lambdalar eklendikten sonra API yazmanın kuralları değişti diyebiliriz. Örneğin, Template Method tasarım desenini gerçekleştirirken çocuk sınıfların ata sınıftaki soyut metotları geçersiz kılması artık pek de çekici değildir. Bunun daha modern olan alternatifi fonksiyon nesnesi kabul eden bir statik fabrika veya yapıcı metot yazmak olacaktır. Genel olarak şunu söyleyebiliriz, fonksiyon nesnelerini parametre olarak alan daha fazla metot tanımlıyor olacaksınız. Ancak bunun için önce doğru fonksiyon türünü seçmek gerekir.

LinkedHashMap sınıfını ele alalım. Bu sınıfı ön bellek (cache) olarak kullanmak için removeEldestEntry metodunu geçersiz kılabilirsiniz. Bu metot LinkedHashMap nesnesine her yeni anahtar eklendiğinde put metodu tarafından çağrılmaktadır ve eğer true döndürürse en eski eleman silinmektedir. Biz bu metodu aşağıdaki gibi geçersiz kılarak map nesnesine en fazla yüz tane girdi eklenmesine izin verebiliriz. Bundan sonra eklenen her girdi için en eski girdi silinecek ve toplam sayı yüz olarak kalacaktır.

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return size() > 100;
}

Bu teknik işe yarasa da lambda ile çok daha iyisini yapmak mümkündür. LinkedHashMap bugün yazılmış olsaydı içinde fonksiyon nesnesi kabul eden bir yapıcı metot veya statik fabrika metodu bulunurdu. Peki bu fonksiyon nesnesi nasıl olmalıdır? removeEldestEntry metodu map içindeki eleman sayısı öğrenmek için size() metodunu çağırmaktadır. Burada bir sorun olmaz çünkü removeEldestEntry de size gibi bir nesne metodudur. Ancak tanımlamak istediğimiz fonksiyon nesnesi LinkedHashMap için bir nesne metodu olmayacaktır ve bu nesneye erişimi de olmaz çünkü fonksiyon yapıcı metot veya statik fabrika içinde işletilirken map nesnesi henüz ortada yoktur. Bu sebeple, fonksiyon nesnesi parametre olarak hem map nesnesini hem de silinecek en eski girdiyi almak durumundadır. Bu amaçla bir fonksiyonel arayüz tanımlayacak olursak:

// gereksiz fonksiyonel arayüz - bunun yerine standart olanı kullanın
@FunctionalInterface 
interface EldestEntryRemovalFunction<K,V>{
    boolean remove(Map<K,V> map, Map.Entry<K,V> eldest);
}

Bu arayüz işinizi görecektir ama gereksizdir. Java bizlere java.util.function paketi içerisinde kullanabileceğimiz çok sayıda standart fonksiyonel arayüz sunmaktadır. Eğer ihtiyacınızı karşılayan standart bir fonksiyonel arayüz varsa, aynı işi yapan başka bir arayüz yazmak anlamsızdır. Bu sayede hem istemciler sınıfınızın nasıl kullanılacağını daha kolay öğrenirler hem de standart arayüzlerde bulunan varsayılan (default) metotlar birçok durumda işinize yarayabilir. Örneğin Predicate arayüzündeki varsayılan metotlar birden fazla Predicate nesnesini birleştirerek kullanmamıza izin verirler. Bizim LinkedHashMap örneğimizde standart olarak tanımlanmış BiPredicate<Map<K,V>, Map.Entry<K,V>> arayüzü EldestEntryRemovalFunction yerine kullanılabilir.

java.util.function paketinde kırk üç tane standart fonksiyonel arayüz bulunmaktadır. Bunların hepsini hatırlamanız tabii ki beklenmez ama altı tane temel arayüzü bilirseniz gerisini bunlardan çıkarabilirsiniz. Bu temel arayüzler nesne referansları ile çalışırlar. Operator arayüzleri argüman ve dönüş türü aynı olan fonksiyonları, Predicate ise bir argüman alıp boolean değer döndüren bir fonksiyonu temsil eder. Function arayüzünde ise argüman türü ve dönüş türü birbirinden farklıdır. Supplier (sağlayıcı) arayüzünün hiçbir argümanı yoktur ama bir değer döndürür. Consumer (tüketici) ise bir argüman alıp hiçbir şey döndürmeyen bir fonksiyonu simgeler. Aşağıda bunları özetleyen bir tablo görüyorsunuz:

ArayüzFonksiyon İmzasıÖrnek
UnaryOperator<T>T apply(T t)String::toLowerCase
BinaryOperator<T>T apply(T t1, T t2)BigInteger::add
Predicate<T>boolean test(T t)Collection::isEmpty
Function<T,R>R apply(T t)Arrays::asList
Supplier<T>T get()Instant::now
Consumer<T>void accept(T t)System.out::println

Bu altı standart arayüzün int, long ve double temel türlerini desteklemek için üçer tane de türevi vardır. İsimleri de buna uygun olarak seçilmiştir. Mesela int türü kabul eden bir predicate IntPredicate, iki tane long değeri alıp tek bir long döndüren binary operator fonksiyonu ise LongBinaryOperator adını almıştır.

Function arayüzünün bundan başka ek türevleri de vardır. Örneğin long değer alıp int döndüren fonksiyonun adı LongToIntFunction olarak belirlenmiştir. int, long ve double türleri için bütün bu kombinasyonları bulabilirsiniz: IntToDoubleFunction, DoubleToLongFunction gibi. Temel türden değerler alıp nesne referansı döndüren fonksiyonlar ise IntToObjFunction, LongToObjFunction gibi isimler alır.

Predicate, Consumer ve Function arayüzlerinin iki argümanlı türevleri de bulunmaktadır. Bunların isimleri BiPredicate<T,U>, BiFunction<T,U,R>, ve BiConsumer<T,U> olarak geçer. BiFunction için temel türleri döndüren türevler de vardır. Örneğin iki argüman alıp int temel türünde değer döndüren fonksiyonun adı ToIntBiFunction<T,U> olarak geçer. Consumer içinse bir tane nesne referansı bir tane de temel tür kabul eden versiyonlar vardır. Örneğin ObjDoubleConsumer<T>, ObjIntConsumer<T> ve ObjLongConsumer<T>. Ayrıca bir de boolean değerler üreten BooleanSupplier arayüzü vardır.

Java bize bunlar gibi toplamda kırk üç tane arayüz sunmaktadır. İtiraf etmek gerekir ki bu sayı fazladır ve bunların hepsini hatırlamak mümkün değildir. Ancak diğer taraftan bu arayüzlerin varlığı bizim fonksiyonel arayüz ihtiyaçlarımızın çoğunu karşılamaktadır. İsimleri de belli bir düzene göre verildiği için kendinize lazım olan bulmak çok zor olmayacaktır.

Bu standart arayüzlerin bir çoğu temel türleri desteklemek için türetilmiştir. Temel türler için yazılmış bu arayüzleri kutulanmış (boxed) türlerle kullanmayı düşünmeyin (int yerine Integer gibi). Bunu yaparsanız kod çalışacaktır ama özellikle toplu işlemler (bulk operation) yaptığınızda ciddi bir performans kaybı yaşanacaktır. (Madde 61)

Artık standart olanlar işimize görüyorsa kendi arayüzlerimizi yazmamamız gerektiğini biliyoruz. Peki bunları kendimiz ne zaman yazmalıyız ve yazarsak nelere dikkat etmeliyiz? Tabii ki bize lazım olan fonksiyon standart arayüzlerin içinde yoksa kendimiz yazmamız gerekir. Örneğin üç parametreli bir Predicate lazım olursa yeni bir arayüz yazmamız gerekir. Ancak bazı durumlarda yapısal olarak bizim ihtiyacımızı karşılayan bir arayüz olsa bile yenisini yazmamız daha doğru olacaktır.

Eski dostumuz Comparator<T> arayüzünü ele alalım. Bu aslında yapısal olarak ToIntBiFunction<T,T> arayüzü ile aynıdır. İkisi de iki tane T türünde parametre alıp bir int döndürmektedir. Comparator dile eklendiğinde ToIntBiFunction henüz yoktu ama olsa bile bunu kullanmak mantıksız olurdu. Ayrı bir Comparator arayüzü yazmamızın birkaç avantajı vardır. Birincisi arayüzün ismi comparator (karşılaştırıcı) arayüzün ne iş yaptığı hakkında bize bilgi vermektedir. İkinci olarak, Comparator nesnelerinin hangi özellikleri taşıması gerektiği konusunda çok kapsamlı bir sözleşme (contract) bulundurmaktadır. Bu arayüzü uyguladığınızda sözleşmeye uyduğunuzu da kabul etmiş olursunuz. Üçüncüsü, arayüz içerisinde bulunan çok sayıdaki varsayılan metot birden fazla karşılaştırıcıyı birleştirmenizi veya farklı biçimlere dönüştürmenizi sağlar.

Eğer sizin de aşağıdaki özelliklerden bir veya daha fazlasını içeren bir fonksiyona ihtiyacınız olursa, standart olanı kullanmak yerine kendiniz yazmayı düşünmelisiniz:

  • Yaygın olarak kullanılmak ve ismi sayesinde kullanıcılara ne amaçla kullanılabileceğini bildirmek
  • Kapsamlı bir sözleşme barındırmak
  • Varsayılan metotlar ile kullanıcılarına fayda sağlamak

Fonksiyonel arayüz yazmaya karar verirseniz bunun bir arayüz olduğunu ve dikkatli tasarlanması gerektiğini unutmayın. (Madde 21)

Yukarıda örnek olarak yazdığımız EldestEntryRemovalFunction arayüzünün @FunctionalInterface notasyonu ile işaretlendiğini farketmişsinizdir. Bu notasyonu kullanmanın çeşitli faydaları vardır. Birincisi, bu notasyonu kullanarak kullanıcılara bu arayüzün lambda fonksiyonları ile birlikte kullanılmak için tasarlandığını bildirmiş oluyoruz. İkinci olarak bir hata sonucu birden fazla soyut metot eklerseniz metot derlenmeyecektir. Aynı şekilde koda daha sonradan bakım amaçlı ekleme yapan kişiler de soyut metot eklemek isterlerse bu mümkün olmaz. Dolayısıyla yazdığınız fonksiyonel arayüzleri mutlaka @FunctionalInterface notasyonu ile işaretleyin.

Özetle, artık lambda fonksiyonları Java diline eklendiğine göre API tasarlarken bunu mutlaka göz önünde bulundurmalıyız. Metot parametrelerinde fonksiyonel arayüz türlerini kabul edin ve/veya bunları dönüş türü olarak kullanın. Java kütüphanelerinin bize sunduğu standart fonksiyonel arayüzleri mümkün olduğunca kullanın, ancak kendi arayüzünüzü yazmanız gereken durumlar olabileceğini de unutmayın.

Share

Leave a Reply

%d bloggers like this: