← Kursa Dön
📄 Text · 15 min

Lambda İfadeleri ve Fonksiyonel Arayüzler

Java 8 ile gelen lambda'lar, Java'nın yüzünü değiştirdi. Eskiden 10 satırda yazdığın şeyi 1 satırda yazabilirsin. Ama sadece kısa yazmak değil mesele — düşünme biçimini değiştiriyor. Fonksiyonel programlama dünyasına açılan kapı burası.


Lambda Nedir?

Bir düşün: Bir arkadaşına "şu işi yap" diyeceksin. Eskiden ona 3 sayfalık talimat yazıyordun (anonymous inner class). Lambda ile sadece "şunu yap" diyorsun — tek satır.

Lambda = isimsiz fonksiyon. Bir metodu, bir değişken gibi taşıyabilir, parametre olarak geçirebilir, return edebilirsin.

Eskiden bir butona tıklama işlemi şöyle yazılırdı:

// Java 8 öncesi — anonymous inner class
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Tıklandı!");
    }
});

Lambda ile aynı şey:

// Java 8+ — lambda
button.addActionListener(e -> System.out.println("Tıklandı!"));

7 satır → 1 satır. Aynı iş. Daha okunabilir.


Lambda Syntax

Lambda'nın genel syntax'ı:

(parametreler) -> { gövde }

Ama Java çok esnek, birçok kısaltma var:

// Tam hali
(int a, int b) -> { return a + b; }

// Tip çıkarımı — derleyici tipleri bilir
(a, b) -> { return a + b; }

// Tek expression — return ve süslü parantez gereksiz
(a, b) -> a + b

// Tek parametre — parantez gereksiz
x -> x * 2

// Parametresiz
() -> System.out.println("Merhaba")

// Çok satırlı gövde — süslü parantez gerekir
(a, b) -> {
    int toplam = a + b;
    System.out.println("Toplam: " + toplam);
    return toplam;
}

💡 Kısa kural: Tek expression ise {} ve return yazma. Tek parametre ise () yazma. Birden fazla satır varsa {} zorunlu.


Fonksiyonel Arayüz (Functional Interface)

Lambda'nın çalışabilmesi için bir fonksiyonel arayüz gerekir. Bu, tek bir abstract metodu olan interface demek.

@FunctionalInterface
public interface Hesaplayici {
    int hesapla(int a, int b);
}

@FunctionalInterface anotasyonu zorunlu değil ama koyman iyi — derleyici kontrol eder, yanlışlıkla ikinci abstract metod eklersen hata verir.

Hesaplayici toplama = (a, b) -> a + b;
Hesaplayici cikarma = (a, b) -> a - b;
Hesaplayici carpma = (a, b) -> a * b;

System.out.println(toplama.hesapla(5, 3)); // 8
System.out.println(cikarma.hesapla(5, 3)); // 2
System.out.println(carpma.hesapla(5, 3));  // 15

Lambda, fonksiyonel arayüzün abstract metodunun implementasyonunu sağlar. (a, b) -> a + b aslında hesapla metodunun gövdesidir.

Fonksiyonel Arayüz Kuralları

@FunctionalInterface
public interface Gecerli {
    void yap();                          // 1 abstract metod ✅
    
    default void varsayilan() { }        // Default metod sayılmaz
    static void statik() { }             // Static metod sayılmaz
    String toString();                   // Object'ten gelen sayılmaz
}

// @FunctionalInterface
public interface Gecersiz {
    void yap();
    void yapBaska(); // 2 abstract metod — fonksiyonel değil ❌
}

Java'nın Hazır Fonksiyonel Arayüzleri

java.util.function paketi, en sık kullanılan 4 arayüzü sunar. Kendi interface'ini yazmadan önce bunlara bak:

1. Predicate\<T\> — Test Et, true/false Döndür

// boolean test(T t)
Predicate<String> uzunMu = s -> s.length() > 5;
Predicate<Integer> pozitifMi = n -> n > 0;

System.out.println(uzunMu.test("Java"));      // false
System.out.println(uzunMu.test("JavaScript")); // true
System.out.println(pozitifMi.test(-3));        // false

