Effective Java Madde 46: Stream Kullanırken Yan Etkisi Olmayan Fonksiyonları Tercih Edin

Stream kullanmaya yeni başladıysanız alışmak zaman alabilir. Yapmak istediğiniz hesaplamayı stream hatları (stream pipeline) kullanarak ifade etmek zor gelebilir. Bunu başardığınız zaman da size pek bir faydası olmadığını düşünebilirsiniz. Streamler sadece yeni bir API değil, aynı zamanda bir fonksiyonel programlamaya yaklaşımıdır. Streamlerden en üst düzeyde faydalanabilmek için sadece API’ı değil bu yaklaşımı da benimsemelisiniz.

Bu yaklaşımın en önemli tarafı yapmak istediğiniz hesaplamayı aşamalı bir dönüşüm olarak ifade edebilmektir. Bu dönüşümler yapılırken mümkün olduğunca saf fonksiyonlar kullanılması önemlidir. Saf fonksiyonlar sonuçları sadece girdilere (input) bağlı olan ve nesne durumunda değişiklik yapmayan fonksiyonlardır. Bunun mümkün olması için de hem ara hem de sonlandırıcı işlem fonksiyonlarının yan etkisiz olması gerekmektedir. Başka bir deyişle fonksiyonun üreteceği sonuç dış etmenlerden etkilenmemeli ve durum değişikliğine sebep olmamalıdır.

Bazen aşağıdaki gibi yazılmış stream kodlarıyla karşılaşabilirsiniz. Bu program bir dosyadaki kelimelerin hangi sıklıkla yer aldığını hesaplamakta ve bir sıklık tablosunda (frequency table) saklamaktadır:

// Stream API kullanıyor ama fonksiyonel programlama yaklaşımından 
// uzak. YAPMAYIN! 
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    }); 
}

Bu koddaki problem nedir? En nihayetinde Stream API, lambda fonksiyonu ve metot referansı kullanarak doğru hesaplamayı yapıyor. Basitçe söylemek gerekirse bu kod stream kodu değildir, stream kodu kılığına girmiş yinelemeli (iterative) koddur. Stream API’dan herhangi bir fayda sağlamadığı gibi anlaşılması da kısmen zordur. Aynı işi yapan yinelemeli bir kod daha kısa ve anlaşılır olacaktır. Buradaki problemin kaynağı ise bütün işin sonlandırıcı işlem olan forEach metodu içinde yapılıyor olmasıdır. Burada lambda fonksiyonu freq veri yapısında değişiklikler yapmakta, yani bir nesnenin durumunu değiştirmektedir. forEach metodunun önceki ara işlemlerde elde edilen sonuçların raporlanması haricinde kullanımı “kötü kokan kod” (code smell) anlamına gelmektedir. Nesne durumlarında değişiklik yapan lambda fonksiyonları da bu kategoriye girer. Peki bu kod nasıl yazılmalıdır?

// Kelime sıklığını hesaplama - streamlerin doğru kullanımı
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
    freq = words
        .collect(groupingBy(String::toLowerCase, counting()));
}

Bu kod öncekiyle aynı işi yapmaktadır ve streamlerin doğru kullanımına örnektir. Daha kısa ve temizdir. Peki bunun yerine programcılar neden önceki versiyonu tercih ediyorlar? Bu sorunun cevabı basittir: alışkanlıklar. Java programcıları for döngülerini çok iyi bilirler ve forEach metodu da buna benzediği için ilk tercihleri o olur. Ancak forEach metodu aslında sonlandırıcı stream işlemleri arasında en zayıfı ve fonksiyonel programlama yaklaşımına en aykırı olanıdır, çünkü esasında yinelemeli kod yazmaya teşvik eder. Ayrıca paralel streamlerle birlikte çalışmaya da uygun değildir. Dolayısıyla forEach metodu sadece önceki hesaplamaları raporlamak için kullanılmalıdır, hesaplama yapmak için değil.

