İçeriğe geç

Java'da Stream API: Pratik Kullanım Rehberi

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

Stream API Nedir ve Neden Kullanmalısınız?

Java 8 ile tanıtılan Stream API, koleksiyonlar üzerinde fonksiyonel programlama tarzında işlemler yapmanızı sağlar. Bir Stream, veri kaynağından (collection, array, I/O channel) gelen elemanlar üzerinde pipeline tarzında işlemler (filter, map, reduce, collect) uygulamanıza olanak tanır.

Geleneksel for döngüleri ile yapılan işlemleri düşünün: döngü değişkeni tanımla, koşul yaz, sayacı artır, geçici listeler oluştur. Stream API ile aynı işlemi çok daha okunabilir ve deklaratif şekilde yapabilirsiniz.

// Geleneksel yaklaşım
List<String> activeUserNames = new ArrayList<>();
for (User user : users) {
    if (user.isActive()) {
        activeUserNames.add(user.getName().toUpperCase());
    }
}
Collections.sort(activeUserNames);

// Stream API ile
List<String> activeUserNames = users.stream()
    .filter(User::isActive)
    .map(User::getName)
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());

İkinci yaklaşım ne yaptığını ilk bakışta anlatıyor: aktif kullanıcıları filtrele, isimlerini al, büyük harfe çevir, sırala, listeye topla.

Temel Stream Operasyonları

Stream operasyonları iki kategoriye ayrılır:

  • Intermediate (Ara) operasyonlar: filter, map, flatMap, sorted, distinct, peek — Bir stream dönerler, lazy (tembel) çalışırlar

  • Terminal (Son) operasyonlar: collect, forEach, reduce, count, findFirst, anyMatch — Sonuç üretir, stream'i tüketirler

Önemli: Bir Stream yalnızca bir kez tüketilebilir. Terminal operasyondan sonra aynı stream'i tekrar kullanamazsınız. Yeni bir stream oluşturmanız gerekir.

filter — Elemanları Filtreleme

filter, bir Predicate alır ve koşulu sağlayan elemanları geçirir:

List<Product> expensiveProducts = products.stream()
    .filter(p -> p.getPrice() > 1000)
    .filter(p -> p.getCategory().equals("Electronics"))
    .collect(Collectors.toList());

// Method reference ile
List<Product> inStockProducts = products.stream()
    .filter(Product::isInStock)
    .collect(Collectors.toList());

map — Dönüştürme

map, her elemanı bir fonksiyonla dönüştürür:

// Kullanıcı isimlerini al
List<String> names = users.stream()
    .map(User::getName)
    .collect(Collectors.toList());

// Fiyatlara %18 KDV ekle
List<Double> pricesWithVat = products.stream()
    .map(Product::getPrice)
    .map(price -> price * 1.18)
    .collect(Collectors.toList());

// DTO dönüşümü — çok yaygın kullanım
List<UserDto> userDtos = users.stream()
    .map(user -> new UserDto(user.getId(), user.getName(), user.getEmail()))
    .collect(Collectors.toList());

reduce — İndirgeme

reduce, stream elemanlarını tek bir değere indirger:

// Toplam fiyat hesapla
double totalPrice = products.stream()
    .map(Product::getPrice)
    .reduce(0.0, Double::sum);

// En uzun ismi bul
Optional<String> longestName = names.stream()
    .reduce((a, b) -> a.length() >= b.length() ? a : b);

// Tüm isimleri birleştir
String allNames = names.stream()
    .reduce("", (a, b) -> a.isEmpty() ? b : a + ", " + b);

reduce kullanırken ilk parametre (identity) önemlidir. Toplama için 0, çarpma için 1, string birleştirme için "" kullanın.

collect ve Collectors

collect, stream sonuçlarını bir veri yapısına toplar. Collectors utility sınıfı birçok hazır collector sunar:

// List'e topla
List<String> list = stream.collect(Collectors.toList());

