← Kursa Dön
📄 Text · 15 min

Generics Temelleri

Java'da generics konusu, ilk bakışta "bu ne karmaşık syntax" dedirten ama bir kez anladıktan sonra "bunlar olmadan nasıl yaşamışım" diyeceğin konulardan biri. Hadi gelin adım adım bakalım.


Generics Nedir ve Neden Var?

Bir düşün: Bir kutu yapıyorsun. Bu kutu bazen elma taşıyacak, bazen portakal, bazen kitap. Ama sen kutuyu yaparken "bu kutu sadece Object taşır" dersen, kutudan bir şey çıkardığında ne çıkacağını bilemezsin. Her seferinde kontrol etmen, cast etmen gerekir.

İşte generics, kutunun üstüne etiket yapıştırmak gibi. "Bu kutu sadece elma taşır" diyorsun ve artık hem koyarken hem çıkarırken herkes ne olduğunu biliyor.

Generics olmadan hayat şöyle görünüyordu:

List liste = new ArrayList();
liste.add("Merhaba");
liste.add(42); // Sorun yok, herkes girebilir!

String s = (String) liste.get(0); // Cast gerekli
String x = (String) liste.get(1); // BOOM! ClassCastException

Gördün mü? Listeye hem String hem Integer koyabildik. Derleyici ses çıkarmadı. Ama çalışma zamanında patlama oldu. Bu, runtime hatası — yani en kötü tür hata. Müşteri görür, sen görmezsin.

Generics ile aynı kod:

List<String> liste = new ArrayList<>();
liste.add("Merhaba");
liste.add(42); // DERLEME HATASI! Buraya gelmeden yakalar.

String s = liste.get(0); // Cast gerekmez

Derleyici daha kod çalışmadan "Dur kardeşim, buraya Integer koyamazsın" diyor. Compile-time safety — hataları erken yakala, gece rahat uyu.


Type Parameter — T, E, K, V Harfleri

Generic yapılarda <T> gibi harfler görürsün. Bunlar type parameter yani "tip parametresi". Aslında istediğin harfi ya da kelimeyi kullanabilirsin ama Java dünyasında gelenekler var:

HarfAnlamKullanım Yeri
TTypeGenel amaçlı tip
EElementKoleksiyonlardaki eleman
KKeyMap'teki anahtar
VValueMap'teki değer
NNumberSayısal tipler
RResultDönüş tipi

Bu harfler zorunlu değil. <Elma> yazsan da çalışır. Ama T yazdığında herkes ne demek istediğini anlar. Kod okunabilirliği her şeydir.

// T = genel amaçlı tip parametresi
public class Kutu<T> {
    private T icerik;
    
    public void koy(T icerik) {
        this.icerik = icerik;
    }
    
    public T al() {
        return icerik;
    }
}

Bu sınıfı kullanırken tip belirtiyorsun:

Kutu<String> stringKutu = new Kutu<>();
stringKutu.koy("Java");
String deger = stringKutu.al(); // Cast yok!

Kutu<Integer> intKutu = new Kutu<>();
intKutu.koy(42);
int sayi = intKutu.al(); // Cast yok!

Aynı sınıf, farklı tipler. Kod tekrarı sıfır. Bu generics'in gücü.


Generic Class Yazmak

Kendi generic sınıfını yazmak aslında çok kolay. Sınıf adının yanına <T> ekle, sonra T'yi sınıfın içinde normal bir tip gibi kullan.

Tek Tip Parametreli

public class Cift<T> {
    private T birinci;
    private T ikinci;
    
    public Cift(T birinci, T ikinci) {
        this.birinci = birinci;
        this.ikinci = ikinci;
    }
    
    public T getBirinci() { return birinci; }
    public T getIkinci() { return ikinci; }
    
    @Override
    public String toString() {
        return "(" + birinci + ", " + ikinci + ")";
    }
}

Kullanımı:

Cift<String> isimler = new Cift<>("Ali", "Veli");
System.out.println(isimler); // (Ali, Veli)

Cift<Integer> sayilar = new Cift<>(10, 20);
System.out.println(sayilar.getBirinci()); // 10

Çoklu Tip Parametreli

Bazen tek tip yetmez. Örneğin bir anahtar-değer çifti tutmak istiyorsun:

public class KeyValue<K, V> {
    private K key;
    private V value;
    
    public KeyValue(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
    
    @Override
    public String toString() {
        return key + " = " + value;
    }
}
KeyValue<String, Integer> yas = new KeyValue<>("Ali", 25);
System.out.println(yas); // Ali = 25

KeyValue<Integer, Boolean> sonuc = new KeyValue<>(1, true);
System.out.println(sonuc); // 1 = true

İki farklı tip parametresi, tek bir sınıf. Map.Entry<K, V> tam olarak böyle çalışır.


Generic Method Yazmak

Bazen tüm sınıfı generic yapmak istemezsin. Sadece bir method generic olsun istersin. Bu da mümkün.

Generic method yazarken, tip parametresini dönüş tipinden önce belirtirsin:

public class Yardimci {
    
    // Generic method: <T> dönüş tipinden önce gelir
    public static <T> void diziyiYazdir(T[] dizi) {
        for (T eleman : dizi) {
            System.out.print(eleman + " ");
        }
        System.out.println();
    }
    
    public static <T> T sonElemaniAl(T[] dizi) {
        if (dizi.length == 0) return null;
        return dizi[dizi.length - 1];
    }
}
String[] isimler = {"Ali", "Veli", "Ayşe"};
Integer[] sayilar = {1, 2, 3, 4, 5};

Yardimci.diziyiYazdir(isimler); // Ali Veli Ayşe
Yardimci.diziyiYazdir(sayilar); // 1 2 3 4 5

String sonIsim = Yardimci.sonElemaniAl(isimler); // Ayşe
Integer sonSayi = Yardimci.sonElemaniAl(sayilar); // 5

Derleyici, parametreye bakarak T'nin ne olduğunu otomatik anlar. Buna type inference denir. Yani Yardimci.<String>diziyiYazdir(isimler) yazmana gerek yok, ama yazabilirsin.

Generic Method vs Generic Class Farkı

// Generic CLASS — sınıfın kendisi generic
public class Kutu<T> {
    public T al() { ... }
}

// Generic METHOD — sadece method generic
public class Yardimci {
    public static <T> T sec(T a, T b) { ... }
}

Generic class'ta T, sınıf oluşturulurken belirlenir. Generic method'da T, method çağrılırken belirlenir. İkisi farklı kapsamda çalışır.


Generic Interface

Interface'ler de generic olabilir. Aslında zaten bildiğin pek çok interface generic:

public interface Comparable<T> {
    int compareTo(T other);
}

public interface List<E> extends Collection<E> {
    E get(int index);
    boolean add(E element);
}

Kendi generic interface'ini yazmak:

public interface Donusturucu<G, H> {
    H donustur(G girdi);
}

Implement ederken tipi belirtirsin:

public class StringToInteger implements Donusturucu<String, Integer> {
    @Override
    public Integer donustur(String girdi) {
        return Integer.parseInt(girdi);
    }
}

Ya da implement eden sınıf da generic olabilir:

public class KimlikDonusturucu<T> implements Donusturucu<T, T> {
    @Override
    public T donustur(T girdi) {
        return girdi; // Aynısını döndür
    }
}

Diamond Operator (<>)

Java 7 ile gelen bir kolaylık. Eskiden şöyle yazıyorduk:

// Java 7 öncesi — tip iki kez yazılır
Map<String, List<Integer>> harita = new HashMap<String, List<Integer>>();

Uzun ve gereksiz tekrar. Java 7'de diamond operator geldi:

// Java 7+ — sağ taraf boş bırakılır, derleyici anlar
Map<String, List<Integer>> harita = new HashMap<>();

Derleyici sol taraftaki tip bilgisinden sağ tarafı çıkarıyor. Buna da type inference denir.

💡 Diamond operator sadece `new` ile çalışır. Değişken tanımında kullanılmaz. var ile birlikte kullanırken dikkat — derleyici tipi Object olarak çıkarabilir.


Type Erasure — Generics'in Gizli Sırrı

Şimdi en ilginç kısma geldik. Java'daki generics aslında bir derleme zamanı illüzyonu. Derleyici tip kontrollerini yaptıktan sonra, bytecode'a çevirirken tüm generic bilgileri siler.

Bu mekanizmaya type erasure denir.

Yani şu kod:

List<String> liste = new ArrayList<>();
liste.add("Merhaba");
String s = liste.get(0);

Derlendikten sonra bytecode'da şuna dönüşür:

List liste = new ArrayList();
liste.add("Merhaba");
String s = (String) liste.get(0); // Cast geri geldi!

Derleyici senin için cast'leri otomatik ekliyor ve tip güvenliğini garanti ediyor. Ama çalışma zamanında List<String> ile List<Integer> arasında hiçbir fark yok — ikisi de sadece List.

Type Erasure'ın Sonuçları

1. instanceof ile generic tip kontrolü yapılamaz:

// DERLEME HATASI!
if (liste instanceof List<String>) { } // Çalışmaz

// Bunun yerine:
if (liste instanceof List<?>) { } // Bu olur

2. Generic tip ile new yapılamaz:

public class Kutu<T> {
    // DERLEME HATASI!
    T nesne = new T(); // Çalışmaz
    