Geliştirilmiş kodda kullanılan collect metodu bir toplayıcıyı (collector) temsil etmektedir. Bu, streamleri doğru kullanmak için öğrenilmesi gereken çok önemli bir kavramdır. Toplayıcıları üreten Collectors API otuz dokuz tane metoda sahip olduğu için gözünüzü korkutabilir ama bu detay seviyesi esasında bizi ilgilendirmiyor. Bu karmaşık arayüzü görmezden gelip toplayıcıları bir indirgeme stratejisi içeren nesneler gibi düşünebilirsiniz. Burada indirgemeden kasıt çok sayıdaki stream elemanlarının tek bir nesneyle ifade edilmesidir. Toplayıcı tarafından üretilen bu nesneler genellikle bir koleksiyon (Collection) olur.

Stream elemanlarını bir Collection içinde toplayan üç tane toplayıcı vardır: toList(), toSet() ve toCollection(collectionFactory). Bunlar sırasıyla bir liste (List), bir küme (Set) ve programcı tarafından belirlenen bir koleksiyon türü döndürürler. Bu bilgiyle, az önce yarattığımız kelimelerin sıklık tablosundan en sık kullanılan on kelimeyi bize döndüren bir stream hattı yazabiliriz.

// Sıklık tablosundaki ilk on kelimeyi bulan kod
List<String> topTen = freq.keySet().stream() 
    .sorted(comparing(freq::get).reversed())
    .limit(10)
    .collect(toList());

Dikkat ederseniz toList statik bir metot olmasına rağmen yazarken sınıf adı kullanmadık. Stream hatlarını daha okunabilir hale getirmek için burada olduğu gibi Collectors metotlarının statik olarak import edilmesi mantıklı olacaktır.

Bu kodda üzerinde konuşulması gereken kısım sorted metoduna geçilen comparing(freq::get).reversed() ifadesidir. Buradaki comparing metodu bir Comparator nesnesi üretmektedir (Madde 14). Argüman olarak geçilen freq::get metot referansı ise tabloda anahtarlara karşılık gelen kelimelerin kaç kere kullanıldığını, yani sıklığını döndürmektedir. Son olarak Comparator üzerinden çağrılan reversed metodu ise sıralamayı tersine çevirmektedir, çünkü biz sıralamayı en sık kullanılandan en aza doğru yapmak istiyoruz. Sonrasında bu hesaplamayı on elemanla kısıtlamak için limit metodu çağrılmakta ve sonuçlar bir listede toplanmaktadır.

Peki toplayıcı üretmek için tanımlanmış diğer otuz altı metot ne için var? Bunların çoğu stream elemanlarını koleksiyon yerine bir map içinde toplamak için yazılmıştır ve çok daha karmaşık olabilirler. Bunları kullanırken her stream elemanı bir anahtar ve bir değerle ifade edilecek şekilde dönüştürülmektedir, bazı elemanlar aynı anahtar değerini paylaşabilirler.

En basit map toplayıcısı toMap(keyMapper, valueMapper) şeklinde ifade edilir. İlk fonksiyon parametresi stream elemanlarını bir anahtarla, ikinci fonksiyon ise bir değerle eşlemek için kullanılır. Madde 34’de fromString metodunu yazarken bunu kullanmıştık. Burada anahtar olarak enum sabitinin String formu, değer olarak da enum sabitinin kendisi kullanılmaktadır:

// toMap toplayıcısı kullanarak enum sabitlerinden map oluşturmak
private static final Map<String, Operation> stringToEnum =
    Stream.of(values()).collect(
        toMap(Object::toString, e -> e));

toMap toplayıcısının bu basit versiyonu her stream elemanının kendine özgü (unique) bir anahtarla ifade edilebildiği durumlar için çok uygundur. Ancak birden fazla eleman aynı anahtarla eşleşirse IllegalStateException fırlatılır.

