Java'da Generics (Jenerikler) - Kapsamlı Rehber

25 Temmuz 2026 · netologist · 16 dakika, 3294 kelime ·

Not: Generics mekanizması Java 5’te tanıtıldı ve o günden beri temel yapısı (type erasure) değişmedi. Java 25’te generics’e özel yeni bir dil özelliği yok, ancak record, sealed sınıflar, pattern matching for switch ve var gibi modern özellikler generics ile birlikte sıkça kullanılıyor. Bu rehberde hem klasik generics kurallarını hem de bu modern özelliklerle nasıl bir arada kullanıldığını göreceksiniz.

İçindekiler

  1. Generics Nedir, Neden Kullanılır?
  2. Generic Sınıflar
  3. Generic Metotlar
  4. Sınırlı Tip Parametreleri (Bounded Types)
  5. Wildcard’lar (?, ? extends, ? super) ve PECS Kuralı
  6. Generic Arayüzler (Interfaces)
  7. Type Erasure (Tip Silme) - Perde Arkası
  8. Generics ve Diziler
  9. Generic Yapıcılar (Constructors)
  10. Recursive (Kendine Referanslı) Generics
  11. Varargs ile Generics
  12. Statik Üyeler ve Generics Kısıtı
  13. Java 25 Ekosisteminde Generics: Record, Sealed, Pattern Matching
  14. Gerçek Dünya Senaryoları
  15. Generics ve Casting (Tip Dönüşümü)
  16. Sık Yapılan Hatalar

1. Generics Nedir, Neden Kullanılır?

Generics, sınıf, arayüz ve metotların tip parametresi alarak tanımlanmasını sağlar. Amaç:

Generics olmadan (Java 5 öncesi tarz):

List list = new ArrayList();
list.add("merhaba");
list.add(42); // derleyici hata vermez, ama mantık hatası!
String s = (String) list.get(1); // ClassCastException çalışma zamanında!

Generics ile:

List<String> list = new ArrayList<>();
list.add("merhaba");
list.add(42); // DERLEME HATASI - hata henüz derlerken yakalanır
String s = list.get(0); // cast gerekmez

2. Generic Sınıflar

Bir sınıf, köşeli parantez içinde bir veya birden fazla tip parametresi tanımlayabilir. Geleneksel isimlendirme: T (Type), E (Element), K (Key), V (Value), N (Number), R (Result).

public class Kutu<T> {
    private T icerik;

    public Kutu(T icerik) {
        this.icerik = icerik;
    }

    public T getIcerik() {
        return icerik;
    }

    public void setIcerik(T icerik) {
        this.icerik = icerik;
    }
}

Kullanım:

Kutu<String> stringKutu = new Kutu<>("Merhaba Dünya");
Kutu<Integer> sayiKutu = new Kutu<>(2026);

System.out.println(stringKutu.getIcerik().toUpperCase());
System.out.println(sayiKutu.getIcerik() + 1);

Birden Fazla Tip Parametresi

public class Cift<K, V> {
    private final K anahtar;
    private final V deger;

    public Cift(K anahtar, V deger) {
        this.anahtar = anahtar;
        this.deger = deger;
    }

    public K getAnahtar() { return anahtar; }
    public V getDeger() { return deger; }

    @Override
    public String toString() {
        return "(" + anahtar + " -> " + deger + ")";
    }
}
Cift<String, Integer> yasBilgisi = new Cift<>("Ahmet", 30);
System.out.println(yasBilgisi); // (Ahmet -> 30)

3. Generic Metotlar

Bir metot, kendi sınıfı generic olmasa bile kendi tip parametresini tanımlayabilir. Tip parametresi, dönüş tipinden önce köşeli parantez içinde yazılır.

public class Yardimci {
    public static <T> T ilkEleman(List<T> liste) {
        return liste.get(0);
    }

    public static <T> void listeYazdir(List<T> liste) {
        for (T eleman : liste) {
            System.out.println(eleman);
        }
    }

