← Kursa Dön
📄 Text · 12 min

Pattern Matching

Giriş

Java'da tip kontrolü ve casting her zaman zahmetli oldu. instanceof ile kontrol et, sonra cast et, sonra kullan — üç adım, hep aynı boilerplate. Java 16'dan itibaren gelen pattern matching özellikleri bu süreci dramatik şekilde kısaltıyor.

Bu ders Java'nın belki de en heyecan verici modern özelliğini kapsıyor. instanceof pattern matching ile başlayıp, switch pattern matching, guarded patterns ve record patterns'a kadar gideceğiz.

instanceof Pattern Matching (Java 16)

Analoji: Gümrük Kontrolü

Pattern matching'i gümrük kontrolü gibi düşün. Eski sistem: "Pasaportunuz var mı?" → "Evet" → "Görebilir miyim?" → Pasaportu al, kontrol et. Üç adım. Yeni sistem: "Pasaportunuzu gösterin" → Tek adımda hem var mı kontrol edilir, hem içeriği okunur. Pattern matching de tam böyle — tip kontrolü ve değişken atamasını tek adımda yapar.

Eski Yol vs Pattern Matching

// ESKİ YOL — kontrol et, cast et, kullan
public void bilgiYazdir(Object obj) {
    if (obj instanceof String) {
        String s = (String) obj;   // Cast gerekiyor
        System.out.println("String uzunluk: " + s.length());
    } else if (obj instanceof Integer) {
        Integer i = (Integer) obj; // Tekrar cast
        System.out.println("Sayı: " + i);
    }
}
// PATTERN MATCHING — tek adım
public void bilgiYazdir(Object obj) {
    if (obj instanceof String s) {      // Kontrol + atama tek satır
        System.out.println("String uzunluk: " + s.length());
    } else if (obj instanceof Integer i) {
        System.out.println("Sayı: " + i);
    }
}

obj instanceof String s ifadesi şunu söylüyor: "obj bir String ise, onu s değişkenine ata." Cast'e gerek yok.

Scope Kuralları

Pattern variable'ın scope'u, derleyicinin onun kesinlikle atanmış olduğunu garanti edebildiği yerdir:

public void ornek(Object obj) {
    // s sadece if bloğu içinde erişilebilir
    if (obj instanceof String s) {
        System.out.println(s.toUpperCase()); // ✅
    }
    // System.out.println(s); // ❌ Derleme hatası — s burada yok

    // Negation pattern — ! ile çevrildiğinde else'te erişilebilir
    if (!(obj instanceof String s)) {
        System.out.println("String değil");
        return;
    }
    // Buraya ulaşıldıysa obj kesinlikle String — s erişilebilir
    System.out.println(s.length()); // ✅
}

&& ile Birleştirme

// Pattern variable'ı aynı koşulda kullanabilirsin
if (obj instanceof String s && s.length() > 5) {
    System.out.println("Uzun string: " + s);
}

// AMA || ile kullanılamaz — mantıken doğru değil
// if (obj instanceof String s || s.length() > 5) { } // ❌ Derleme hatası

&& kullanabilirsin çünkü sol taraf true ise sağ taraf değerlendirilir ve s kesinlikle atanmıştır. || ile olmaz çünkü sol taraf false olabilir, bu durumda s atanmamış olur.

Switch Pattern Matching (Java 21)

Java 21 ile switch ifadesi ciddi bir güç kazandı. Artık switch'te sadece primitive ve enum değil, tip kontrolü de yapabiliyorsun.

Temel Kullanım

public String tipBilgisi(Object obj) {
    return switch (obj) {
        case Integer i  -> "Tam sayı: " + i;
        case Double d   -> "Ondalık: " + d;
        case String s   -> "Metin: " + s;
        case null       -> "null değer";
        default         -> "Bilinmeyen tip: " + obj.getClass().getName();
    };
}

