İçeriğe geç

Java Stream API: Koleksiyonları Fonksiyonel Programlama ile Yönetmenin Tam Rehberi

T
Tolgahan
· · 14 dk okuma · 89 görüntülenme

Java Stream API: Koleksiyonları Fonksiyonel Programlama ile Yönetmenin Tam Rehberi

Bir e-ticaret projesinde çalıştığını düşün. Elinde yüz binlerce sipariş var. Bu siparişlerden son 30 günde yapılmış, toplam tutarı 500 TL'nin üstünde olan ve İstanbul'a gönderilmiş olanları bulmak, bunları toplam tutara göre sıralamak ve ilk 10'unu almak istiyorsun. Klasik Java'da bu iş üç tane iç içe for döngüsü, birkaç if bloğu ve en az 20-25 satır kod demek. Stream API ile? Tek bir zincirleme ifade.

Java 8 ile hayatımıza giren Stream API, koleksiyonlar üzerinde fonksiyonel (functional) ve bildirimsel (declarative) işlemler yapmamızı sağlayan bir araçtır. "Ne yapılacağını" söylersin, "nasıl yapılacağını" Stream halleder. Bu yazıda Stream API'yi temelden ileri seviyeye kadar, gerçek dünya örnekleriyle ve yaygın tuzaklarıyla birlikte derinlemesine inceleyeceğiz.

Stream Nedir? Koleksiyon mu, Veri Yapısı mı?

İlk ve en kritik ayrım: Stream bir veri yapısı değildir. Veri saklamaz. Stream, bir veri kaynağı üzerinden akan ve o veri üzerinde işlemler uygulamanı sağlayan bir boru hattıdır (pipeline).

Bunu şöyle düşün: elinde bir fabrika üretim bandı var. Hammadde (veri kaynağı — liste, dizi, dosya) bandın başından girer. Band üzerinde sırasıyla filtreleme, dönüştürme, sıralama gibi istasyonlar var. Sonunda ürün (sonuç) çıkar. Üretim bandının kendisi hammaddeyi "saklamaz" — sadece işler ve iletir. Stream de tam olarak bu.

Bir Stream pipeline üç parçadan oluşur:

  1. Kaynak (Source): Verinin geldiği yer — List, Set, Map, dizi, dosya, hatta sonsuz bir üretici

  2. Ara işlemler (Intermediate Operations): filter, map, sorted, distinct gibi — Stream döndürürler, zincirleme çalışırlar

  3. Terminal işlem (Terminal Operation): collect, forEach, reduce, count gibi — pipeline'ı tetikler ve sonuç üretir

Kritik bir nokta: Ara işlemler tembel (lazy) çalışır. Sen filter() veya map() çağırdığında hiçbir şey olmaz. Gerçek iş, terminal işlem çağrıldığında başlar. Bu tembellik, Stream API'nin performans optimizasyonları yapabilmesinin temelidir.

Temel Stream İşlemleri: Filtreleme, Dönüştürme, Toplama

Hadi temelden başlayalım. Bir çalışan listesi üzerinde çeşitli işlemler yapalım:

import java.util.*;
import java.util.stream.*;