    // Birden fazla tip parametresi
    public static <K, V> boolean anahtarVarMi(Map<K, V> map, K anahtar) {
        return map.containsKey(anahtar);
    }
}

Kullanım:

List<String> isimler = List.of("Ali", "Veli", "Ayşe");
String ilk = Yardimci.<String>ilkEleman(isimler); // tip açıkça belirtilebilir
String ilk2 = Yardimci.ilkEleman(isimler); // ya da derleyici çıkarım yapar (type inference)

4. Sınırlı Tip Parametreleri (Bounded Types)

Tip parametresinin belirli bir sınıftan türemesini veya belirli bir arayüzü uygulamasını zorunlu kılabilirsiniz.

Üst Sınır (Upper Bound) - extends

public class SayisalKutu<T extends Number> {
    private T sayi;

    public SayisalKutu(T sayi) {
        this.sayi = sayi;
    }

    public double ikiKatDegeri() {
        return sayi.doubleValue() * 2;
    }
}
SayisalKutu<Integer> kutu = new SayisalKutu<>(10);
System.out.println(kutu.ikiKatDegeri()); // 20.0

// SayisalKutu<String> hataKutu = new SayisalKutu<>("merhaba"); // DERLEME HATASI

Birden Fazla Sınır (Multiple Bounds)

interface Karsilastirilabilir<T> {
    int karsilastir(T other);
}

// En fazla bir sınıf, sınırsız sayıda arayüz olabilir; sınıf ilk sırada yazılmalı
public class Karma<T extends Number & Karsilastirilabilir<T>> {
    private T deger;
    // ...
}

5. Wildcard’lar (?, ? extends, ? super) ve PECS Kuralı

Wildcard, bilinmeyen bir tipi temsil eder ve genellikle metot parametrelerinde esneklik sağlamak için kullanılır.

Sınırsız Wildcard: List<?>

public static void listeninBoyutu(List<?> liste) {
    System.out.println("Boyut: " + liste.size());
    // liste.add(...) yapılamaz (null hariç), çünkü tip bilinmiyor
}

Üst Sınırlı Wildcard: ? extends T - Sadece Okuma (Producer)

public static double toplamHesapla(List<? extends Number> sayilar) {
    double toplam = 0;
    for (Number n : sayilar) {
        toplam += n.doubleValue();
    }
    return toplam;
}
List<Integer> tamsayilar = List.of(1, 2, 3);
List<Double> ondalikli = List.of(1.5, 2.5);

System.out.println(toplamHesapla(tamsayilar)); // 6.0
System.out.println(toplamHesapla(ondalikli));  // 4.0

Alt Sınırlı Wildcard: ? super T - Sadece Yazma (Consumer)

public static void tamsayiEkle(List<? super Integer> liste) {
    liste.add(1);
    liste.add(2);
    liste.add(3);
}
List<Number> sayilar = new ArrayList<>();
tamsayiEkle(sayilar); // Number, Integer'ın üst tipi olduğu için çalışır

List<Object> objeler = new ArrayList<>();
tamsayiEkle(objeler); // Object her şeyin üst tipi

PECS Kuralı: Producer Extends, Consumer Super

Collections.copy metodunun imzası bu kuralın klasik örneğidir:

public static <T> void copy(List<? super T> hedef, List<? extends T> kaynak) {
    for (int i = 0; i < kaynak.size(); i++) {
        hedef.set(i, kaynak.get(i));
    }
}

Wildcard Sadece Koleksiyonlara Özgü Değildir: Class<?>, Optional<?>, Comparator<?>

Wildcard (?), List veya Map gibi koleksiyon tipleriyle sınırlı değildir - herhangi bir generic tip wildcard ile kullanılabilir. En sık karşılaşılan örneklerden biri Class<?>‘dir.

Class<?> herhangiBirTip = String.class; // tipi bilinmeyen (veya önemli olmayan) bir Class referansı
Class<?> baskaBirTip = Integer.class;
Class<?> ucuncuTip = MyCustomClass.class;