// Set'e topla (unique elemanlar)
Set<String> set = stream.collect(Collectors.toSet());

// Map'e dönüştür
Map<Long, String> userMap = users.stream()
    .collect(Collectors.toMap(User::getId, User::getName));

// String birleştirme
String csv = names.stream()
    .collect(Collectors.joining(", "));

// Ortalama hesapla
Double avgPrice = products.stream()
    .collect(Collectors.averagingDouble(Product::getPrice));

// İstatistik özeti
DoubleSummaryStatistics stats = products.stream()
    .collect(Collectors.summarizingDouble(Product::getPrice));
// stats.getAverage(), stats.getMax(), stats.getMin(), stats.getSum(), stats.getCount()

flatMap — İç İçe Yapıları Düzleştirme

flatMap, her elemanı bir stream'e dönüştürüp sonuçları tek bir stream'de birleştirir. Nested koleksiyonlarda çok kullanışlıdır:

// Her siparişin ürünlerini tek listede topla
List<Product> allProducts = orders.stream()
    .flatMap(order -> order.getProducts().stream())
    .collect(Collectors.toList());

// Cümleleri kelimelere ayır
List<String> words = sentences.stream()
    .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
    .distinct()
    .collect(Collectors.toList());

// Optional ile flatMap
Optional<String> city = user
    .flatMap(User::getAddress)
    .flatMap(Address::getCity)
    .map(String::toUpperCase);

groupingBy ve partitioningBy

groupingBy, elemanları bir key fonksiyonuna göre gruplar. SQL'deki GROUP BY ifadesinin Java karşılığıdır:

// Kategoriye göre grupla
Map<String, List<Product>> byCategory = products.stream()
    .collect(Collectors.groupingBy(Product::getCategory));

// Kategoriye göre grupla ve sayısını hesapla
Map<String, Long> countByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.counting()
    ));

// Kategoriye göre grupla ve ortalama fiyatı hesapla
Map<String, Double> avgPriceByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.averagingDouble(Product::getPrice)
    ));

// Çok seviyeli gruplama: kategori -> marka -> ürünler
Map<String, Map<String, List<Product>>> nested = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.groupingBy(Product::getBrand)
    ));

partitioningBy, elemanları bir predicate'e göre iki gruba ayırır (true/false):

Map<Boolean, List<Product>> partition = products.stream()
    .collect(Collectors.partitioningBy(p -> p.getPrice() > 500));

List<Product> expensive = partition.get(true);
List<Product> affordable = partition.get(false);

sorted, distinct, limit ve skip

// Fiyata göre sırala
List<Product> sorted = products.stream()
    .sorted(Comparator.comparing(Product::getPrice))
    .collect(Collectors.toList());

// Fiyata göre azalan sırala
List<Product> sortedDesc = products.stream()
    .sorted(Comparator.comparing(Product::getPrice).reversed())
    .collect(Collectors.toList());

// Çoklu sıralama: önce kategori, sonra fiyat
List<Product> multiSorted = products.stream()
    .sorted(Comparator.comparing(Product::getCategory)
        .thenComparing(Product::getPrice))
    .collect(Collectors.toList());

// İlk 10 pahalı ürün
List<Product> top10 = products.stream()
    .sorted(Comparator.comparing(Product::getPrice).reversed())
    .limit(10)
    .collect(Collectors.toList());

// Pagination: sayfa 3, sayfa boyutu 20
List<Product> page3 = products.stream()
    .skip(40)  // (3 - 1) * 20
    .limit(20)
    .collect(Collectors.toList());

Parallel Streams

parallelStream() veya stream().parallel() ile stream işlemlerini birden fazla thread'de çalıştırabilirsiniz:

// Parallel stream
long count = products.parallelStream()
    .filter(p -> p.getPrice() > 100)
    .count();

