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üz | Fonksiyon İ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.