Neden Class<Object> değil de Class<?>? Çünkü invariance nedeniyle Class<Integer>, Class<Object>‘e atanamaz - ama Class<?>‘e her zaman atanabilir:

Class<Integer> intClass = Integer.class;
// Class<Object> objClass = intClass; // DERLEME HATASI - invariance
Class<?> anyClass = intClass; // Bu çalışır - wildcard her generic tipi kabul eder

Bu yüzden reflection tabanlı kodlarda, framework’lerde ve önceki bölümdeki EventBus örneğinde (Map<Class<?>, List<Consumer<?>>>) Class<?> sıkça görülür - “hangi tip olduğu şu an önemli değil, ama bir tip” demenin yoludur.

Aynı mantık başka generic tipler için de geçerlidir:

public static void degeriYazdir(Optional<?> deger) {
    if (deger.isPresent()) {
        System.out.println(deger.get()); // Object olarak okunur, ama yazdırmak için yeterli
    }
}

public static void listeyiSirala(List<?> liste, Comparator<?> karsilastirici) {
    // Not: bu imza aslında pratikte pek kullanışlı değildir çünkü
    // Comparator<?> ile List<?> arasındaki tip ilişkisi kurulamaz.
    // Bunun yerine genelde bir generic metot tercih edilir: <T> void sirala(List<T> liste, Comparator<T> c)
}

Class<?> ile Tip Bilgisini Kullanma: isInstance ve cast

Class<?> referansı elinizdeyken bile, o Class nesnesinin kendi metotlarıyla (isInstance, cast) güvenli tip kontrolü yapılabilir - bkz. Bölüm 15.3.

public static boolean tipUyuyorMu(Class<?> tip, Object nesne) {
    return tip.isInstance(nesne); // örn: String.class.isInstance("merhaba") -> true
}

6. Generic Arayüzler

public interface Depo<T> {
    void ekle(T eleman);
    T getir(int index);
    int boyut();
}

public class ListeDepo<T> implements Depo<T> {
    private final List<T> veriler = new ArrayList<>();

    @Override
    public void ekle(T eleman) { veriler.add(eleman); }

    @Override
    public T getir(int index) { return veriler.get(index); }

    @Override
    public int boyut() { return veriler.size(); }
}

Bir arayüzü uygularken somut bir tip de verebilirsiniz:

public class SadeceStringDepo implements Depo<String> {
    private final List<String> veriler = new ArrayList<>();

    @Override
    public void ekle(String eleman) { veriler.add(eleman); }

    @Override
    public String getir(int index) { return veriler.get(index); }

    @Override
    public int boyut() { return veriler.size(); }
}

Standart kütüphaneden tanıdık örnekler: Comparable<T>, Comparator<T>, Iterable<T>, Function<T, R>, Supplier<T>, Consumer<T>.


7. Type Erasure (Tip Silme) - Perde Arkası

Java generics’i derleme zamanında uygular; çalışma zamanında (bytecode’da) generic tip bilgisi büyük ölçüde silinir. Bu yaklaşıma type erasure denir ve geriye dönük uyumluluğu (Java 5 öncesi kodla) sağlamak için tasarlanmıştır.

List<String> stringListe = new ArrayList<>();
List<Integer> intListe = new ArrayList<>();

System.out.println(stringListe.getClass() == intListe.getClass()); // true!

Derleyici, sınırsız T tipini Object ile, sınırlı T extends Number tipini ise Number ile değiştirir ve gerekli yerlere otomatik cast ekler.

Type Erasure’ın Sonuçları

  1. Generic tip parametresiyle örnek oluşturamazsınız:

    public class Kutu<T> {
        // T deger = new T(); // DERLEME HATASI
    }
    
  2. instanceof ile generic tip kontrolü yapamazsınız:

    List<String> liste = new ArrayList<>();
    // if (liste instanceof List<String>) {} // DERLEME HATASI
    if (liste instanceof List<?>) {} // Bu geçerlidir
    
  3. Generic dizi oluşturamazsınız (bkz. Bölüm 8).

  4. Statik alanlar tip parametresini paylaşamaz (bkz. Bölüm 12).

