Selamlar arkadaşlar, Java 8 yeniliklerinden bahsettiğimiz yazı serisinin 3.bölümüne geldik. Önceki bölümde yardımcı fonksiyonel arayüzlerden (Consumer, Predicate …) bahsetmiştik. Bu yazıda ise özellikle diziler ve Collection’lar üzerinde çeşitli işlemler yapabilmek için kullanabileceğimiz Java’nın bizim için sunmuş olduğu Stream API‘ı inceleyeceğiz.
Stream Nedir ?
Stream kelimesinin türkçesine baktığımızda “akış, nehir, akarsu” olarak belirtilmekte. Aslında bu şekilde düşündüğümüzde yapısını kolayca anlayabiliriz. Bir nehir var ve devamlı akmakta, akıp gittiği için stream içindeki elemanlar sadece bir kere ziyaret edilir. Aynı elemanı tekrar işleme dahil edebilmek için yeni bir stream/nehir oluşturmamız gerekmektedir. Bu akış genel itibariyle kullanmış olduğumuz işlemlerin türüne bağlı olacak şekilde bir boru hattı gibi düşünülebilir.
Stream API’da En Çok Kullanılan Metodlar
Metod | Açıklama |
---|---|
forEach | İterasyon |
filter | Filtreleme |
map | Dönüştürme |
reduce | İndirgeme |
distinct | Tekilleştirme |
sorted | Sıralama |
limit | Limitleme |
count | Sayma |
collect | Toplamak/Biriktirmek |
Bu tabloda bulunan tüm metodları anlatamaya çalışacağım tabi bu tabloda bulunmayan Stream arayüzü içerisinde bulunan çok daha fazla metod bulunmakta. Buradan ulaşabilirsiniz.
Stream Nesnesi Nasıl Elde Edilir ?
Stream bir arayüz olduğundan dolayı doğrudan nesne oluşturmak mümkün değildir. Stream nesnesini elde etmenin birden fazla yolu var. Ancak bu makalede Collection API üzerinde çalışacağımız için ve Collection arayüzünden türeyen nesneler stream() ya da parallelStream() metodlarını çağırarak geriye Stream nesnesi dönebilmektedir.
Stream’in Hayat Döngüsü
Java’da Stream’in hayat döngüsünü 3 işleme ayırabiliriz.
- Dizi veya Collection gibi bir veri kaynağından stream kaynağı oluşturma.
- Bir stream’e filtreleme, sıralama, dönüştürme gibi ara işlemler uygulanıp tekrardan işlenebilecek kaynak oluşturulabilir. Bu sebeple birden fazla kez kullanılabilir, zincirleme işlemler yapılabilir.
- Elimizdeki kaynağa sonuç ürettirebilecek terminal işlemleri olarak adlandıran forEach, reduce gibi işlemler uygulanır. Stream’e hayat döngüsü boyunca yalnızca 1 adet terminal işlemi uygulanabilir.
Yukarıda bahsettiğimiz ara işlemler ve terminal işlemlerini gruplayalım.
Ara İşlemler (Intermediate) | Terminal İşlemleri |
filter(Predicate<T>) | forEach, forEachOrdered |
map(Function<T>) | toArray |
flatmap(Function<T>) | reduce |
sorted(Comparator<T>) | collect |
peek(Consumer<T>) | min, max |
distinct() | count |
limit(long n) | anyMatch, allMatch, noneMatch |
skip(long n) | findAny, findFirst |
Şimdi, en çok kullanılan metodları tek tek anlatmaya çalışacağım…
1. ForEach
En basit ve en çok kullanılan, Stream içindeki veriyi tek tek tüketmek için oluşturulmuş bir yapıdır. Terminal işlemi olduğundan dolayı stream artık tüketilmiş olarak kabul edilir ve artık kullanılamayacağı anlamına gelir.
Örnek üzerinde inceleyelim ;
2. Filter
Dizimiz veya Collection’ımız üzerinde bizim belirteceğimiz koşullar doğrultusunda filtreleme işlemi yapmamıza yarayan bir yapıdır. Predicate arayüzünden türüyen bir parametre bekler. Bu işlem sonrası belirttiğimiz koşula uygun olmayan elemanlar bir sonraki aşamada kullanılamazlar. Artık elimizde filtrelenmiş/süzülmüş/elenmiş bir veri vardır. Örneğe bakalım;
3. Map
Stream içerisinde erişilen her bir eleman üzerinde işlem yapmamıza ve başka elemanlara dönüştürmemize imkan sağlayan map metodu parametre olarak Function arayüzünden türetilmiş bir ifade beklemektedir. Örneğimize bakalım;
4. Reduce
Türkçe karşılığı indirgeme/azaltma olarak geçen reduce çeşitli şekillerde kullanılırken genel olarak map metoduyla birlikte kullanılmakta. Tek başına reduce kullanımında elemanları teker teker işler. Bir önceki adımda elde edilen sonuç, bir sonraki elemanla işlemle tutulur. Reduce metodunu çeşitli halleri vardır ben 2 parametre alan metodunu anlatacağım.
T reduce(T identity, BinaryOperator<T> accumulator);
Burada identity değeri ilk olarak işleme başlarken hangi değer ile başlaması gerektiğini ifade etmekte. Örnek üzerinden inceleyelim;
5. Map & Reduce
Yukarıda hem map işlemini hem de reduce işleminden bahsettik. Bu iki metod birlikte kullanıldıkta çok güçlü olmakta. Örneğin elimizde öğrencilerin isim ve not bilgisinin olduğu bir liste olsun. Bu listede notu 50 üzerinde olan öğrencilerin notlarının toplamını bulalım;
6. Distinct
Stream içerisinde bulunan ve tekrar eden elemanlar varsa bunları distinct metodu ile çıkartabilir ve elimizde tekrar etmeyen birbirinden farklı elemanlar barından bir veri setimiz kalır. Örneğin;
7. Sorted
Bazı durumlarda elimizde ki veri setini belirli bir parametreye göre sıralamak isteyebiliriz. 2 türü bulunmaktadır. Birinci hali parametre almaz küçükten büyüğe şekilde sıralar. İkinci formatı ise Comparator arayüzünden türediği için bizim belirlemiş olduğumuz parametreye göre sıralama işlemi yapar. Örneğin;
8. Limit
Elimizde var olan bir veri kaynağını, göstermek istediğimiz kadarını göstermemize yarayan, sınırlandıran bir metoddur. Parametre olarak vereceğimiz rakamla aslında Stream içerisinde dönen çok sayıda veri var ancak sen bize şu kadarını göster diyoruz. Örnek üzerinden bakacak olursak;
9. Count
Stream içerisinde bulunan toplam veri sayısını öğrenmemize yarar.
10. Collect
Collect metodu Stream türündeki nesneleri başka biçimdeki nesneye, veri yapısına dönüştürmek için kullanılır. Collector arayüzünden türeyen bir parametre beklemektedir. Bu parametre bilgisi ile istenilen türe dönüşüm sağlanabilir. Collector türünden arayüzler Collectors sınıfının içinde bulunan metodlar ile elde edilebilir. Örnek üzerinden inceleyelim;
11. AnyMatch & AllMatch & NoneMatch
Bu 3 metodda Predicate arayüzünden türemiş bir parametre bekler. İsimlerini türkçeye çevirdiğimizde aslında az çok anlayabiliyoruz. Şöyle ki;
- AnyMath: Vereceğimiz şarta bağlı olarak Stream içerisinde gezinir ve herhangi bir elemanla eşleşme durumunda true dönecektir.
- AllMatch: Verilen şarta göre Stream içerisindeki tüm elemanların bu şarta uyması durumunda true dönecektir.
- NoneMatch: Şarta göre Stream içindeki hiç bir elemanın şartı sağlamaması durumunda true dönecektir.
Örnek üzerinden incelemeye çalışalım.
12. Paralel Stream
Stream bizlere paralel işlem yapabilme kabiliyeti ile performans açısından önemsenecek sonuçlar vermekte. Örnek olarak bir ülke listemiz olsun ve bu ülke listemizi forEach ile gezelim ama her elemanı gezerken 1 sn gecikme koyalım. Böylelikle paralel işlem yaptığımızda farklı bir thread’e görev devredilecek mi izleyelim;
Programı çalıştırdığımızda çıktı aşağıdaki gibi olacaktır. Toplam geçen süreye baktığımızda aradaki farkı görebiliyoruz. Bu örnekle stream’i paralel şekilde yürütebileceğimizi ve performansa etkisini bir nebzede olsa gözlemleyebildiğimizi söyleyebiliriz.
Yazımı burada sonlandırıyorum. Stream API hakkında örnekler üzerinden bilgi vermeye çalıştım. Bir sonraki yazıda Java 8 ile gelen tarih ve saat işlemlerinde kolaylık sağlayan LocalDate ve LocalTime sınıflarından, ayrıca Optinal sınıfından bahsedeceğim. Umarım yazdıklarım faydalı olmuştur. 👌
Kaynaklar
- https://kodedu.com/java-8-ebook/
- https://dogukanhan.com/java-stream-api-yazi-serisi/
- https://medium.com/@metinalniacik/allmatch-anymatch-ve-nonematch-kullan%C4%B1m%C4%B1-1449fbe96008
- https://medium.com/@sinanselimoglu/java-8-streams-paralel-i%CC%87%C5%9Flemler-3-a01233c8fd9f
- https://www.mobilhanem.com/stream-api-filter-sorted-map-match-reduce/
- https://howtodoinjava.com/java8/java-streams-by-examples/