← Kursa Dön
📄 Text · 15 min

Stream API

Generics, wildcards ve lambda'yı gördük. Şimdi hepsini bir araya getiren, Java 8'in en güçlü özelliği: Stream API. Bir koleksiyondaki veriyi sorgulamak, dönüştürmek, filtrelemek — hepsi tek bir akıcı pipeline ile.


Stream Nedir?

Bir nehir düşün. Nehirden su akıyor. Sen nehrin kenarında oturuyorsun ve akan suya filtreler, dönüştürücüler koyuyorsun. Kirli su giriyor, temiz su çıkıyor. Ama nehrin kendisini değiştirmiyorsun — sadece akışı yönlendiriyorsun.

Stream tam olarak bu. Veri kaynağından (koleksiyon, dizi, dosya) akan verileri, zincirleme operasyonlarla işleyip sonuç üretiyorsun. Orijinal veri asla değişmez.

List<String> isimler = List.of("Ali", "Veli", "Ayşe", "Zeynep", "Burak");

List<String> sonuc = isimler.stream()         // Akış başla
    .filter(s -> s.length() > 3)              // 3 harften uzun olanlar
    .map(String::toUpperCase)                 // Büyük harfe çevir
    .sorted()                                 // Sırala
    .toList();                                // Listeye topla

System.out.println(sonuc); // [AYŞE, BURAK, ZEYNEP]
System.out.println(isimler); // [Ali, Veli, Ayşe, Zeynep, Burak] — değişmedi!

Stream Oluşturma

Stream'i farklı kaynaklardan oluşturabilirsin:

// 1. Koleksiyondan
List<String> liste = List.of("a", "b", "c");
Stream<String> s1 = liste.stream();

// 2. Diziden
String[] dizi = {"x", "y", "z"};
Stream<String> s2 = Arrays.stream(dizi);

// 3. Doğrudan değerlerden
Stream<String> s3 = Stream.of("merhaba", "dünya");

// 4. Sayı aralığından
IntStream s4 = IntStream.range(1, 10);      // 1-9
IntStream s5 = IntStream.rangeClosed(1, 10); // 1-10

// 5. Boş stream
Stream<String> s6 = Stream.empty();

// 6. Sonsuz stream (dikkat!)
Stream<Double> s7 = Stream.generate(Math::random); // Sonsuz rastgele sayı
Stream<Integer> s8 = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...

// 7. Sınırlı iterate (Java 9+)
Stream<Integer> s9 = Stream.iterate(0, n -> n < 100, n -> n + 2); // 0, 2, ..., 98

⚠️ Sonsuz stream'lere `limit()` eklemeyi unutma! Yoksa program sonsuza kadar çalışır.

List<Double> rastgele = Stream.generate(Math::random)
    .limit(5) // 5 tane al
    .toList();

Intermediate vs Terminal Operasyonlar

Stream operasyonları iki kategoriye ayrılır:

TürÖzellikÖrnekler
IntermediateStream döner, zincirlenirfilter, map, sorted, distinct, limit, skip
TerminalSonuç üretir, stream bitercollect, forEach, count, reduce, findFirst, anyMatch

Önemli kural: Terminal operasyon çağrılana kadar intermediate operasyonlar çalışmaz. Buna lazy evaluation denir.

// Bu satır HİÇBİR ŞEY yapmaz — terminal operasyon yok!
Stream<String> stream = isimler.stream()
    .filter(s -> {
        System.out.println("Filtre: " + s); // Bu print çalışmaz!
        return s.length() > 3;
    });

// Terminal operasyon eklenince çalışır
long sayi = stream.count(); // Şimdi filtre çalışır

💡 Lazy evaluation neden iyi? Gereksiz işlem yapmaz. 1 milyon elemanlı listede filter().findFirst() dersen, ilk eşleşmede durur — 1 milyon elemanı filtrelemez.


filter — Filtrele

Predicate alır, koşulu sağlayanları geçirir:

List<Integer> sayilar = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Çift sayılar
List<Integer> ciftler = sayilar.stream()
    .filter(n -> n % 2 == 0)
    .toList();
System.out.println(ciftler); // [2, 4, 6, 8, 10]