Class<List<String>> Elde Edilemez: Süper Tip Token’ları (Super Type Tokens)

Type erasure’ın en can sıkıcı sonuçlarından biri: List<String> için ayrı bir Class nesnesi yoktur. Çalışma zamanında yalnızca tek bir List.class (raw) vardır; List<String> ve List<Integer> aynı Class nesnesini paylaşır.

Class<List<String>> tip = List<String>.class; // DERLEME HATASI - böyle bir şey yok!

En yaygın (ama bilgi kaybına yol açan) “çözüm”, raw type kullanmaktır:

@SuppressWarnings("unchecked")
Class<List> rawTip = (Class<List>) (Class<?>) List.class; // İçindeki eleman tipi (String, Integer...) tamamen kayboldu

Bunun gerçek çözümü Super Type Token (Neal Gafter tarafından popülerleştirilen) desenidir. Mantık şu: bir generic sınıftan anonim bir alt sınıf türetirseniz, derleyici bu alt sınıfın .class dosyasına üst sınıfın generic imzasını (ParameterizedType) gömer. Bu bilgi, normal bir örnekte silinse de, sınıf bildirimi seviyesinde kalıcıdır ve reflection ile okunabilir.

public abstract class TipReferansi<T> {
    private final Type tip;

    protected TipReferansi() {
        Type ustSinif = getClass().getGenericSuperclass();
        this.tip = ((ParameterizedType) ustSinif).getActualTypeArguments()[0];
    }

    public Type getTip() {
        return tip;
    }
}

Kullanımı - dikkat: sonundaki {} zorunludur, çünkü asıl numara burada; bu, anonim bir alt sınıf oluşturur ve o alt sınıfın generic üst sınıf bilgisi class dosyasında saklanır:

TipReferansi<List<String>> referans = new TipReferansi<List<String>>() {}; // anonim alt sınıf!
Type tip = referans.getTip(); // artık çalışma zamanında "List<String>" bilgisine tam olarak sahibiz
System.out.println(tip); // java.util.List<java.lang.String>

Bu desen o kadar kullanışlıdır ki, birçok kütüphane kendi versiyonunu sunar:

Gerçek dünya örneği - Jackson ile generic bir listeyi JSON’dan deserialize etmek:

ObjectMapper mapper = new ObjectMapper();

// Bu ÇALIŞMAZ: mapper generic parametreyi (String) bilemez, List olarak deserialize eder
// List<Kullanici> liste = mapper.readValue(json, List.class);

// Bu ÇALIŞIR: TypeReference, anonim alt sınıf numarasıyla tam tip bilgisini taşır
List<Kullanici> liste = mapper.readValue(json, new TypeReference<List<Kullanici>>() {});

Özetle: Class<T> yalnızca reifiable (çalışma zamanında tam bilgisi olan) tipleri temsil edebilir - bu da String, Integer gibi somut sınıflar ve raw generic tipler (List) anlamına gelir, ama List<String> gibi parametreleştirilmiş tipler anlamına gelmez. Parametreleştirilmiş bir tipin tam bilgisine ihtiyacınız varsa, Class<T> yerine java.lang.reflect.Type (ve genellikle bir Super Type Token sarmalayıcısı) kullanmanız gerekir.


8. Generics ve Diziler

Java’da generic tipte doğrudan dizi oluşturmak mümkün değildir, çünkü diziler çalışma zamanında kendi eleman tipini bilir (reifiable), generics ise bilmez (type erasure nedeniyle).

public class DiziOrnek<T> {
    // private T[] dizi = new T[10]; // DERLEME HATASI

    @SuppressWarnings("unchecked")
    private T[] dizi = (T[]) new Object[10]; // yaygın "workaround", uyarı bastırılır
}

Bunun yerine genelde List<T> tercih edilir; eğer dizi zorunluysa, Class<T> parametresiyle Array.newInstance kullanılabilir:

@SuppressWarnings("unchecked")
public static <T> T[] diziOlustur(Class<T> tip, int boyut) {
    return (T[]) java.lang.reflect.Array.newInstance(tip, boyut);
}

// Kullanım:
String[] stringDizi = diziOlustur(String.class, 5);

9. Generic Yapıcılar (Constructors)

Bir sınıfın kendisi generic olmasa bile, yapıcı metodu kendi tip parametresine sahip olabilir.

public class Loglayici {
    private String etiket;

    public <T> Loglayici(T baslangicDegeri) {
        this.etiket = "Başlangıç: " + baslangicDegeri.toString();
    }
}
Loglayici l1 = new Loglayici(42);
Loglayici l2 = new Loglayici("merhaba");

10. Recursive (Kendine Referanslı) Generics

“Curiously Recurring Generic Pattern” olarak da bilinir. Genellikle fluent builder veya kendi tipine göre karşılaştırma yaparken kullanılır.

public abstract class Sekil<T extends Sekil<T>> implements Comparable<T> {
    protected double alan;

    @SuppressWarnings("unchecked")
    public T buyut(double kat) {
        this.alan *= kat;
        return (T) this;
    }
}

public class Daire extends Sekil<Daire> {
    @Override
    public int compareTo(Daire other) {
        return Double.compare(this.alan, other.alan);
    }
}
Daire d = new Daire();
d.buyut(2.0).buyut(1.5); // metot zincirleme, tip Daire olarak kalır

Enum<E extends Enum<E>> standart kütüphanedeki en bilinen örnektir.


11. Varargs ile Generics

@SafeVarargs
public static <T> List<T> listeOlustur(T... elemanlar) {
    return Arrays.asList(elemanlar);
}
List<String> liste = listeOlustur("bir", "iki", "üç");

@SafeVarargs anotasyonu, generic varargs kullanımında oluşabilecek “heap pollution” uyarısını bastırır; yalnızca static, final veya private metotlarda kullanılabilir.


12. Statik Üyeler ve Generics Kısıtı

Bir sınıfın tip parametresi (T), statik alan veya statik metotlarda doğrudan kullanılamaz, çünkü T, her nesne örneğine (instance) özgüdür; statik üyeler ise sınıfın kendisine aittir ve tip erişimi mümkün olmadan önce (nesne oluşmadan) var olurlar.

public class Kutu<T> {
    // private static T varsayilan; // DERLEME HATASI

    // Ama statik bir generic metot tanımlanabilir (kendi bağımsız T'si ile):
    public static <U> Kutu<U> bosKutuOlustur() {
        return new Kutu<>();
    }
}

13. Java 25 Ekosisteminde Generics: Record, Sealed, Pattern Matching

Java 25’te generics’in kendisi değişmedi, ancak modern dil özellikleriyle birlikte çok daha güçlü ve okunabilir kod yazılabiliyor.

Generic Record’lar

public record Sonuc<T>(T deger, boolean basarili, String mesaj) {
    public static <T> Sonuc<T> basari(T deger) {
        return new Sonuc<>(deger, true, "OK");
    }

    public static <T> Sonuc<T> hata(String mesaj) {
        return new Sonuc<>(null, false, mesaj);
    }
}
Sonuc<Integer> sonuc = Sonuc.basari(42);
if (sonuc.basarili()) {
    System.out.println("Değer: " + sonuc.deger());
}

Sealed Interfaces + Generics + Pattern Matching for Switch

public sealed interface Islem<T> permits Basarili, Basarisiz {}
public record Basarili<T>(T deger) implements Islem<T> {}
public record Basarisiz<T>(String hataMesaji) implements Islem<T> {}

public static <T> String sonucuIsle(Islem<T> islem) {
    return switch (islem) {
        case Basarili<T> b -> "Başarılı: " + b.deger();
        case Basarisiz<T> f -> "Hata: " + f.hataMesaji();
    };
}