Predicate'ler birleştirilebilir:

Predicate<Integer> pozitif = n -> n > 0;
Predicate<Integer> cift = n -> n % 2 == 0;

Predicate<Integer> pozitifVeCift = pozitif.and(cift);
Predicate<Integer> pozitifVeyaCift = pozitif.or(cift);
Predicate<Integer> negatif = pozitif.negate();

System.out.println(pozitifVeCift.test(4));  // true
System.out.println(pozitifVeCift.test(3));  // false
System.out.println(negatif.test(-5));       // true

2. Function\<T, R\> — Dönüştür

// R apply(T t)
Function<String, Integer> uzunluk = s -> s.length();
Function<Integer, String> sayidanYazi = n -> "Sayı: " + n;

System.out.println(uzunluk.apply("Java"));     // 4
System.out.println(sayidanYazi.apply(42));      // Sayı: 42

Function'lar zincirlenebilir:

Function<String, String> buyukHarf = s -> s.toUpperCase();
Function<String, String> unlemEkle = s -> s + "!";

Function<String, String> bagir = buyukHarf.andThen(unlemEkle);
System.out.println(bagir.apply("merhaba")); // MERHABA!

Function<String, String> terstenBagir = buyukHarf.compose(unlemEkle);
System.out.println(terstenBagir.apply("merhaba")); // MERHABA!! (önce ünlem, sonra büyük harf)

andThen: Önce bu, sonra o. compose: Önce o, sonra bu.

3. Consumer\<T\> — Tüket, Bir Şey Döndürme

// void accept(T t)
Consumer<String> yazdir = s -> System.out.println(s);
Consumer<String> buyukYazdir = s -> System.out.println(s.toUpperCase());

yazdir.accept("merhaba");      // merhaba
buyukYazdir.accept("merhaba"); // MERHABA

Consumer da zincirlenebilir:

Consumer<String> logla = s -> System.out.println("[LOG] " + s);
Consumer<String> kaydet = s -> System.out.println("[SAVE] " + s);

Consumer<String> loglaVeKaydet = logla.andThen(kaydet);
loglaVeKaydet.accept("veri");
// [LOG] veri
// [SAVE] veri

4. Supplier\<T\> — Üret, Parametre Alma

// T get()
Supplier<String> selamVer = () -> "Merhaba Dünya!";
Supplier<Double> rastgele = () -> Math.random();
Supplier<List<String>> bosliste = () -> new ArrayList<>();

System.out.println(selamVer.get());  // Merhaba Dünya!
System.out.println(rastgele.get());  // 0.7234... (rastgele)

Supplier genellikle lazy initialization (tembel başlatma) için kullanılır:

// Nesne sadece ihtiyaç olunca oluşturulur
public static <T> T getOrDefault(T value, Supplier<T> defaultSupplier) {
    return value != null ? value : defaultSupplier.get();
}

String isim = null;
String sonuc = getOrDefault(isim, () -> "Bilinmeyen");
System.out.println(sonuc); // Bilinmeyen

Özet Tablo

ArayüzMetodParametreDönüş
Predicate<T>test(T)Tboolean
Function<T,R>apply(T)TR
Consumer<T>accept(T)Tvoid
Supplier<T>get()T

İki Parametreli Versiyonlar

// BiPredicate — iki parametre, boolean dönüş
BiPredicate<String, Integer> uzunlukKontrol = 
    (s, n) -> s.length() > n;

// BiFunction — iki parametre, bir dönüş
BiFunction<String, String, String> birlestir = 
    (a, b) -> a + " " + b;

// BiConsumer — iki parametre, void
BiConsumer<String, Integer> yazdir = 
    (isim, yas) -> System.out.println(isim + ": " + yas);

System.out.println(uzunlukKontrol.test("Java", 3));   // true
System.out.println(birlestir.apply("Merhaba", "Java")); // Merhaba Java
yazdir.accept("Ali", 25); // Ali: 25

Method Reference (::)

Lambda'nın daha da kısa hali. Eğer lambda sadece mevcut bir metodu çağırıyorsa, method reference kullanabilirsin.

4 Tür Method Reference

1. Static method reference — `Sınıf::staticMetod`