    // DERLEME HATASI!
    T[] dizi = new T[10]; // Çalışmaz
}

Neden? Çünkü çalışma zamanında T'nin ne olduğu bilinmiyor. new operatörü gerçek bir tip istiyor.

3. Static alanda tip parametresi kullanılamaz:

public class Kutu<T> {
    // DERLEME HATASI!
    private static T statikDeger; // Çalışmaz
}

Static alan sınıfa ait, instance'a değil. Ama T her instance için farklı olabilir. Bu çelişki.

4. Overload generic tiplerle çalışmaz:

// DERLEME HATASI! İkisi de erasure sonrası aynı imzaya sahip
public void islem(List<String> liste) { }
public void islem(List<Integer> liste) { } // Aynı: islem(List)

Type erasure sonrası ikisi de islem(List) olur. Derleyici ayırt edemez.

⚠️ Type erasure, Java'nın geriye uyumluluk kararının sonucu. Java 5'te generics eklendiğinde, eski kodların (Java 4 ve öncesi) hâlâ çalışması gerekiyordu. Bu yüzden generic bilgi bytecode'a yazılmadı. Bugün C# gibi dillerde generics çalışma zamanında da korunur (reified generics), ama Java'da bu lüks yok.


Raw Type — Generics'siz Kullanım

Generic bir sınıfı tip parametresi vermeden kullanmaya raw type denir:

// Raw type — tip belirtilmemiş
List rawList = new ArrayList();
rawList.add("Merhaba");
rawList.add(42);
rawList.add(true);

Bu, generics öncesi koda uyumluluk için var. Ama asla kullanma. Derleyici uyarı verir ve tip güvenliği kaybolur.

// Raw type → uyarı
List rawList = new ArrayList();

// Parametreli type → güvenli
List<String> safeList = new ArrayList<>();

⚠️ Raw type kullanmak, emniyet kemeri takmadan araba sürmek gibidir. Derleyici uyarır ama engellemez. Kaza olursa (ClassCastException) sonuçlarına katlanırsın.


Generics ve Primitive Tipler

Generics sadece referans tipler ile çalışır. Primitive tipler (int, double, boolean vs.) kullanılamaz.

// DERLEME HATASI!
List<int> sayilar = new ArrayList<>();

// Doğrusu — wrapper class kullan
List<Integer> sayilar = new ArrayList<>();

Java'nın autoboxing/unboxing özelliği sayesinde primitive ve wrapper arasında otomatik dönüşüm olur:

List<Integer> sayilar = new ArrayList<>();
sayilar.add(5);           // autoboxing: int → Integer
int deger = sayilar.get(0); // unboxing: Integer → int

Ama dikkat: autoboxing'in bir performans maliyeti var. Çok büyük koleksiyonlarda (milyonlarca eleman) fark edilebilir.


Gerçek Hayat Örneği — Generic Repository

Veritabanı işlemlerinde aynı CRUD operasyonları her tablo için tekrarlanır. Generic ile bunu tek seferde çözelim:

public interface Repository<T, ID> {
    T findById(ID id);
    List<T> findAll();
    void save(T entity);
    void delete(ID id);
}
public class UserRepository implements Repository<User, Long> {
    @Override
    public User findById(Long id) {
        // Veritabanından User bul
        return new User(id, "Ali");
    }
    