class Main {
    // Çalışan sınıfı
    record Employee(String name, String department, double salary, int age) {}

    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Ahmet", "Engineering", 45000, 28),
            new Employee("Ayşe", "Engineering", 52000, 32),
            new Employee("Mehmet", "Marketing", 38000, 25),
            new Employee("Fatma", "Engineering", 61000, 35),
            new Employee("Can", "Marketing", 42000, 29),
            new Employee("Zeynep", "HR", 35000, 24),
            new Employee("Ali", "HR", 48000, 31),
            new Employee("Elif", "Engineering", 55000, 30)
        );

        // 1. Mühendislik departmanındaki çalışanların isimlerini al
        List<String> engineerNames = employees.stream()
            .filter(e -> e.department().equals("Engineering"))
            .map(Employee::name)
            .collect(Collectors.toList());
        System.out.println("Mühendisler: " + engineerNames);

        // 2. Maaşı 40000'den yüksek olanları maaşa göre sırala
        List<Employee> highEarners = employees.stream()
            .filter(e -> e.salary() > 40000)
            .sorted(Comparator.comparingDouble(Employee::salary).reversed())
            .collect(Collectors.toList());
        System.out.println("Yüksek maaşlılar:");
        highEarners.forEach(e -> System.out.println("  " + e.name() + " - " + e.salary()));

        // 3. Toplam maaş harcaması
        double totalSalary = employees.stream()
            .mapToDouble(Employee::salary)
            .sum();
        System.out.println("Toplam maaş: " + totalSalary);

        // 4. Departmana göre ortalama maaş
        Map<String, Double> avgByDept = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.averagingDouble(Employee::salary)
            ));
        System.out.println("Departman ortalamaları: " + avgByDept);

        // 5. En genç çalışan
        employees.stream()
            .min(Comparator.comparingInt(Employee::age))
            .ifPresent(e -> System.out.println("En genç: " + e.name() + " (" + e.age() + ")"));
    }
}

Bu örnekte beş farklı Stream işlemi görüyorsun: filtreleme (filter), dönüştürme (map), sıralama (sorted), gruplama (groupingBy) ve indirgeme (min). Her biri tek bir zincirleme ifade. For döngüsü yok, geçici değişken yok.

filter() bir Predicate alır — her eleman için true/false döner. True dönen elemanlar devam eder, false dönenler elenir. map() bir Function alır — her elemanı başka bir şeye dönüştürür. collect() ise sonuçları toplar — listeye, map'e, string'e, istediğin yapıya.

Collectors: Sonuçları İstediğin Şekle Sok

Collectors sınıfı, Stream sonuçlarını toplamak için hazır metotlar sunar. Bu metotlar inanılmaz güçlü ve çoğu zaman yeterince kullanılmıyor. İşte en pratik olanları:

import java.util.*;
import java.util.stream.*;

class Main {
    record Product(String name, String category, double price, boolean inStock) {}

    public static void main(String[] args) {
        List<Product> products = List.of(
            new Product("Laptop", "Electronics", 25000, true),
            new Product("Phone", "Electronics", 15000, true),
            new Product("Tablet", "Electronics", 12000, false),
            new Product("Desk", "Furniture", 5000, true),
            new Product("Chair", "Furniture", 3000, true),
            new Product("Lamp", "Furniture", 800, false),
            new Product("Book", "Education", 150, true),
            new Product("Course", "Education", 500, true)
        );

        // 1. Kategoriye göre gruplama
        Map<String, List<Product>> byCategory = products.stream()
            .collect(Collectors.groupingBy(Product::category));
        byCategory.forEach((cat, prods) ->
            System.out.println(cat + ": " + prods.size() + " ürün"));

        // 2. Stokta var / yok olarak ayırma (partitioning)
        Map<Boolean, List<Product>> partitioned = products.stream()
            .collect(Collectors.partitioningBy(Product::inStock));
        System.out.println("Stokta olan: " + partitioned.get(true).size());
        System.out.println("Stokta olmayan: " + partitioned.get(false).size());

        // 3. Kategoriye göre toplam fiyat
        Map<String, Double> totalByCategory = products.stream()
            .collect(Collectors.groupingBy(
                Product::category,
                Collectors.summingDouble(Product::price)
            ));
        System.out.println("Kategori toplamları: " + totalByCategory);

        // 4. Virgülle ayrılmış ürün listesi
        String productNames = products.stream()
            .map(Product::name)
            .collect(Collectors.joining(", "));
        System.out.println("Tüm ürünler: " + productNames);

        // 5. Kategoriye göre en pahalı ürün
        Map<String, Optional<Product>> mostExpensive = products.stream()
            .collect(Collectors.groupingBy(
                Product::category,
                Collectors.maxBy(Comparator.comparingDouble(Product::price))
            ));
        mostExpensive.forEach((cat, prod) ->
            prod.ifPresent(p -> System.out.println(cat + " en pahalı: " + p.name())));

        // 6. İstatistik özeti (count, sum, min, max, average bir arada)
        DoubleSummaryStatistics stats = products.stream()
            .mapToDouble(Product::price)
            .summaryStatistics();
        System.out.println("Fiyat istatistikleri:");
        System.out.println("  Ortalama: " + stats.getAverage());
        System.out.println("  Min: " + stats.getMin());
        System.out.println("  Max: " + stats.getMax());
        System.out.println("  Toplam: " + stats.getSum());
    }
}

