Döngüler Yerine Pipeline'lar
Bir stream, bir değer dizisini işlemek için kullanılan bir pipeline'dır. Bir kaynaktan - genellikle bir koleksiyondan - başlarsınız, "yalnızca bunları tut", "her birini dönüştür", "sırala" gibi işlemleri zincirler ve sonucu toplayarak bitirirsiniz. Geçici bir liste ve içinde bir if olan bir döngü yazmak yerine, ne istediğinizi tanımlarsınız ve gezinmeyi stream'e bırakırsınız.
Artık lambda'larınız olduğuna göre, stream'ler yerine oturuyor: her işlem, tek bir öğe için yapılacak işi tanımlayan bir lambda (ya da method referansı) alır.
Yukarıdan aşağıya okuyun: isimleri al, uzun olanları tut, büyük harfe çevir, bir listede topla. Orijinal names listesi asla değiştirilmez - bir stream yeni bir sonuç üretir ve kaynağı olduğu gibi bırakır.
Bir Stream'in Anatomisi
Her pipeline'ın üç parçası vardır:
- Bir kaynak -
list.stream(),Arrays.stream(array)ya daStream.of(a, b, c). - Sıfır veya daha fazla ara işlem -
filter,map,sorted,distinct,limit. Her biri başka bir stream döndürür, böylece onları zincirleyebilirsiniz. - Tam olarak bir terminal işlem -
collect,count,forEach,reduce,findFirst. Bu, işi başlatır ve bir sonuç (ya da bir yan etki) üretir.
Bu ayrım, başta herkesi şaşırtan bir kural yüzünden önemlidir.
Ara İşlemler Tembeldir
Ara işlemler tek başlarına hiçbir şey yapmaz. Yalnızca ne olması gerektiğini kaydederler. Pipeline yalnızca bir terminal işlem bir sonuç istediğinde çalışır.
Çalıştırın: map lambda'sı asla yürütülmez. Bir terminal işlem ekleyin, tüm zincir canlanır:
Bu tembellik savaşılacak bir tuhaflık değildir - stream'in işlemleri birleştirmesini ve ihtiyaç duymadığı işi atlamasını sağlar. Ama bu, terminal işlemi olmayan bir pipeline'ın hiçbir şey yapmadığı anlamına gelir; bu da yaygın bir "neden hiçbir şey olmuyor?" hatasıdır.
filter ve map: Tut ve Dönüştür
Bu ikisi yükün büyük kısmını taşır. filter bir Predicate (boolean döndüren bir lambda) alır ve geçen öğeleri tutar. map bir Function alır ve her öğeyi onu uygulamanın sonucuyla değiştirir.
map'in öğe tipini değiştirebileceğine dikkat edin: burada bir String stream'i bir Integer stream'ine dönüşür. String::length bir method referansıdır - w -> w.length() lambda'sının kısaltmasıdır.
Terminal İşlemler Bir Sonuç Üretir
Stream'e şekil verdikten sonra, bir terminal işlem onu somut bir şeye dönüştürür:
Yaygın terminaller: collect (bir liste/küme/map'te topla), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max ve reduce. Bir terminal çalıştıktan sonra stream tüketilir - onu tekrar kullanamazsınız. Yeni bir pipeline için list.stream()'i tekrar çağırın.
Sonuçları Toplama
Collectors ile collect, bir sonuç oluşturmanın iş atıdır. En yaygını Collectors.toList()'tir:
Collectors ayrıca toSet(), joining(...), groupingBy(...) ve counting() de sunar. Java 16 ve sonrasında collect(Collectors.toList()) yerine daha kısa olan .toList() kullanabilirsiniz (değiştirilemez bir liste döndürür):
List<String> result = names.stream().map(String::toUpperCase).toList();
sorted, distinct ve limit
Bu ara işlemler, toplamadan önce stream'i yeniden şekillendirir:
Argümansız sorted() doğal sıralamayı kullanır; özel bir sıralama için bir Comparator (burada bir lambda) geçin. Comparator.reverseOrder() azalan sıralama için daha temiz bir yoldur.
reduce: Bir Stream'i Tek Bir Değere İndirgemek
Tüm öğeleri tek bir sonuçta birleştirmeniz gerektiğinde - bir toplam, bir çarpım, en uzun string - reduce genel araçtır. Ona bir başlangıç değeri ve iki değeri birleştiren bir fonksiyon verirsiniz:
Sıradan toplamlar ve ortalamalar için özelleşmiş stream'ler daha nettir: nums.stream().mapToInt(Integer::intValue).sum(). Hazır bir toplayıcı olmadığında reduce'a başvurun.
Stream'ler Her Döngünün Yerini Tutmaz
Stream'ler, bir koleksiyonu bir sonuca dönüştürdüğünüzde parlar. Bir döngüden otomatik olarak daha hızlı değildirler ve dış durumu değiştirmeniz ya da çetrefilli şekillerde erken çıkmanız gerektiğinde hantal kalırlar. İyi bir kural: pipeline tek bir net cümle gibi okunuyorsa bir stream kullanın; paylaşılan bir sayaca veya bir index'e uzanıyorsanız, sade bir for döngüsü dürüsttür ve gayet iyidir.
Ayrıca bir stream'in tek kullanımlık olduğunu unutmayın. Bu, hata fırlatır:
Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count(); // IllegalStateException: stream has already been operated upon
Sırada: Optional
Birkaç stream terminali - findFirst, min, max, kimlik (identity) olmadan reduce - hiçbir şey bulamayabilir, bu yüzden çıplak bir değer döndürmezler. Java'nın "belki bir değer, belki hiçbir şey" kapsayıcısı olan bir Optional döndürürler; bu da nihayet null döndürmeye temiz bir alternatif sunar. Bu, bir sonraki sayfa.
Sıkça Sorulan Sorular
Java'da stream nedir?
Stream, bir öğe dizisini - genellikle bir koleksiyondan gelen - filter, map ve sorted gibi bir işlem zinciri üzerinden işleyen ve collect ya da count gibi bir uçtaki (terminal) işlemle sona eren bir pipeline'dır. Veri saklamaz ve kaynağı değiştirmez; bir hesaplamayı tanımlar. Bir tanesini list.stream() ile oluşturur ve zinciri bir tarif gibi yukarıdan aşağıya okursunuz.
Java'da bir stream tekrar listeye nasıl dönüştürülür?
Pipeline'ı bir terminal toplayıcıyla bitirin: list.stream().filter(...).collect(Collectors.toList()). Java 16+ sürümünde, değiştirilemez bir liste döndüren daha kısa .toList() kullanabilirsiniz. Bir terminal işlem olmadan hiçbir şey çalışmaz, çünkü filter ve map gibi ara işlemler tembeldir.
Ne zaman for döngüsü yerine stream kullanmalısınız?
Bir koleksiyonu bir sonuca dönüştürürken veya filtrelerken stream'e başvurun - pipeline tek bir net cümle gibi okunur ("isimleri al, uzun olanları tut, büyük harfe çevir, bir listede topla"). Dış durumu değiştirmeniz, karmaşık şekillerde erken çıkmanız ya da mantığın adım adım emir kipiyle daha basit olduğu durumlarda sade bir for döngüsüne sadık kalın. Stream'ler ham hızla değil, netlikle ilgilidir.