// Lambda
Function<String, Integer> parse = s -> Integer.parseInt(s);

// Method reference
Function<String, Integer> parse = Integer::parseInt;

2. Instance method reference (belirli nesne) — `nesne::metod`

String mesaj = "Merhaba Java";

// Lambda
Supplier<String> buyuk = () -> mesaj.toUpperCase();

// Method reference
Supplier<String> buyuk = mesaj::toUpperCase;

3. Instance method reference (rastgele nesne) — `Sınıf::metod`

// Lambda — s parametresi üzerinde metod çağrılıyor
Function<String, String> buyuk = s -> s.toUpperCase();

// Method reference
Function<String, String> buyuk = String::toUpperCase;

4. Constructor reference — `Sınıf::new`

// Lambda
Supplier<ArrayList<String>> listeOlustur = () -> new ArrayList<>();

// Method reference
Supplier<ArrayList<String>> listeOlustur = ArrayList::new;

// Parametreli constructor
Function<String, StringBuilder> sbOlustur = StringBuilder::new;

Method Reference Örnekleri

List<String> isimler = List.of("ali", "veli", "ayse");

// Lambda
isimler.forEach(s -> System.out.println(s));

// Method reference — daha temiz
isimler.forEach(System.out::println);
List<String> sayilar = List.of("3", "1", "4", "1", "5");

// Lambda
List<Integer> intList = sayilar.stream()
    .map(s -> Integer.parseInt(s))
    .toList();

// Method reference
List<Integer> intList = sayilar.stream()
    .map(Integer::parseInt)
    .toList();

⚠️ Method reference her zaman kullanılmaz. Eğer lambda içinde ek işlem yapıyorsan (örn. s -> s.trim().toUpperCase()), method reference kullanılamaz. Sadece doğrudan tek metod çağrısı yapıyorsan kullan.


Lambda ve Değişken Yakalama (Closure)

Lambda, dışarıdaki değişkenlere erişebilir. Ama bir kural var: effectively final olmalı.

String prefix = "Merhaba"; // effectively final — değeri değişmiyor

Consumer<String> selamla = isim -> System.out.println(prefix + " " + isim);
selamla.accept("Ali"); // Merhaba Ali
int sayac = 0;
// DERLEME HATASI!
Consumer<String> say = s -> sayac++; // sayac değişiyor — effectively final değil

Effectively final: Değişkene atandıktan sonra değeri hiç değişmiyorsa, final yazmasan bile effectively final'dır.

// DOĞRU — effectively final
String sehir = "İstanbul";
Supplier<String> al = () -> sehir;

// YANLIŞ — değer değişiyor, effectively final değil
String ulke = "Türkiye";
ulke = "Turkey"; // Bu satır yüzünden lambda'da kullanılamaz
// Supplier<String> al = () -> ulke; // HATA!

Neden bu kural var? Lambda farklı bir thread'de çalışabilir. Değişen bir değişkene iki thread'den erişmek race condition yaratır. Java bunu baştan engelliyor.

Workaround — Dizi veya AtomicInteger

// Dizi ile (referans final, içerik değişebilir)
int[] sayac = {0};
Consumer<String> say = s -> sayac[0]++;

// AtomicInteger ile (daha temiz)
AtomicInteger atomicSayac = new AtomicInteger(0);
Consumer<String> say2 = s -> atomicSayac.incrementAndGet();

Lambda Kullanım Alanları

Koleksiyonları Sıralama

List<String> isimler = new ArrayList<>(List.of("Zeynep", "Ali", "Burak"));