Birden fazla elemanın aynı anahtarla eşleştiği durumları ele almak için toMap metodunun farklı biçimleri veya groupingBy metodu kullanılabilir. Buna örnek olarak toMap metodunun üç parametreli biçimini verebiliriz: toMap(keyMapper, valueMapper, mergeFunction). Burada yeni eklenen mergeFunction parametresi V map için değer türünü ifade etmek üzere BinaryOperator<V> türünde bir fonksiyondur. Aynı anahtarla eşleşen değerler bu fonksiyon kullanılarak birleştirilebilirler. Örneğin, mergeFunction yerine matematiksel çarpım yapan bir fonksiyon geçersek, aynı anahtara sahip olan değerler birbirleriyle çarpılarak tek bir değer üretilecektir.

Üç parametreli toMap versiyonunu başka şekillerde de kullanabiliriz. Farzedelim ki elimizde çeşitli sanatçılara ait albümleri içeren bir stream olsun. Biz de her sanatçıyı en çok satan albümüyle eşleştiren bir map oluşturmak istiyoruz. Aşağıdaki kod işimizi görecektir:

// anahtarları seçilmiş elemanlarla eşleyen toplayıcı örneği
Map<Artist, Album> topHits = albums.collect( 
    toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));

Bu kodda kullanılan maxBy metodu BinaryOperator arayüzünden statik import ile gelmektedir. Bu metot bir Comparator<T> nesnesi alıp bunu BinaryOperator<T>‘ye dönüştürmektedir. Bu örnekte Comparator nesnesi comparing(Album::sales) tarafından üretilmektedir. Bunun anlamı aslında şudur: aynı anahtarla (sanatçı) eşleşen ikinci bir değer (albüm) bulunursa, bunu mevcut değerle satış rakamlarını kullanarak (Album::sales) karşılaştır. İçlerinden hangisi daha fazla satış rakamına sahipse (maxBy) anahtarı o değerle eşleştir. Böylece aynı sanatçıya sahip albümler stream tarafından işlendikçe bu karşılaştırma yapılacak ve sonunda en çok satış rakamına sahip albüm eşlenmiş olacaktır.

Üç parametreli toMap toplayıcısını aşağıdaki gibi de kullanabiliriz:

toMap(keyMapper, valueMapper, (v1, v2) -> v2)

Bu kod aynı anahtarla eşleşen ikinci bir değer bulduğunda ikinci değeri ilk değerin üzerine yazmaktadır, yani ilk değer kaybolmaktadır. Her ne kadar birçok stream için bu kabul edilebilir bir durum olmasa da bu değerlerin birbirleriyle aynı olduğu veya hepsinin geçerli olduğu durumlarda kullanılabilir.

toMap metodunun bir de dört parametreli versiyonu vardır. Dördüncü parametre map üreten bir fabrikayı temsil etmektedir. Sonuçta üretilen map türünü özellikle belirlemek istiyorsanız (EnumMap, TreeMap gibi) bunu kullanabilirsiniz. Ayrıca paralel işletim için kullanılan ConcurrentHashMap üreten versiyonları da toConcurrentMap olarak tanımlanmıştır.

Collectors API toMap metotlarına ek olarak bir de groupingBy sunmaktadır. Bu metot stream elemanlarını gruplayarak bir map ile ifade edilebilmelerini sağlar. Gruplamanın neye göre yapılacağı argüman olarak geçilen sınıflandırma fonksiyonu tarafından belirlenir. Başka bir deyişle bu fonksiyonun görevi bir stream elemanını alıp hangi gruba girdiğini döndürmektir. Bu metodunun en basit hali Madde 45’de anagramları gruplarken kullandığımız şekliyledir:

words.collect(groupingBy(word -> alphabetize(word)))