Bu yapı, generic tip parametresini koruyarak exhaustive (tüm durumları kapsayan) ve tip güvenli switch ifadeleri yazmayı mümkün kılar - derleyici default dalına gerek kalmadan tüm durumların ele alındığını doğrular.

var ile Generics

var liste = new ArrayList<Map<String, List<Integer>>>(); // uzun tip bildirimini kısaltır

var, tip çıkarımı yapar ama generic tip bilgisi korunur - sadece yazım kolaylığı sağlar, tip güvenliğinden ödün verilmez.


14. Gerçek Dünya Senaryoları

Senaryo 1: Generic Repository Deseni

public interface Repository<T, ID> {
    T findirilById(ID id);
    List<T> findAll();
    T save(T entity);
    void deleteById(ID id);
}

public class KullaniciRepository implements Repository<Kullanici, Long> {
    private final Map<Long, Kullanici> depo = new HashMap<>();

    @Override
    public Kullanici findirilById(Long id) { return depo.get(id); }

    @Override
    public List<Kullanici> findAll() { return new ArrayList<>(depo.values()); }

    @Override
    public Kullanici save(Kullanici entity) {
        depo.put(entity.getId(), entity);
        return entity;
    }

    @Override
    public void deleteById(Long id) { depo.remove(id); }
}

Senaryo 2: Generic Event Bus

public class EventBus {
    private final Map<Class<?>, List<Consumer<?>>> dinleyiciler = new HashMap<>();

    public <T> void abone(Class<T> tip, Consumer<T> dinleyici) {
        dinleyiciler.computeIfAbsent(tip, k -> new ArrayList<>()).add(dinleyici);
    }

    @SuppressWarnings("unchecked")
    public <T> void yayinla(T olay) {
        List<Consumer<?>> liste = dinleyiciler.get(olay.getClass());
        if (liste != null) {
            for (Consumer<?> d : liste) {
                ((Consumer<T>) d).accept(olay);
            }
        }
    }
}

Senaryo 3: Generic Builder Deseni (Recursive Generics ile)

public abstract class SorguBuilder<T extends SorguBuilder<T>> {
    protected StringBuilder sorgu = new StringBuilder("SELECT * FROM tablo");

    @SuppressWarnings("unchecked")
    public T where(String kosul) {
        sorgu.append(" WHERE ").append(kosul);
        return (T) this;
    }

    public String build() { return sorgu.toString(); }
}

public class KullaniciSorguBuilder extends SorguBuilder<KullaniciSorguBuilder> {
    public KullaniciSorguBuilder aktifKullanicilar() {
        return where("aktif = true");
    }
}

Senaryo 4: Optional / Either benzeri Wrapper

public sealed interface Either<L, R> permits Left, Right {}
public record Left<L, R>(L deger) implements Either<L, R> {}
public record Right<L, R>(R deger) implements Either<L, R> {}

Senaryo 5: Generic Caching Katmanı

public class Cache<K, V> {
    private final Map<K, V> depo = new ConcurrentHashMap<>();
    private final Function<K, V> yukleyici;

    public Cache(Function<K, V> yukleyici) {
        this.yukleyici = yukleyici;
    }

    public V getir(K anahtar) {
        return depo.computeIfAbsent(anahtar, yukleyici);
    }
}
Cache<String, Kullanici> kullaniciCache = new Cache<>(id -> veritabanindanGetir(id));
Kullanici k = kullaniciCache.getir("kullanici-123");

15. Generics ve Casting (Tip Dönüşümü)

Generic koleksiyonlarla çalışırken casting, type erasure yüzünden hem çok sık karşılaşılan hem de en çok kafa karıştıran konulardan biridir. Bu bölümde tüm senaryoları örneklerle inceliyoruz.

15.1. Neden List<Object>, List<String>‘e Cast Edilemez? (Invariance)

Java’da generic tipler invariant’tır: String, Object‘in alt tipi olsa bile, List<String>, List<Object>‘in alt tipi değildir.

List<String> stringListe = new ArrayList<>();
// List<Object> objeListesi = stringListe; // DERLEME HATASI