// Eskiden
Collections.sort(isimler, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

// Lambda ile
isimler.sort((a, b) -> a.compareTo(b));

// Method reference ile
isimler.sort(String::compareTo);

// Comparator helper'ları ile
isimler.sort(Comparator.naturalOrder());
isimler.sort(Comparator.reverseOrder());

Nesneleri Özel Sıralama

List<Ogrenci> ogrenciler = List.of(
    new Ogrenci("Ali", 85),
    new Ogrenci("Zeynep", 92),
    new Ogrenci("Burak", 78)
);

// Nota göre sırala
ogrenciler.sort(Comparator.comparingInt(Ogrenci::getNot));

// Nota göre ters sırala
ogrenciler.sort(Comparator.comparingInt(Ogrenci::getNot).reversed());

// Önce nota, sonra isme göre
ogrenciler.sort(
    Comparator.comparingInt(Ogrenci::getNot)
              .thenComparing(Ogrenci::getIsim)
);

Koleksiyonlarda Filtreleme ve İterasyon

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

// removeIf — Predicate alır
sayilar.removeIf(n -> n % 2 == 0); // Çiftleri kaldır
System.out.println(sayilar); // [1, 3, 5]

// forEach — Consumer alır
sayilar.forEach(n -> System.out.println("Sayı: " + n));

// replaceAll — UnaryOperator alır
sayilar.replaceAll(n -> n * 10);
System.out.println(sayilar); // [10, 30, 50]

Map İşlemleri

Map<String, Integer> puanlar = new HashMap<>();
puanlar.put("Ali", 85);
puanlar.put("Veli", 90);

// forEach
puanlar.forEach((isim, puan) -> 
    System.out.println(isim + ": " + puan));

// computeIfAbsent — yoksa hesapla ve ekle
puanlar.computeIfAbsent("Ayşe", isim -> 75);

// merge — birleştir
puanlar.merge("Ali", 5, (eski, ek) -> eski + ek);
System.out.println(puanlar.get("Ali")); // 90

// replaceAll
puanlar.replaceAll((isim, puan) -> puan + 10);

Kendi Fonksiyonel Arayüzün

Hazır arayüzler yetmezse kendi yaz:

@FunctionalInterface
public interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);
}
TriFunction<Integer, Integer, Integer, Integer> topla3 = 
    (a, b, c) -> a + b + c;

System.out.println(topla3.apply(1, 2, 3)); // 6
@FunctionalInterface
public interface Validator<T> {
    boolean validate(T t);
    
    // Default metod — validator'ları birleştir
    default Validator<T> and(Validator<T> other) {
        return t -> this.validate(t) && other.validate(t);
    }
}
Validator<String> bosDegilMi = s -> !s.isEmpty();
Validator<String> uzunMu = s -> s.length() >= 3;

Validator<String> gecerliMi = bosDegilMi.and(uzunMu);

System.out.println(gecerliMi.validate(""));     // false
System.out.println(gecerliMi.validate("ab"));   // false
System.out.println(gecerliMi.validate("abc"));  // true

Lambda vs Anonymous Inner Class

ÖzellikLambdaAnonymous Inner Class
SyntaxKısaUzun
this referansıDış sınıfı gösterirKendi instance'ını gösterir
Fonksiyonel arayüzZorunlu (tek abstract metod)Herhangi bir interface/class
PerformansDaha iyi (invokedynamic)Yeni class dosyası üretir
public class Ornek {
    String mesaj = "Dış sınıf";
    
    void test() {
        // Anonymous inner class — this = inner class
        Runnable r1 = new Runnable() {
            String mesaj = "İç sınıf";
            public void run() {
                System.out.println(this.mesaj); // "İç sınıf"
            }
        };
        
        // Lambda — this = dış sınıf (Ornek)
        Runnable r2 = () -> {
            System.out.println(this.mesaj); // "Dış sınıf"
        };
    }
}

💡 Lambda'da `this`, her zaman dış sınıfı (enclosing class) gösterir. Anonymous inner class'ta ise kendi instance'ını gösterir. Bu önemli bir fark.


Özet

  • Lambda = isimsiz fonksiyon, (parametreler) -> gövde syntax'ı ile yazılır

  • Fonksiyonel arayüz = tek abstract metodu olan interface — lambda'nın hedefi

  • Predicate (test), Function (dönüştür), Consumer (tüket), Supplier (üret) — 4 temel fonksiyonel arayüz

  • Method reference (::) = lambda'nın kısa hali, sadece mevcut metod çağrısı yapıyorsan kullan

  • Effectively final kuralı: lambda dış değişkene erişebilir ama değiştiremez

  • Lambda, Stream API ile birleştiğinde gerçek gücünü gösterir — bir sonraki ders!