Hatırlarsanız burada kelimeleri alfabetik olarak sıralayan alphabetize metodu gruplama için kullanılıyordu. Yani alfabetik olarak sıralandığında aynı sonucu üreten kelimeler anagram oldukları için aynı gruba dahil oluyordu. Bu haliyle groupingBy metodu aynı gruptaki birden fazla elemanı temsil etmek için map değerleri olarak liste yapıları kullanır. Bunu değiştirmek için groupingBy metoduna ikinci argüman olarak başka bir toplayıcı geçebilirsiniz. Bu ikincil toplayıcı aynı gruba düşen elemanları tek bir koleksiyona indirgemek için kullanılacaktır. Örneğin toSet() geçerseniz aynı gruptaki elemanlar liste yapılarında değil kümelerde (set) saklanacaktır. Buna alternatif olarak toCollection(collectionFactory) da geçebilirsiniz. Böylece aynı gruptaki elemanların ne tür bir koleksiyonda toplanmasını istediğinizi belirtebilirsiniz.

İki parametreli groupingBy metodunu counting() toplayıcısını geçerek de kullanabilirsiniz. Bu kullanım aynı gruptaki elemanları liste veya küme gibi bir koleksiyonda tutmak yerine sadece gruptaki eleman sayısını tutacaktır. Bu yazının başında gördüğünüz kelimelerin kullanım sıklığını hesaplayan kod bunu yapmaktadır:

Map<String, Long> freq = words 
    .collect(groupingBy(String::toLowerCase, counting()));

groupingBy metodunun üç parametreli versiyonu ise toMap‘de olduğu gibi önceki parametrelere ek olarak map fabrikası geçmemize olanak sağlar. Bunu kullanarak örneğin değerleri TreeSet türünde olan bir TreeMap oluşturabilirsiniz. Paralel işletimde kullanılmak üzere ConcurrentHashMap döndüren groupingByConcurrent metotları da mevcuttur.

counting metodu tarafından döndürülen toplayıcılar groupingBy örneğinde gördüğümüz gibi ikincil toplayıcı olarak kullanılmak için tasarlanmıştır. Aynı işlevsellik count metodu ile streamler için zaten mevcuttur. Bu sebeple collect(counting()) gibi bir kullanım için hiçbir sebep yoktur. Collectors içinde bu özelliği taşıyan on beş tane daha toplayıcı vardır. Bunların içinde isimleri summing, averaging ve summarizing ile başlayan toplayıcılar, reducing metodu ve bunun aşırı yüklenmiş (overloaded) versiyonları, filtering, mapping, flatMapping ve collectingAndThen metotları vardır. Bunlar streamlerde zaten mevcut olan işlevselliği toplayıcılar için tekrarlamaktadır. Buradaki amaç ikincil toplayıcıların küçük bir stream gibi davranabilmesini sağlamaktır.

Collectors metotları içinde yer alan ve sadece String gibi CharSequence nesneleri ile kullanılabilen joining metodundan da bahsetmekte fayda var. Parametresiz kullanıldığında bu metot streamdeki elemanların hepsini arka arkaya ekleyen bir toplayıcı döndürecektir. Tek parametre ile kullanıldığında elemanları arka arkaya eklerken bir ayırıcı belirtebilirsiniz. Örneğin virgül kullanırsanız elemanlar arasına virgül eklenmiş şekilde birleştirilirler. Üç parametreli versiyonunda ise ayrıcının yanında önek (prefix) ve sonek (suffix) belirtebilirsiniz. Sonuçta toplayıcının üreteceği değerler koleksiyonları yazdırdığınızda gördüğünüz gibi olacaktır, örneğin [came, saw, conquered]

Özetle, stream hatlarını programlanın özü yan etkisi olmayan fonksiyonlar kullanmaktır. Sonlandırıcı işlem olan forEach sadece stream hattı tarafından yapılmış hesaplamayı raporlamak için kullanılmalıdır, hesaplamanın kendisi için değil. Streamleri doğru kullanabilmek için toplayıcıları (collector) iyi bilmeniz gerekmektedir. Bunlardan en önemlileri toList, toSet, toMap, groupingBy ve joining toplayıcılarıdır.

Share

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