groupingBy bir SQL'deki GROUP BY'ın Java karşılığı. İkinci parametre olarak downstream collector alır — bu sayede gruplama sonrası toplama, sayma, ortalama gibi işlemleri zincirleme yaparsın. partitioningBy ise özel bir gruplama — veriyi true/false olarak ikiye böler. joining birleştirme için, summingDouble / averagingDouble sayısal hesaplamalar için kullanılır.

DoubleSummaryStatistics ise tek geçişte hem sayım, hem toplam, hem ortalama, hem minimum hem de maksimum değerleri hesaplar. Ayrı ayrı stream açmak yerine tek seferde her şeyi alırsın.

flatMap: İç İçe Yapıları Düzleştir

map her elemanı bire bir dönüştürür. Peki ya bir eleman birden fazla sonuç üretiyorsa? İşte flatMap burada devreye girer. Her elemanı bir Stream'e dönüştürür ve tüm bu Stream'leri tek bir düz Stream'de birleştirir.

import java.util.*;
import java.util.stream.*;

class Main {
    record Order(String customer, List<String> items) {}

    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("Ahmet", List.of("Laptop", "Mouse", "Keyboard")),
            new Order("Ayşe", List.of("Phone", "Case")),
            new Order("Mehmet", List.of("Tablet", "Keyboard", "Monitor")),
            new Order("Fatma", List.of("Mouse", "Webcam"))
        );

        // Tüm siparişlerdeki benzersiz ürünleri bul
        // map kullansan: Stream<List<String>> olur — istemediğin şey
        // flatMap kullansan: Stream<String> olur — tam istediğin şey
        List<String> allUniqueItems = orders.stream()
            .flatMap(order -> order.items().stream())  // Her siparişin item listesini düzleştir
            .distinct()  // Tekrarları kaldır
            .sorted()    // Alfabetik sırala
            .collect(Collectors.toList());
        System.out.println("Benzersiz ürünler: " + allUniqueItems);

        // Her müşterinin kaç ürün sipariş ettiği
        orders.stream()
            .map(o -> o.customer() + ": " + o.items().size() + " ürün")
            .forEach(System.out::println);

        // En çok sipariş edilen ürün
        orders.stream()
            .flatMap(order -> order.items().stream())
            .collect(Collectors.groupingBy(
                item -> item,
                Collectors.counting()
            ))
            .entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .ifPresent(e -> System.out.println("En popüler ürün: " + e.getKey()
                + " (" + e.getValue() + " sipariş)"));

        // Cümlelerdeki kelimeleri ayırma örneği
        List<String> sentences = List.of(
            "Java Stream API çok güçlü",
            "Fonksiyonel programlama temiz kod sağlar",
            "Stream lazy evaluation kullanır"
        );
        long wordCount = sentences.stream()
            .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
            .count();
        System.out.println("Toplam kelime sayısı: " + wordCount);
    }
}

flatMap'in mantığı basit: map + flatten. Önce her elemanı bir koleksiyona (Stream'e) dönüştür, sonra hepsini düzleştirip tek bir Stream yap. İç içe listeler, çok seviyeli yapılar ve one-to-many ilişkiler için vazgeçilmez bir araç.

reduce: Tüm Elemanları Tek Bir Değere İndirge

reduce, Stream'deki tüm elemanları birleştirip tek bir sonuç üretir. Toplama, çarpma, string birleştirme, en büyük/en küçük bulma — hepsi reduce'un özel halleri.

import java.util.*;
import java.util.stream.*;

class Main {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Toplam — başlangıç değeri ile
        int sum = numbers.stream()
            .reduce(0, Integer::sum);
        System.out.println("Toplam: " + sum);