// Birden fazla koşul
List<Integer> buyukCiftler = sayilar.stream()
    .filter(n -> n % 2 == 0)
    .filter(n -> n > 5)
    .toList();
System.out.println(buyukCiftler); // [6, 8, 10]

map — Dönüştür

Function alır, her elemanı dönüştürür:

List<String> isimler = List.of("ali", "veli", "ayşe");

// Büyük harfe çevir
List<String> buyuk = isimler.stream()
    .map(String::toUpperCase)
    .toList();
System.out.println(buyuk); // [ALİ, VELİ, AYŞE]

// Uzunluklara çevir
List<Integer> uzunluklar = isimler.stream()
    .map(String::length)
    .toList();
System.out.println(uzunluklar); // [3, 4, 4]

mapToInt, mapToDouble, mapToLong

Primitive stream'lere dönüştürmek için (boxing maliyetinden kaçınır):

List<String> kelimeler = List.of("Java", "Python", "Go");

int toplamUzunluk = kelimeler.stream()
    .mapToInt(String::length)
    .sum(); // IntStream'in özel metodu
System.out.println(toplamUzunluk); // 12

OptionalDouble ortalama = kelimeler.stream()
    .mapToInt(String::length)
    .average();
System.out.println(ortalama.orElse(0)); // 4.0

flatMap — Düzleştir

İç içe koleksiyonları tek seviyeye indirir:

List<List<String>> icIce = List.of(
    List.of("Ali", "Veli"),
    List.of("Ayşe", "Fatma"),
    List.of("Burak")
);

// map kullanırsan: Stream<List<String>> — hâlâ iç içe
// flatMap kullanırsan: Stream<String> — düz

List<String> duz = icIce.stream()
    .flatMap(Collection::stream) // Her alt listeyi stream'e çevir ve düzleştir
    .toList();
System.out.println(duz); // [Ali, Veli, Ayşe, Fatma, Burak]

Gerçek hayat örneği — her öğrencinin aldığı dersleri düzleştirmek:

List<Ogrenci> ogrenciler = List.of(
    new Ogrenci("Ali", List.of("Java", "Python")),
    new Ogrenci("Veli", List.of("Java", "C++"))
);

List<String> tumDersler = ogrenciler.stream()
    .flatMap(o -> o.getDersler().stream())
    .distinct() // Tekrarları kaldır
    .toList();
System.out.println(tumDersler); // [Java, Python, C++]

sorted — Sırala

List<String> isimler = List.of("Zeynep", "Ali", "Burak", "Ayşe");

// Doğal sıralama
List<String> sirali = isimler.stream()
    .sorted()
    .toList();
System.out.println(sirali); // [Ali, Ayşe, Burak, Zeynep]

// Ters sıralama
List<String> ters = isimler.stream()
    .sorted(Comparator.reverseOrder())
    .toList();

// Uzunluğa göre sırala
List<String> uzunluga = isimler.stream()
    .sorted(Comparator.comparingInt(String::length))
    .toList();
System.out.println(uzunluga); // [Ali, Ayşe, Burak, Zeynep]

distinct, limit, skip

List<Integer> sayilar = List.of(1, 2, 2, 3, 3, 3, 4, 5);

// Tekrarları kaldır
List<Integer> benzersiz = sayilar.stream()
    .distinct()
    .toList();
System.out.println(benzersiz); // [1, 2, 3, 4, 5]

// İlk 3'ü al
List<Integer> ilkUc = sayilar.stream()
    .limit(3)
    .toList();
System.out.println(ilkUc); // [1, 2, 2]

// İlk 3'ü atla
List<Integer> kalanlar = sayilar.stream()
    .skip(3)
    .toList();
System.out.println(kalanlar); // [3, 3, 3, 4, 5]

// Sayfalama (skip + limit)
int sayfa = 2, sayfaBoyutu = 3;
List<Integer> sayfaVerisi = sayilar.stream()
    .skip((long) (sayfa - 1) * sayfaBoyutu)
    .limit(sayfaBoyutu)
    .toList();
System.out.println(sayfaVerisi); // [3, 3, 3]

reduce — İndirge

Tüm elemanları tek bir değere indirger:

List<Integer> sayilar = List.of(1, 2, 3, 4, 5);

// Toplam
int toplam = sayilar.stream()
    .reduce(0, (a, b) -> a + b);
System.out.println(toplam); // 15

// Method reference ile
int toplam2 = sayilar.stream()
    .reduce(0, Integer::sum);

// Çarpım
int carpim = sayilar.stream()
    .reduce(1, (a, b) -> a * b);
System.out.println(carpim); // 120

// Başlangıç değeri olmadan (Optional döner)
Optional<Integer> max = sayilar.stream()
    .reduce(Integer::max);
max.ifPresent(m -> System.out.println("Max: " + m)); // Max: 5

String Birleştirme

List<String> kelimeler = List.of("Java", "çok", "güzel");

String cumle = kelimeler.stream()
    .reduce("", (a, b) -> a + " " + b)
    .trim();
System.out.println(cumle); // Java çok güzel

// Daha iyi yol — String.join veya Collectors.joining
String cumle2 = String.join(" ", kelimeler);
String cumle3 = kelimeler.stream()
    .collect(Collectors.joining(" "));

collect — Topla

collect, stream'in sonuçlarını bir koleksiyona veya başka bir yapıya toplar. En sık kullandığın terminal operasyon.

Temel Collectors

List<String> isimler = List.of("Ali", "Veli", "Ayşe", "Ali", "Burak");

// Listeye topla
List<String> liste = isimler.stream()
    .filter(s -> s.length() > 3)
    .collect(Collectors.toList());

// Java 16+ kısa yol (unmodifiable list döner)
List<String> liste2 = isimler.stream()
    .filter(s -> s.length() > 3)
    .toList();

// Set'e topla (tekrarlar silinir)
Set<String> set = isimler.stream()
    .collect(Collectors.toSet());
System.out.println(set); // [Ali, Veli, Ayşe, Burak]

// String birleştir
String birlesik = isimler.stream()
    .collect(Collectors.joining(", "));
System.out.println(birlesik); // Ali, Veli, Ayşe, Ali, Burak

// Virgül + prefix/suffix
String formatli = isimler.stream()
    .collect(Collectors.joining(", ", "[", "]"));
System.out.println(formatli); // [Ali, Veli, Ayşe, Ali, Burak]

groupingBy — Grupla

SQL'deki GROUP BY gibi:

List<String> isimler = List.of("Ali", "Ayşe", "Burak", "Berk", "Can");

// İlk harfe göre grupla
Map<Character, List<String>> gruplar = isimler.stream()
    .collect(Collectors.groupingBy(s -> s.charAt(0)));
System.out.println(gruplar);
// {A=[Ali, Ayşe], B=[Burak, Berk], C=[Can]}
// Uzunluğa göre grupla ve say
Map<Integer, Long> uzunlukSayisi = isimler.stream()
    .collect(Collectors.groupingBy(
        String::length,
        Collectors.counting()
    ));
System.out.println(uzunlukSayisi); // {3=[Ali, Can], 4=[Ayşe, Berk], 5=[Burak]}

partitioningBy — İkiye Böl

Predicate'e göre true/false olarak ikiye ayırır:

List<Integer> sayilar = List.of(1, 2, 3, 4, 5, 6);

Map<Boolean, List<Integer>> bolunmus = sayilar.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println(bolunmus);
// {false=[1, 3, 5], true=[2, 4, 6]}

toMap — Map'e Topla

List<String> isimler = List.of("Ali", "Veli", "Ayşe");

Map<String, Integer> isimUzunluk = isimler.stream()
    .collect(Collectors.toMap(
        s -> s,            // key
        String::length     // value
    ));
System.out.println(isimUzunluk); // {Ali=3, Veli=4, Ayşe=4}

⚠️ `toMap` ile aynı key varsa `IllegalStateException` fırlatır! Merge fonksiyonu ekle:

List<String> kelimeler = List.of("ali", "ay", "araba", "bal", "bir");

Map<Character, String> ilkHarf = kelimeler.stream()
    .collect(Collectors.toMap(
        s -> s.charAt(0),
        s -> s,
        (var1, var2) -> var1 + ", " + var2 // Çakışmada birleştir
    ));