    @Override
    public List<User> findAll() {
        return List.of(new User(1L, "Ali"), new User(2L, "Veli"));
    }
    
    @Override
    public void save(User entity) {
        System.out.println("User kaydedildi: " + entity);
    }
    
    @Override
    public void delete(Long id) {
        System.out.println("User silindi: " + id);
    }
}

Aynı interface'i ProductRepository implements Repository<Product, String> şeklinde de kullanabilirsin. Kod tekrarı sıfır.


Generic Metod ile Tip Güvenli Yardımcılar

Birkaç faydalı generic metod örneği:

public class GenericUtils {
    
    // Null-safe karşılaştırma
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }
    
    // Listeyi filtreleme
    public static <T> List<T> filtrele(List<T> liste, T hedef) {
        List<T> sonuc = new ArrayList<>();
        for (T eleman : liste) {
            if (eleman.equals(hedef)) {
                sonuc.add(eleman);
            }
        }
        return sonuc;
    }
    
    // Diziyi listeye çevir
    public static <T> List<T> diziyiListeYap(T[] dizi) {
        List<T> liste = new ArrayList<>();
        for (T eleman : dizi) {
            liste.add(eleman);
        }
        return liste;
    }
}
System.out.println(GenericUtils.max(10, 20));      // 20
System.out.println(GenericUtils.max("Ali", "Veli")); // Veli

String[] isimler = {"Ali", "Veli", "Ali"};
List<String> sonuc = GenericUtils.filtrele(
    GenericUtils.diziyiListeYap(isimler), "Ali"
);
System.out.println(sonuc); // [Ali, Ali]

<T extends Comparable<T>> kısmına şimdilik takılma — bir sonraki derste (bounded types) detaylıca göreceğiz.


Sık Yapılan Hatalar

1. Generic Dizi Oluşturmaya Çalışmak

// YANLIŞ — derlenmez
T[] dizi = new T[10];

// DOĞRU — Object dizisi kullan ve cast et
@SuppressWarnings("unchecked")
T[] dizi = (T[]) new Object[10];

2. Static Context'te Tip Parametresi

public class Kutu<T> {
    // YANLIŞ — T instance'a bağlı, static'te kullanılmaz
    public static T create() { return null; }
    
    // DOĞRU — kendi tip parametresini tanımla
    public static <T> T create(Class<T> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }
}

3. Raw Type ile Generic Karıştırma

List<String> stringList = new ArrayList<>();
List rawList = stringList; // Uyarı ama çalışır
rawList.add(42);           // Uyarı ama çalışır

// Sonra...
String s = stringList.get(0); // ClassCastException! Runtime patlama

Raw type kullandığın an, generic'in sağladığı güvenliği kaybedersin. Heap pollution denilen bu durum, uzak bir yerde patlama yaratır.


Generics ve Kalıtım İlişkisi

Önemli bir kural: `Integer` `Number`'ın alt sınıfı olsa bile, `List<Integer>` `List<Number>`'ın alt tipi DEĞİLDİR.

List<Integer> intList = new ArrayList<>();
// DERLEME HATASI!
List<Number> numList = intList; // Çalışmaz!

Neden? Eğer bu çalışsaydı:

List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Diyelim ki çalıştı...
numList.add(3.14); // Double ekledik — çünkü Number kabul eder
Integer i = intList.get(0); // AMA intList'ten Integer bekliyorduk! BOOM!

Bu durumu çözmek için wildcards kullanılır. Bir sonraki dersin konusu tam olarak bu.

💡 Kural olarak hatırla: Generics invariant'tır. A extends B olsa bile List<A> extends List<B> değildir. Bu durum dizilerde farklıdır — Integer[] bir Number[]'dır (covariant). Ama bu, dizilerde runtime hatasına yol açabilir.


Özet

  • Generics, tip güvenliğini compile-time'da sağlar — ClassCastException'ı önler

  • Type parameter (T, E, K, V) ile sınıf, method ve interface'ler generic yapılabilir

  • Diamond operator (<>) gereksiz tip tekrarını önler

  • Type erasure nedeniyle generic bilgi çalışma zamanında silinir — instanceof, new T(), static alan kısıtlamaları buradan gelir

  • Raw type kullanma — her zaman tip parametresi belirt

  • Generics invariant'tırList<Integer>, List<Number>'ın alt tipi değildir