        // Çarpım — başlangıç değeri ile
        int product = numbers.stream()
            .reduce(1, (a, b) -> a * b);
        System.out.println("Çarpım: " + product);

        // En büyük — Optional döner (liste boş olabilir)
        numbers.stream()
            .reduce(Integer::max)
            .ifPresent(max -> System.out.println("En büyük: " + max));

        // String birleştirme ile reduce
        List<String> words = List.of("Java", "Stream", "API", "Güçlü");
        String combined = words.stream()
            .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
        System.out.println("Birleşik: " + combined);

        // Gerçek dünya: Alışveriş sepeti toplam tutarı
        record CartItem(String name, double price, int quantity) {}
        List<CartItem> cart = List.of(
            new CartItem("Laptop", 25000, 1),
            new CartItem("Mouse", 350, 2),
            new CartItem("USB Kablo", 75, 3)
        );

        double total = cart.stream()
            .mapToDouble(item -> item.price() * item.quantity())
            .sum();
        System.out.println("Sepet toplamı: " + total + " TL");

        // reduce ile aynı işlem (daha genel form)
        double totalWithReduce = cart.stream()
            .map(item -> item.price() * item.quantity())
            .reduce(0.0, Double::sum);
        System.out.println("Sepet toplamı (reduce): " + totalWithReduce + " TL");
    }
}

reduce iki form alır: başlangıç değerli (reduce(identity, accumulator)) ve başlangıç değersiz (reduce(accumulator)). Başlangıç değersiz form Optional döner çünkü Stream boş olabilir. Sayısal işlemler için mapToInt, mapToDouble gibi ilkel Stream'ler ve onların sum(), average(), max() metotları genellikle daha okunabilir ve performanslı bir alternatif sunar.

Yaygın Hatalar: Herkesin Düştüğü Tuzaklar

Stream API'de en sık yapılan hatalar ve nasıl kaçınılacağı:

1. Stream'i Birden Fazla Kez Kullanmak

// YANLIŞ — Stream tek kullanımlıktır!
Stream<String> stream = List.of("a", "b", "c").stream();
stream.forEach(System.out::println);  // Çalışır
stream.forEach(System.out::println);  // IllegalStateException fırlatır!

// DOĞRU — Her kullanımda yeni Stream oluştur
List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println);  // İlk kullanım
list.stream().forEach(System.out::println);  // İkinci kullanım — sorunsuz

Stream tek seferlik bir yapıdır. Terminal işlem çağrıldığında tükenir. Kaynağı saklayıp her seferinde yeni Stream oluştur.

2. Stream İçinde Dış Durumu Değiştirmek (Side Effect)

// YANLIŞ — dış listeyi stream içinden değiştirme
List<String> results = new ArrayList<>();
List.of("java", "python", "cpp").stream()
    .filter(s -> s.length() > 3)
    .forEach(results::add);  // Side effect — tehlikeli, özellikle parallel stream'de

// DOĞRU — collect kullan
List<String> results2 = List.of("java", "python", "cpp").stream()
    .filter(s -> s.length() > 3)
    .collect(Collectors.toList());

forEach içinde dış bir koleksiyona eleman eklemek, özellikle parallelStream kullandığında race condition'a yol açar. Sonuçları her zaman collect ile topla.

3. Gereksiz Yerde Stream Kullanmak

// YANLIŞ — basit bir döngü için Stream overkill
List.of("a").stream().forEach(System.out::println);

// DOĞRU — basit iterasyon için enhanced for yeterli
for (String s : List.of("a")) {
    System.out.println(s);
}

// Stream'in gerçekten parladığı yer: zincirleme dönüşümler
List<String> result = employees.stream()
    .filter(e -> e.salary() > 40000)
    .map(Employee::name)
    .sorted()
    .collect(Collectors.toList());

Tek bir forEach için Stream açmak gereksiz karmaşıklık. Stream, zincirleme dönüşümler olduğunda gerçek gücünü gösterir.

4. Sıralama Tuzağı: sorted() Her Zaman Stabil midir?