System.out.println(ilkHarf); // {a=ali, ay, araba, b=bal, bir}

Summarizing — İstatistik

List<Integer> notlar = List.of(85, 90, 78, 92, 88);

IntSummaryStatistics istat = notlar.stream()
    .collect(Collectors.summarizingInt(Integer::intValue));

System.out.println("Sayı: " + istat.getCount());     // 5
System.out.println("Toplam: " + istat.getSum());      // 433
System.out.println("Min: " + istat.getMin());         // 78
System.out.println("Max: " + istat.getMax());         // 92
System.out.println("Ortalama: " + istat.getAverage()); // 86.6

Terminal Operasyonlar — Diğerleri

forEach

List<String> isimler = List.of("Ali", "Veli", "Ayşe");
isimler.stream().forEach(System.out::println);

// Kısa yol — doğrudan List üzerinde
isimler.forEach(System.out::println);

count

long uzunIsimSayisi = isimler.stream()
    .filter(s -> s.length() > 3)
    .count();

findFirst, findAny

Optional<String> ilk = isimler.stream()
    .filter(s -> s.startsWith("A"))
    .findFirst();
ilk.ifPresent(System.out::println); // Ali

// findAny — parallel stream'de daha hızlı
Optional<String> herhangi = isimler.stream()
    .filter(s -> s.startsWith("A"))
    .findAny();

anyMatch, allMatch, noneMatch

List<Integer> sayilar = List.of(2, 4, 6, 8);

boolean hepsiCift = sayilar.stream().allMatch(n -> n % 2 == 0);   // true
boolean tekVarMi = sayilar.stream().anyMatch(n -> n % 2 != 0);    // false
boolean negatifYok = sayilar.stream().noneMatch(n -> n < 0);      // true

min, max

Optional<Integer> min = sayilar.stream().min(Comparator.naturalOrder());
Optional<Integer> max = sayilar.stream().max(Comparator.naturalOrder());

Parallel Stream

Stream operasyonlarını birden fazla thread'de çalıştırır:

List<Integer> buyukListe = IntStream.rangeClosed(1, 1_000_000)
    .boxed()
    .toList();

// Sequential
long baslangic = System.currentTimeMillis();
long toplam1 = buyukListe.stream()
    .mapToLong(Integer::longValue)
    .sum();
System.out.println("Sequential: " + (System.currentTimeMillis() - baslangic) + "ms");

// Parallel
baslangic = System.currentTimeMillis();
long toplam2 = buyukListe.parallelStream()
    .mapToLong(Integer::longValue)
    .sum();
System.out.println("Parallel: " + (System.currentTimeMillis() - baslangic) + "ms");

Parallel stream oluşturma yolları:

// Yol 1: parallelStream()
liste.parallelStream()...

// Yol 2: stream().parallel()
liste.stream().parallel()...

⚠️ Parallel stream her zaman daha hızlı DEĞİLDİR! Thread oluşturma ve koordinasyon maliyeti var. Küçük veri setlerinde sequential daha hızlı. Parallel stream şu durumlarda işe yarar: - Büyük veri (100.000+ eleman) - CPU-yoğun operasyonlar - Shared state yok (yan etkisiz operasyonlar)


Lazy Evaluation Detaylı

Stream'in en önemli optimizasyonlarından biri:

List<Integer> sayilar = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Optional<Integer> ilkBuyuk = sayilar.stream()
    .filter(n -> {
        System.out.println("Filter: " + n);
        return n > 5;
    })
    .map(n -> {
        System.out.println("Map: " + n);
        return n * 10;
    })
    .findFirst();

Çıktı:

Filter: 1
Filter: 2
Filter: 3
Filter: 4
Filter: 5
Filter: 6
Map: 6

6'yı bulunca durdu! 7, 8, 9, 10'a bakmadı bile. Bu short-circuit evaluation.

💡 Short-circuit operasyonlar: findFirst, findAny, anyMatch, allMatch, noneMatch, limit. Bu operasyonlar, sonucu bulmak için tüm stream'i işlemek zorunda değil.


Stream Tek Kullanımlık

Bir stream sadece bir kez kullanılabilir:

Stream<String> stream = isimler.stream();
stream.forEach(System.out::println); // ✅ İlk kullanım

// HATA! IllegalStateException: stream has already been operated upon
stream.forEach(System.out::println); // ❌ İkinci kullanım

Her seferinde yeni stream oluştur:

isimler.stream().forEach(System.out::println); // ✅
isimler.stream().count();                       // ✅ Yeni stream

Gerçek Hayat Örnekleri

E-ticaret Senaryosu

record Urun(String isim, String kategori, double fiyat, int stok) {}

List<Urun> urunler = List.of(
    new Urun("Laptop", "Elektronik", 15000, 10),
    new Urun("Telefon", "Elektronik", 8000, 25),
    new Urun("Kulaklık", "Elektronik", 500, 100),
    new Urun("Masa", "Mobilya", 3000, 15),
    new Urun("Sandalye", "Mobilya", 1500, 30)
);

// Kategori bazlı toplam stok
Map<String, Integer> stoklar = urunler.stream()
    .collect(Collectors.groupingBy(
        Urun::kategori,
        Collectors.summingInt(Urun::stok)
    ));
System.out.println(stoklar); // {Elektronik=135, Mobilya=45}

// En pahalı 3 ürün
List<String> pahalilar = urunler.stream()
    .sorted(Comparator.comparingDouble(Urun::fiyat).reversed())
    .limit(3)
    .map(Urun::isim)
    .toList();
System.out.println(pahalilar); // [Laptop, Telefon, Masa]

// Stokta olan elektronik ürünlerin ortalama fiyatı
double ortFiyat = urunler.stream()
    .filter(u -> u.kategori().equals("Elektronik"))
    .filter(u -> u.stok() > 0)
    .mapToDouble(Urun::fiyat)
    .average()
    .orElse(0);
System.out.println("Ort. fiyat: " + ortFiyat); // 7833.33

Kelime Frekansı

String metin = "java güzel java kolay java eğlenceli güzel kolay";

Map<String, Long> frekans = Arrays.stream(metin.split(" "))
    .collect(Collectors.groupingBy(
        Function.identity(),
        Collectors.counting()
    ));
System.out.println(frekans);
// {java=3, güzel=2, kolay=2, eğlenceli=1}

Optional ile Stream

Stream operasyonları sıklıkla Optional döner:

List<String> isimler = List.of("Ali", "Veli", "Ayşe");

// findFirst → Optional
isimler.stream()
    .filter(s -> s.startsWith("Z"))
    .findFirst()
    .ifPresentOrElse(
        s -> System.out.println("Buldum: " + s),
        () -> System.out.println("Bulamadım")
    );

// Optional'ı stream'e çevir (Java 9+)
Optional<String> opt = Optional.of("Merhaba");
List<String> liste = opt.stream().toList(); // [Merhaba]

Optional<String> bos = Optional.empty();
List<String> bosListe = bos.stream().toList(); // []

Stream vs For Loop — Ne Zaman Hangisi?

DurumTercih
Basit dönüştürme/filtrelemeStream
Karmaşık durum yönetimi (index, break, continue)For loop
Okunabilirlik önemliStream (genellikle)
Performans kritikFor loop (çok küçük fark)
Paralel işlem gerekliStream
// STREAM — temiz ve okunabilir
List<String> sonuc = isimler.stream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList();

// FOR LOOP — aynı iş, daha fazla kod
List<String> sonuc = new ArrayList<>();
for (String s : isimler) {
    if (s.length() > 3) {
        sonuc.add(s.toUpperCase());
    }
}
Collections.sort(sonuc);

Özet

  • Stream, koleksiyonları fonksiyonel tarzda işlemenin yolu — orijinal veriyi değiştirmez

  • Intermediate operasyonlar (filter, map, sorted) zincirlenir ve lazy çalışır

  • Terminal operasyonlar (collect, forEach, reduce) stream'i tetikler ve sonuç üretir

  • collect ile toList(), toSet(), groupingBy(), joining() gibi toplayıcılar kullanılır

  • Parallel stream büyük veri setlerinde performans kazandırır ama her zaman daha hızlı değildir

  • Stream tek kullanımlıktır — bir kez consume edildikten sonra tekrar kullanılamaz