// Büyük veri setlerinde faydalı
List<ProcessedData> results = largeDataSet.parallelStream()
    .map(this::heavyComputation)
    .collect(Collectors.toList());

Dikkat edilmesi gerekenler:

  • Parallel stream, ForkJoinPool.commonPool() kullanır (varsayılan thread sayısı: CPU core - 1)

  • Küçük koleksiyonlarda (< 10.000 eleman) parallel stream daha yavaş olabilir — thread oluşturma maliyeti

  • Stateful operasyonlardan kaçının (shared mutable state)

  • Sıralama (order) önemliyse forEachOrdered() kullanın

  • I/O bound işlemlerde parallel stream yerine CompletableFuture tercih edin

// YANLIS: Shared mutable state
List<String> results = new ArrayList<>();  // Thread-safe degil!
stream.parallel().forEach(item -> results.add(process(item)));

// DOGRU: Collect kullanın
List<String> results = stream.parallel()
    .map(this::process)
    .collect(Collectors.toList());

Performans İpuçları

  1. Stream'i tekrar kullanmayın. Her kullanımda yeni stream oluşturun.

  2. Gereksiz boxing'den kaçının. IntStream, LongStream, DoubleStream kullanın:

// Yavaş: Integer boxing/unboxing
int sum = numbers.stream()
    .map(n -> n * 2)
    .reduce(0, Integer::sum);

// Hızlı: Primitive stream
int sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .map(n -> n * 2)
    .sum();
  1. Kısa devre (short-circuit) operasyonları kullanın: findFirst(), anyMatch(), limit() stream'i erken sonlandırır.

  2. filter'ı map'ten önce koyun. Önce filtreleme yaparak dönüştürülecek eleman sayısını azaltın.

  3. Büyük veri setleri için Stream.of() yerine Collection.stream() kullanın.

Gerçek Dünya Örneği: Sipariş Raporu

Birden fazla Stream operasyonunu birleştiren bir rapor oluşturalım:

public OrderReport generateReport(List<Order> orders) {
    // Son 30 günün siparişleri
    LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30);

    List<Order> recentOrders = orders.stream()
        .filter(o -> o.getOrderDate().isAfter(thirtyDaysAgo))
        .filter(o -> o.getStatus() != OrderStatus.CANCELLED)
        .collect(Collectors.toList());

    // Toplam ciro
    double totalRevenue = recentOrders.stream()
        .flatMap(o -> o.getItems().stream())
        .mapToDouble(item -> item.getPrice() * item.getQuantity())
        .sum();

    // Kategoriye göre ciro
    Map<String, Double> revenueByCategory = recentOrders.stream()
        .flatMap(o -> o.getItems().stream())
        .collect(Collectors.groupingBy(
            OrderItem::getCategory,
            Collectors.summingDouble(item -> item.getPrice() * item.getQuantity())
        ));

    // En çok satan 5 ürün
    List<String> topProducts = recentOrders.stream()
        .flatMap(o -> o.getItems().stream())
        .collect(Collectors.groupingBy(
            OrderItem::getProductName,
            Collectors.summingInt(OrderItem::getQuantity)
        ))
        .entrySet().stream()
        .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
        .limit(5)
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());

    return new OrderReport(totalRevenue, revenueByCategory, topProducts);
}

Özet

  • Stream API koleksiyonlar üzerinde fonksiyonel tarzda, okunabilir ve deklaratif işlemler sağlar

  • filter/map/reduce üçlüsü en temel operasyonlardır — çoğu işi bunlarla çözersiniz

  • flatMap iç içe yapıları düzleştirirken, groupingBy gruplama ve aggregation için kullanılır

  • Parallel streams büyük veri setlerinde faydalıdır ama shared mutable state'den kaçının

  • Primitive stream'ler (IntStream, LongStream) boxing maliyetini ortadan kaldırır

  • filter'ı map'ten önce koyarak performansı artırın

Paylaş:
Son güncelleme: Jul 19, 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