sorted() stabil sıralama yapar (eşit elemanların orijinal sırası korunur). Ama parallelStream ile kullanıldığında sıralamanın korunacağına dikkat etmen gerekir — forEachOrdered kullanmazsan terminal işlemde sıra bozulabilir.

5. Optional Zincirinde NPE

// YANLIŞ — get() çağırmadan önce kontrol yok
String name = employees.stream()
    .filter(e -> e.department().equals("Finance"))
    .findFirst()
    .get();  // NoSuchElementException — Finance departmanı yok!

// DOĞRU — orElse, orElseGet veya ifPresent kullan
String safeName = employees.stream()
    .filter(e -> e.department().equals("Finance"))
    .findFirst()
    .map(Employee::name)
    .orElse("Bulunamadı");

Optional.get() çağırmak, null kontrolü yapmadan referans kullanmakla aynı şey. Her zaman orElse, orElseGet, ifPresent veya map kullan.

Parallel Stream: Ne Zaman Kullanmalı, Ne Zaman Kaçınmalı?

parallelStream() işlemleri birden fazla thread'e dağıtarak hızlandırır — ama her durumda hızlandırmaz.

Kullan:

  • Eleman sayısı çok fazlaysa (on binlerce veya daha fazla)

  • Her eleman üzerindeki işlem CPU yoğunsa (hesaplama, dönüştürme)

  • Elemanlar arasında bağımlılık yoksa

  • Paylaşılan mutable state yoksa

Kaçın:

  • Küçük koleksiyonlarda (thread yönetim maliyeti > kazanç)

  • I/O işlemlerinde (veritabanı, dosya, ağ) — thread havuzu tıkanır

  • Sıra önemliyse ve forEachOrdered kullanmıyorsan

  • LinkedList gibi rastgele erişimi yavaş veri yapılarında

import java.util.*;
import java.util.stream.*;

class Main {
    public static void main(String[] args) {
        // Büyük veri setinde parallel stream etkisi
        List<Integer> largeList = new ArrayList<>();
        for (int i = 0; i < 10_000_000; i++) {
            largeList.add(i);
        }

        // Sıralı (sequential) stream
        long start = System.currentTimeMillis();
        long count = largeList.stream()
            .filter(n -> n % 2 == 0)
            .mapToLong(n -> (long) n * n)
            .filter(n -> n > 1000)
            .count();
        long seqTime = System.currentTimeMillis() - start;
        System.out.println("Sequential: " + seqTime + "ms, sonuç: " + count);

        // Parallel stream
        start = System.currentTimeMillis();
        long countParallel = largeList.parallelStream()
            .filter(n -> n % 2 == 0)
            .mapToLong(n -> (long) n * n)
            .filter(n -> n > 1000)
            .count();
        long parTime = System.currentTimeMillis() - start;
        System.out.println("Parallel: " + parTime + "ms, sonuç: " + countParallel);
    }
}

Parallel stream ortak ForkJoinPool.commonPool() kullanır. Bu havuzun varsayılan thread sayısı Runtime.getRuntime().availableProcessors() - 1 kadardır. Eğer bir parallel stream uzun süren bir I/O işlemi yaparsa, diğer parallel stream'ler de dahil olmak üzere tüm uygulama etkilenir. I/O yoğun işlemlerde parallel stream yerine özel thread havuzları veya CompletableFuture tercih et.

Best Practices: Profesyonel İpuçları

1. Stream pipeline'ları kısa ve okunabilir tut. Tek bir pipeline 5-6 işlemden fazlaysa, ara sonuçları açıklayıcı değişkenlere ata veya yardımcı metotlara ayır.

2. Method reference tercih et. Lambda e -> e.getName() yerine Employee::getName hem daha kısa hem daha okunabilir. Ama karmaşık lambda'larda zorla method reference kullanmaya çalışma — okunabilirlik her zaman önce gelir.

3. İlkel (primitive) Stream'leri kullan. Stream<Integer> yerine IntStream, Stream<Double> yerine DoubleStream kullan. Boxing/unboxing maliyetinden kurtulursun. mapToInt, mapToDouble, mapToLong bu geçişi sağlar.