List<Object> objeListesi = (List<Object>) (List<?>) stringListe; // "double cast" ile zorlanabilir

Bu kısıtlama olmasaydı şu tehlike ortaya çıkardı:

List<String> stringListe = new ArrayList<>();
List<Object> objeListesi = stringListe; // eğer izin verilseydi...
objeListesi.add(42); // Integer eklenirdi!
String s = stringListe.get(0); // ClassCastException! (Integer -> String)

15.2. Unchecked Cast: (T) ile Cast Etmek

Type erasure nedeniyle derleyici, çalışma zamanında T‘nin gerçek tipini bilmez. Bu yüzden Object‘i doğrudan T‘ye cast ettiğinizde derleyici sadece bir uyarı (warning) verir, hata vermez - çünkü kontrol edemez:

public class Kutu<T> {
    private Object icerik;

    @SuppressWarnings("unchecked")
    public T getIcerik() {
        return (T) icerik; // unchecked cast uyarısı
    }
}

Bu cast çalışma zamanında gerçekten kontrol edilmez - eğer icerik yanlış tipteyse, hata getIcerik() çağrıldığı anda değil, dönen değer başka bir tipe atanmaya çalışıldığında ortaya çıkar (gecikmeli / delayed ClassCastException):

Kutu rawKutu = new Kutu(); // raw type kullanımı - tehlikeli!
rawKutu.icerik = 42;
Kutu<String> stringKutu = rawKutu; // derleyici uyarır ama izin verir
String s = stringKutu.getIcerik(); // ClassCastException burada patlar, cast anında değil!

15.3. Class<T>.cast() - Güvenli Runtime Cast

Reflection tabanlı, gerçekten kontrol edilen bir cast için Class<T>.cast() metodu kullanılabilir:

public static <T> T guvenliCast(Class<T> tip, Object nesne) {
    return tip.cast(nesne); // nesne, tip'e uymuyorsa hemen ClassCastException fırlatır
}
Object deger = "merhaba";
String s = guvenliCast(String.class, deger); // güvenli, anında kontrol edilir

Object yanlisDeger = 42;
String s2 = guvenliCast(String.class, yanlisDeger); // ClassCastException - HEMEN fırlatılır

Bu, (T) obj unchecked cast’ine göre çok daha güvenlidir çünkü hata, sorunun kaynağında (cast anında) yakalanır, sonradan başka bir yerde değil.

15.4. Koleksiyonlarda Collections.checkedList / checkedMap

Bir koleksiyona yanlışlıkla yanlış tipte eleman eklenmesini (örneğin raw type üzerinden) çalışma zamanında yakalamak için:

List<String> stringListe = new ArrayList<>();
List<String> guvenliListe = Collections.checkedList(stringListe, String.class);

List rawListe = guvenliListe; // raw type ile erişim
rawListe.add(42); // ClassCastException HEMEN fırlatılır (eklerken), sonradan değil!

Normal bir ArrayList<String> kullanılsaydı, rawListe.add(42) derleyici uyarısıyla geçerdi ama hata ancak get() ile okurken (cast sırasında) ortaya çıkardı. checkedList bu hatayı ekleme anında yakalar.

15.5. Wildcard Capture ile Cast Sorunları

List<?> üzerinde doğrudan set()/add() çağıramazsınız çünkü derleyici tipin ne olduğunu bilmez. “Wildcard capture” yardımcı generic metotla çözülür:

public static void tersineCevir(List<?> liste) {
    tersineCevirYardimci(liste);
}

// Yardımcı metot, ? tipini T olarak "yakalar" (capture)
private static <T> void tersineCevirYardimci(List<T> liste) {
    List<T> kopya = new ArrayList<>(liste);
    Collections.reverse(kopya);
    for (int i = 0; i < liste.size(); i++) {
        ((List<T>) liste).set(i, kopya.get(i)); // artık T olarak biliniyor, cast güvenli
    }
}