Birkaç önemli nokta:

  • null kontrolü switch içinde yapılabilir — artık NullPointerException riski yok

  • default her zaman olmalı (exhaustive olmayan switch'lerde)

  • Her case bir pattern ve bir değişken tanımlar

null Handling

Eski switch'lerde null değer NullPointerException fırlatırdı. Yeni switch'te null'ı açıkça ele alabilirsin:

public void islemYap(String deger) {
    switch (deger) {
        case null -> System.out.println("Değer null!");
        case "admin" -> System.out.println("Yönetici");
        case String s when s.startsWith("user_") -> System.out.println("Kullanıcı: " + s);
        default -> System.out.println("Diğer: " + deger);
    }
}

💡 Not: case null ile case null, default farklı şeyler. İlki sadece null'ı yakalar, ikincisi hem null'ı hem hiçbir pattern'a uymayan değerleri yakalar.

Guarded Patterns (when)

Pattern matching'de ekstra koşul eklemek istediğinde when keyword'ünü kullanırsın:

public String degerlendir(Object obj) {
    return switch (obj) {
        case Integer i when i < 0    -> "Negatif: " + i;
        case Integer i when i == 0   -> "Sıfır";
        case Integer i when i > 100  -> "Büyük sayı: " + i;
        case Integer i               -> "Normal sayı: " + i;  // Geri kalan
        case String s when s.isBlank() -> "Boş metin";
        case String s                -> "Metin: " + s;
        default                      -> "Bilinmeyen";
    };
}

when guard'ı, case'in eşleşmesi için ek koşul ekler. Sıralama önemli — daha spesifik case'ler önce gelmeli.

// Pratik örnek: HTTP status code değerlendirme
public String httpStatus(Object response) {
    return switch (response) {
        case Integer code when code >= 200 && code < 300 -> "Başarılı";
        case Integer code when code >= 400 && code < 500 -> "İstemci hatası";
        case Integer code when code >= 500 -> "Sunucu hatası";
        case Integer code -> "Bilinmeyen status: " + code;
        case String msg -> "Mesaj: " + msg;
        default -> "Geçersiz response";
    };
}

⚠️ Dikkat: Guard'lı case'ler önce, guard'sız (genel) case sonra gelmeli. Aksi halde derleyici hata verir çünkü genel case tüm alt case'leri kapsar (dominance).

Record Patterns (Java 21)

Record pattern matching, bir record'u deconstruct etmeni sağlar — yani içindeki bileşenlere doğrudan erişirsin.

public record Nokta(int x, int y) { }

// Eski yol
public void yazdir(Object obj) {
    if (obj instanceof Nokta n) {
        int x = n.x();
        int y = n.y();
        System.out.println("Koordinat: " + x + ", " + y);
    }
}

// Record Pattern — doğrudan deconstruct
public void yazdir(Object obj) {
    if (obj instanceof Nokta(int x, int y)) {
        System.out.println("Koordinat: " + x + ", " + y);
    }
}

Nokta(int x, int y) ifadesi record'un component'lerini doğrudan değişkenlere açıyor. Accessor çağrısına bile gerek yok.

Switch ile Record Pattern

public sealed interface Sekil permits Daire, Dikdortgen, Ucgen { }
public record Daire(double yaricap) implements Sekil { }
public record Dikdortgen(double en, double boy) implements Sekil { }
public record Ucgen(double taban, double yukseklik) implements Sekil { }

public double alanHesapla(Sekil sekil) {
    return switch (sekil) {
        case Daire(double r)          -> Math.PI * r * r;
        case Dikdortgen(double e, double b) -> e * b;
        case Ucgen(double t, double h) -> t * h / 2;
    };
}

Burada default case'e gerek yok. Neden? Çünkü Sekil sealed interface ve tüm permitted subtypes ele alınmış. Derleyici bunu bilir — exhaustive switch.

İç İçe Record Patterns

Record'lar iç içe olduğunda pattern'lar da iç içe gidebilir:

public record Nokta(int x, int y) { }
public record Cizgi(Nokta baslangic, Nokta bitis) { }

// İç içe deconstruct
public void cizgiBilgisi(Object obj) {
    if (obj instanceof Cizgi(Nokta(int x1, int y1), Nokta(int x2, int y2))) {
        double uzunluk = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
        System.out.println("Uzunluk: " + uzunluk);
    }
}

Tek satırda Cizgi → Nokta → int değerlerine kadar iniyorsun. Gerçekten güçlü.

// Switch ile iç içe pattern
public String konumBilgisi(Cizgi cizgi) {
    return switch (cizgi) {
        case Cizgi(Nokta(int x1, int y1), Nokta(int x2, int y2))
            when x1 == x2 -> "Dikey çizgi, x=" + x1;
        case Cizgi(Nokta(int x1, int y1), Nokta(int x2, int y2))
            when y1 == y2 -> "Yatay çizgi, y=" + y1;
        case Cizgi c -> "Eğik çizgi";
    };
}

Sealed Class ile Pattern Matching

Sealed class ve pattern matching birlikte Java'nın en güçlü modern kombinasyonu.

public sealed interface Islem permits Topla, Cikar, Carp, Bol { }
public record Topla(double a, double b) implements Islem { }
public record Cikar(double a, double b) implements Islem { }
public record Carp(double a, double b) implements Islem { }
public record Bol(double a, double b) implements Islem { }

public double hesapla(Islem islem) {
    return switch (islem) {
        case Topla(double a, double b) -> a + b;
        case Cikar(double a, double b) -> a - b;
        case Carp(double a, double b)  -> a * b;
        case Bol(double a, double b)   -> {
            if (b == 0) throw new ArithmeticException("Sıfıra bölme!");
            yield a / b;
        }
    };
}

Bu pattern'ın güzelliği: yarın yeni bir Mod record'u eklersen, switch derlenmez — derleyici seni uyarır. Exhaustiveness check sayesinde hiçbir case'i unutamazsın.

// Kullanım
Islem islem = new Topla(10, 5);
double sonuc = hesapla(islem); // 15.0

islem = new Bol(10, 3);
sonuc = hesapla(islem); // 3.333...

Pratik Örnek: JSON-benzeri Veri Modeli

Gerçek dünyada bu pattern'ları bir arada kullanalım:

public sealed interface JsonDeger
    permits JsonString, JsonSayi, JsonBool, JsonDizi, JsonNull { }

public record JsonString(String deger) implements JsonDeger { }
public record JsonSayi(double deger) implements JsonDeger { }
public record JsonBool(boolean deger) implements JsonDeger { }
public record JsonDizi(List<JsonDeger> elemanlar) implements JsonDeger { }
public record JsonNull() implements JsonDeger { }

public String jsonToString(JsonDeger deger) {
    return switch (deger) {
        case JsonString(String s)  -> "\"" + s + "\"";
        case JsonSayi(double d)    -> String.valueOf(d);
        case JsonBool(boolean b)   -> String.valueOf(b);
        case JsonNull()            -> "null";
        case JsonDizi(List<JsonDeger> elemanlar) -> {
            String icerik = elemanlar.stream()
                .map(this::jsonToString)
                .collect(Collectors.joining(", "));
            yield "[" + icerik + "]";
        }
    };
}

💡 İpucu: Bu pattern fonksiyonel programlama dünyasında "Algebraic Data Types (ADT)" olarak bilinir. Sealed interface = sum type, Record = product type. Java artık bu kavramları destekliyor.

Unnamed Patterns (Java 22+)

Bazen record'un tüm component'leri gerekmez. Java 22 ile _ (underscore) kullanarak ilgilenmediğin component'leri atlayabilirsin:

// Java 22+ — sadece ihtiyacın olan component'i al
public double alanHesapla(Sekil sekil) {
    return switch (sekil) {
        case Daire(double r)     -> Math.PI * r * r;
        case Dikdortgen(double e, double b) -> e * b;
        case Ucgen(double t, _)  -> t; // yükseklik gerekmiyorsa
    };
}

Bu henüz çok yeni bir özellik. Projende Java 22+ kullanmıyorsan şimdilik bilmen yeterli.

Eski Switch vs Yeni Switch Karşılaştırma

// ESKİ — statement switch, break gerekli
switch (gun) {
    case PAZARTESI:
    case SALI:
        System.out.println("Hafta başı");
        break;
    case CUMA:
        System.out.println("CUMA!");
        break;
    default:
        System.out.println("Diğer");
}

// YENİ — expression switch, arrow syntax
String mesaj = switch (gun) {
    case PAZARTESI, SALI -> "Hafta başı";
    case CUMA -> "CUMA!";
    default -> "Diğer";
};

Yeni switch'in avantajları:

  • Expression — değer döndürebilir

  • Arrow syntax — fall-through yok, break'e gerek yok

  • Pattern matching — tip kontrolü yapabilir

  • Exhaustiveness — sealed type'larda tüm case'ler zorunlu

⚠️ Dikkat: Yeni switch'te birden fazla satır gerektiğinde yield keyword'ü kullanılır:

String sonuc = switch (obj) {
    case String s -> {
        String processed = s.trim().toUpperCase();
        yield processed; // yield = bu bloğun değeri
    }
    default -> "bilinmeyen";
};

Pratik Örnek: Komut İşleme Sistemi

Gerçek bir uygulamada pattern matching'i nasıl kullanırsın? Bir CLI komut işleyici yapalım:

public sealed interface Komut
    permits Olustur, Sil, Listele, Guncelle, Cikis { }

public record Olustur(String isim, Map<String, String> ozellikler) implements Komut { }
public record Sil(int id, boolean zorla) implements Komut { }
public record Listele(int sayfa, int boyut) implements Komut { }
public record Guncelle(int id, String alan, String deger) implements Komut { }
public record Cikis() implements Komut { }
public String komutIsle(Komut komut) {
    return switch (komut) {
        case Olustur(String isim, Map<String, String> oz)
            when isim.isBlank() -> "Hata: İsim boş olamaz";
        case Olustur(String isim, Map<String, String> oz) -> {
            // Veritabanına kaydet
            yield "'%s' oluşturuldu (%d özellik)".formatted(isim, oz.size());
        }
        case Sil(int id, boolean zorla) when zorla ->
            "ID=%d zorla silindi".formatted(id);
        case Sil(int id, boolean zorla) ->
            "ID=%d silindi".formatted(id);
        case Listele(int sayfa, int boyut) ->
            "Sayfa %d gösteriliyor (%d kayıt)".formatted(sayfa, boyut);
        case Guncelle(int id, String alan, String deger) ->
            "ID=%d %s=%s güncellendi".formatted(id, alan, deger);
        case Cikis() -> "Hoşça kal!";
    };
}

Bu yaklaşımın avantajları:

  • Yeni komut eklenince switch derlenmez → hiçbir case unutulmaz

  • Her komutun parametreleri tip-güvenli

  • Guard'lar ile validasyon switch içinde yapılır

  • if-else zincirleri yerine temiz, düz bir yapı

Pratik Örnek: Expression Evaluator

Matematiksel ifadeleri değerlendiren basit bir interpreter:

public sealed interface Expr
    permits Sayi, Topla, Cikar, Carp, Bol, Negatif { }

public record Sayi(double deger) implements Expr { }
public record Topla(Expr sol, Expr sag) implements Expr { }
public record Cikar(Expr sol, Expr sag) implements Expr { }
public record Carp(Expr sol, Expr sag) implements Expr { }
public record Bol(Expr sol, Expr sag) implements Expr { }
public record Negatif(Expr ifade) implements Expr { }

public double degerlendir(Expr expr) {
    return switch (expr) {
        case Sayi(double d) -> d;
        case Topla(Expr s, Expr sa) -> degerlendir(s) + degerlendir(sa);
        case Cikar(Expr s, Expr sa) -> degerlendir(s) - degerlendir(sa);
        case Carp(Expr s, Expr sa) -> degerlendir(s) * degerlendir(sa);
        case Bol(Expr s, Expr sa) -> {
            double bolen = degerlendir(sa);
            if (bolen == 0) throw new ArithmeticException("Sıfıra bölme!");
            yield degerlendir(s) / bolen;
        }
        case Negatif(Expr e) -> -degerlendir(e);
    };
}
// (3 + 4) * 2 = 14
Expr ifade = new Carp(
    new Topla(new Sayi(3), new Sayi(4)),
    new Sayi(2)
);
System.out.println(degerlendir(ifade)); // 14.0

Bu pattern fonksiyonel programlama dillerinde çok yaygın. Java 21 ile artık Java'da da doğal şekilde yazılabiliyor.

Pattern Matching ve Polymorphism

"Pattern matching OOP prensiplerini bozmuyor mu?" diye düşünebilirsin. Geleneksel OOP'ta davranış nesnenin içine konur (method override), pattern matching'te ise dışarıda tanımlanır.

Ne Zaman Hangisi?

OOP (method override) tercih et:

  • Yeni alt tip ekleme sık, yeni operasyon ekleme nadir

  • Her alt tip kendi davranışını bilir

Pattern matching tercih et:

  • Alt tipler sabit (sealed), yeni operasyon ekleme sık

  • Davranış alt tipe değil, dış kontekste bağlı

// OOP yaklaşımı — her şekil kendi alanını bilir
public sealed interface Sekil permits Daire, Kare {
    double alan(); // Her alt tip implement eder
}
public record Daire(double r) implements Sekil {
    public double alan() { return Math.PI * r * r; }
}
public record Kare(double kenar) implements Sekil {
    public double alan() { return kenar * kenar; }
}

// Pattern matching yaklaşımı — operasyon dışarıda tanımlanır
public String svgCiz(Sekil s) {
    return switch (s) {
        case Daire(double r) -> "<circle r=\"%s\"/>".formatted(r);
        case Kare(double k)  -> "<rect width=\"%s\" height=\"%s\"/>".formatted(k, k);
    };
}

İkisi birbirini tamamlar. alan() gibi temel davranışları interface'e koy, svgCiz() gibi dış operasyonları pattern matching ile yaz.

Yaygın Hatalar

1. Case Sırası Hatası

// ❌ DERLEME HATASI — genel case önce gelirse özel case'e ulaşılamaz
return switch (obj) {
    case Integer i -> "Sayı";            // Tüm Integer'ları yakalar
    case Integer i when i > 0 -> "Pozitif"; // Buraya asla ulaşılamaz!
    default -> "Diğer";
};

// ✅ Özel case önce
return switch (obj) {
    case Integer i when i > 0 -> "Pozitif";
    case Integer i -> "Sayı";
    default -> "Diğer";
};

2. Exhaustiveness Eksikliği

// sealed interface ile default'a gerek yok (tüm case'ler var)
// ama sealed olmayan tipte default zorunlu

return switch (obj) {
    case String s -> "Metin";
    case Integer i -> "Sayı";
    // default yok → ❌ DERLEME HATASI (Object sealed değil)
};

3. null Unutma

// Pattern matching switch'te null kontrolü eklemezsen ve null gelirse
// → NullPointerException
String s = null;
switch (s) {
    case "hello" -> System.out.println("merhaba");
    // null case yok → NPE!
}

// Güvenli versiyon
switch (s) {
    case null -> System.out.println("null!");
    case "hello" -> System.out.println("merhaba");
    default -> System.out.println("diğer");
}

Migration Rehberi

Mevcut kodunu pattern matching'e nasıl geçirirsin?

Adım 1: instanceof Chain'leri

// ÖNCE
if (obj instanceof String) {
    String s = (String) obj;
    return s.length();
} else if (obj instanceof List) {
    List<?> l = (List<?>) obj;
    return l.size();
}

// SONRA
if (obj instanceof String s) {
    return s.length();
} else if (obj instanceof List<?> l) {
    return l.size();
}

// DAHA DA İYİ — switch
return switch (obj) {
    case String s  -> s.length();
    case List<?> l -> l.size();
    default        -> 0;
};

Adım 2: Visitor Pattern Yerine

Eski Java'da polimorfik davranış için Visitor pattern kullanılırdı. Pattern matching buna çoğu zaman gerek bırakmıyor:

// ESKİ — Visitor pattern ile
interface SekilVisitor {
    double visit(Daire d);
    double visit(Dikdortgen d);
}

// YENİ — Pattern matching ile
double alan = switch (sekil) {
    case Daire(double r) -> Math.PI * r * r;
    case Dikdortgen(double e, double b) -> e * b;
};

Daha az kod, daha okunabilir, daha güvenli.

Pattern Matching Performans Notu

Pattern matching switch, derleyici tarafından optimize edilir. if-else zincirine göre genellikle eşdeğer veya daha hızlıdır çünkü:

  • Derleyici tip hiyerarşisini bilir

  • Jump table veya binary search oluşturabilir

  • Sealed class ile tüm alt tipler bilinir

// Bu switch, if-else'ten yavaş DEĞİL
return switch (sekil) {
    case Daire d -> d.alan();
    case Kare k -> k.alan();
    case Ucgen u -> u.alan();
};

Performans kaygısıyla pattern matching'den kaçınma. Derleyici zaten arka planda benzeri optimizasyonları yapıyor.

Java Versiyonu Geçiş Tablosu

ÖzellikJava VersiyonuDurum
instanceof pattern16Final
Switch expressions14Final
Sealed classes17Final
Switch pattern matching21Final
Record patterns21Final
Guarded patterns (when)21Final
Unnamed patterns (_)22Preview

Projende hangi Java versiyonu varsa, o versiyona kadar olan özellikleri kullanabilirsin. Java 21 LTS (Long Term Support) olduğu için çoğu yeni projenin hedefi bu versiyon.

Özet

  • instanceof pattern matching (Java 16): Tip kontrolü ve cast tek adımda — if (obj instanceof String s)

  • Switch pattern matching (Java 21): Switch'te tip kontrolü, null handling ve arrow syntax ile temiz kod

  • Guarded patterns (when): Case'lere ek koşul ekleme — case Integer i when i > 0

  • Record patterns: Record'ları doğrudan deconstruct etme — case Nokta(int x, int y)

  • Sealed class + record + pattern matching üçlüsü exhaustive switch sağlar — hiçbir case unutulmaz

  • Pattern matching, Visitor pattern gibi karmaşık tasarım kalıplarına olan ihtiyacı azaltır