4. `peek()` sadece debug için. peek() yan etkiler için tasarlanmamıştır, pipeline'daki elemanları "gözetlemek" içindir. Debug amaçlı peek(System.out::println) koy, production'da kaldır.

5. Erken filtreleme yap. Veri setini mümkün olduğunca erken daralt. Önce filter, sonra map veya sorted çağır. 1 milyon elemandan 100'ünü filtreleyip sonra sıralamak, 1 milyon elemanı sıralayıp sonra filtrelemekten çok daha hızlı.

6. `toList()` kısayolunu kullan (Java 16+). collect(Collectors.toList()) yerine doğrudan stream().toList() yazabilirsin. Ama dikkat: toList() unmodifiable (değiştirilemez) liste döndürür, Collectors.toList() ise modifiable (değiştirilebilir).

7. `Optional` zincirini Stream ile harmanlayarak kullan.

// Tek bir akışta: filtrele → dönüştür → bulamazsan varsayılan değer
String topEmployee = employees.stream()
    .filter(e -> e.department().equals("Engineering"))
    .max(Comparator.comparingDouble(Employee::salary))
    .map(Employee::name)
    .orElse("Mühendis bulunamadı");

Gerçek Dünya Senaryosu: Log Analiz Sistemi

Birden fazla kavramı birleştiren bütünleşik bir örnek yapalım. Bir uygulamanın log kayıtlarını analiz ediyoruz:

import java.util.*;
import java.util.stream.*;
import java.time.*;

class Main {
    enum LogLevel { INFO, WARN, ERROR, FATAL }

    record LogEntry(LocalDateTime timestamp, LogLevel level, String service, String message) {}

    public static void main(String[] args) {
        // Simüle edilmiş log verileri
        List<LogEntry> logs = List.of(
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 0), LogLevel.INFO, "auth-service", "User login successful"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 5), LogLevel.ERROR, "payment-service", "Payment timeout"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 10), LogLevel.WARN, "auth-service", "Rate limit approaching"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 15), LogLevel.ERROR, "payment-service", "Database connection failed"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 20), LogLevel.FATAL, "payment-service", "Service crashed"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 25), LogLevel.INFO, "order-service", "Order processed"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 30), LogLevel.ERROR, "auth-service", "Token validation failed"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 35), LogLevel.WARN, "order-service", "Slow query detected"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 40), LogLevel.ERROR, "order-service", "Inventory check failed"),
            new LogEntry(LocalDateTime.of(2026, 2, 28, 8, 45), LogLevel.INFO, "auth-service", "User login successful")
        );

        // 1. Kritik loglar (ERROR + FATAL), servise göre grupla
        System.out.println("=== Kritik Log Raporu ===");
        Map<String, List<LogEntry>> criticalByService = logs.stream()
            .filter(log -> log.level() == LogLevel.ERROR || log.level() == LogLevel.FATAL)
            .collect(Collectors.groupingBy(LogEntry::service));

        criticalByService.forEach((service, entries) -> {
            System.out.println("\n" + service + " (" + entries.size() + " kritik log):");
            entries.forEach(e -> System.out.println("  [" + e.level() + "] " + e.message()));
        });

        // 2. Her servisin sağlık skoru (ERROR/FATAL oranı)
        System.out.println("\n=== Servis Sağlık Skorları ===");
        Map<String, Long> totalByService = logs.stream()
            .collect(Collectors.groupingBy(LogEntry::service, Collectors.counting()));

        Map<String, Long> errorsByService = logs.stream()
            .filter(log -> log.level() == LogLevel.ERROR || log.level() == LogLevel.FATAL)
            .collect(Collectors.groupingBy(LogEntry::service, Collectors.counting()));

        totalByService.forEach((service, total) -> {
            long errors = errorsByService.getOrDefault(service, 0L);
            double healthScore = 100.0 * (1.0 - (double) errors / total);
            System.out.printf("  %s: %.1f%% sağlıklı (%d/%d hatasız)%n",
                service, healthScore, total - errors, total);
        });

        // 3. Log seviyesi dağılımı
        System.out.println("\n=== Log Seviyesi Dağılımı ===");
        logs.stream()
            .collect(Collectors.groupingBy(LogEntry::level, Collectors.counting()))
            .entrySet().stream()
            .sorted(Map.Entry.<LogLevel, Long>comparingByValue().reversed())
            .forEach(e -> System.out.println("  " + e.getKey() + ": " + e.getValue()));

        // 4. En sorunlu servis
        logs.stream()
            .filter(log -> log.level() == LogLevel.ERROR || log.level() == LogLevel.FATAL)
            .collect(Collectors.groupingBy(LogEntry::service, Collectors.counting()))
            .entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .ifPresent(e -> System.out.println("\n⚠ En sorunlu servis: " + e.getKey()
                + " (" + e.getValue() + " hata)"));
    }
}