15.6. Object[]‘i T[]‘e Cast Etmek (Heap Pollution Riski)

@SuppressWarnings("unchecked")
public static <T> T[] listeyiDiziyeCevir(List<T> liste, T[] sablon) {
    return liste.toArray(sablon); // içeride güvenli cast yapılır
}

Kendi elle yazdığınız cast’lerde ise dikkatli olun:

public static <T> T[] tehlikeliDiziOlustur(int boyut) {
    return (T[]) new Object[boyut]; // ÇALIŞMA ZAMANINDA ClassCastException RİSKİ
}

// Kullanım:
String[] diziler = tehlikeliDiziOlustur(5); // burada patlar! Object[] -> String[] cast'i geçersiz

Bu, heap pollution‘ın klasik örneğidir: derleyici cast’e izin verir (uyarıyla), ama dizinin gerçek çalışma zamanı tipi Object[] olduğu için, bu diziyi String[] bekleyen bir yere ilettiğinizde ClassCastException alırsınız.

15.7. Raw Type’lardan Generic Tiplere Cast (Legacy Kod Entegrasyonu)

Eski (generics öncesi) API’lerle çalışırken sıkça karşılaşılan bir senaryo:

@SuppressWarnings("unchecked")
public List<String> eskiApiIleUyumlu(Object eskiSonuc) {
    // eskiSonuc aslında bir raw List, ama tipi Object olarak geliyor (örn. bir kütüphaneden)
    List rawListe = (List) eskiSonuc;
    return (List<String>) rawListe; // iki aşamalı unchecked cast
}

Bu tür cast’lerde @SuppressWarnings("unchecked") anotasyonunu mümkün olduğunca dar bir kapsamda (metodun tamamı yerine sadece ilgili değişkende) kullanmak iyi bir pratiktir; böylece diğer olası hatalar gizlenmemiş olur.

15.8. Özet Tablo: Hangi Cast Ne Zaman Güvenli?

Cast TürüDerleme Zamanı KontrolÇalışma Zamanı KontrolGüvenlik
(T) object (doğrudan unchecked cast)Uyarı verir, izin verirYok - hata sonradan, başka yerde patlarDüşük
Class<T>.cast(object)DerlerVar - anında ClassCastExceptionYüksek
Collections.checkedList/checkedMapDerlerVar - ekleme anında kontrol ederYüksek
List<Object> x = (List<Object>)(List<?>) yUyarı verir, izin verirYok (invariance’ı bypass eder)Düşük - dikkatli kullanın
(T[]) new Object[n]Uyarı verir, izin verirYok - dizi dışarı sızarsa patlarDüşük - mümkünse List<T> tercih edin

16. Sık Yapılan Hatalar

HataAçıklama
new T() yazmaya çalışmakType erasure nedeniyle mümkün değil; Supplier<T> veya Class<T> + reflection kullanın
List<T>[] gibi generic dizi oluşturmakMümkün değil; List<List<T>> veya @SuppressWarnings ile workaround kullanın
Wildcard’a add() çağırmak (List<? extends T>)? extends sadece okumaya izin verir; PECS kuralını hatırlayın
Statik alanda T kullanmakStatik üyeler tip parametresine erişemez
raw type kullanmak (List yerine List<String>)Tip güvenliğini tamamen kaybedersiniz, derleyici uyarır
instanceof List<String> yazmakDerleme hatası; sadece instanceof List<?> geçerlidir

Özet

Generics, Java’nın tip güvenliği felsefesinin temel taşlarından biridir. Type erasure nedeniyle bazı kısıtlamalar olsa da (dizi oluşturma, statik alan kısıtı, çalışma zamanı tip kontrolü), wildcard’lar, sınırlı tipler ve recursive generics gibi araçlarla oldukça esnek ve güvenli API’ler tasarlanabilir. Java 25 itibarıyla generics; record, sealed interfaces ve pattern matching gibi modern özelliklerle birlikte kullanılarak çok daha okunabilir ve sağlam kod tabanları oluşturmaya imkân tanır.