Bu örnekte filter, groupingBy, counting, sorted, max, forEach ve ifPresent hep birlikte çalışıyor. Gerçek bir log analiz senaryosunda bu pipeline'lar çok daha büyük veri setleri üzerinde çalışır ve parallel stream'le birlikte kullanılabilir.

Stream API ile Geleneksel Döngü: Hangisi Ne Zaman?

Stream her zaman doğru cevap değildir. İşte karar rehberin:

Stream tercih et:

  • Zincirleme dönüşümler (filtrele → dönüştür → topla)

  • Gruplama, istatistik, birleştirme işlemleri

  • Parallel işlem potansiyeli olan büyük veri setleri

  • Okunabilirliğin daha yüksek olduğu durumlarda

For döngüsü tercih et:

  • Döngü içinde exception fırlatman gerekiyorsa (checked exceptions Stream lambda'larında zahmetli)

  • İndeks bazlı erişim gerekiyorsa (list.get(i) ve list.get(i+1) karşılaştırması gibi)

  • Döngüden erken çıkış gerekiyorsa (break — Stream'de doğrudan karşılığı yok, takeWhile kısıtlı)

  • Mutable state yönetimi kaçınılmazsa

Sonuç

Java Stream API, koleksiyonlar üzerinde çalışma şeklini kökten değiştiren bir araç. Bu yazıda öğrendiklerini özetleyelim:

  • Stream bir veri yapısı değil, bir işlem boru hattıdır. Veri saklamaz, dönüştürür ve iletir. Tek kullanımlıktır — bir kez tüketildi mi tekrar kullanılamaz.

  • Ara işlemler tembel (lazy), terminal işlemler tetikleyicidir. filter ve map çağırdığında hiçbir şey olmaz; collect, forEach veya reduce çağırdığında tüm pipeline çalışır.

  • Collectors inanılmaz güçlü. groupingBy, partitioningBy, joining, summingDouble — SQL benzeri işlemleri Java'da doğrudan yapabilirsin.

  • flatMap iç içe yapıları düzleştirir. Liste içinde liste, one-to-many ilişkiler varsa flatMap kullan.

  • Parallel stream dikkatli kullan. Büyük veri + CPU yoğun iş = iyi. Küçük veri veya I/O yoğun iş = kötü. Her zaman ölç, varsayma.

  • Side effect'lerden kaçın, `collect` kullan. Stream içinde dış durumu değiştirmek bug kaynağı. Sonuçları her zaman terminal işlemle topla.

Stream API'yi öğrenmek bir yatırım. İlk başta for döngüsünden daha karmaşık gelebilir ama bir kez alıştığında, kodunun hem daha kısa hem daha okunabilir hem de daha güvenli olduğunu göreceksin. Önemli olan pratik — her gün bir koleksiyon işlemini Stream ile yazmayı dene, iki hafta içinde doğal gelecek.

Paylaş:
Son güncelleme: Jun 04, 2026

Yorumlar

Giriş yapın ve yorum bırakın.

Henüz yorum yok

Düşüncelerinizi paylaşan ilk siz olun!

Bu yazıyı beğendiniz mi?

Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.

İlgili Yazılar