← Kursa Dön
📄 Text · 12 min

Bounded Types ve Wildcards

Bir önceki derste generics'in temellerini gördük. Şimdi generics'in en güçlü ve en kafa karıştırıcı kısmına geliyoruz: wildcards. Ama merak etme, bir kez anladığında "aa bu kadar basitmiş" diyeceksin.


Bounded Type Parameters — Tipi Sınırla

Bazen generic bir metod yazarsın ama "her tip gelmesin, sadece sayılar gelsin" dersin. İşte bounded type tam bunu yapar.

Analoji: Bir konsere bilet satıyorsun. Generics "herkes gelsin" demek. Bounded type ise "sadece 18 yaş üstü gelsin" demek. Kapıda kontrol var.

Upper Bound — extends ile

// Sadece Number ve alt sınıfları (Integer, Double, Long...) kabul eder
public static <T extends Number> double topla(List<T> liste) {
    double toplam = 0;
    for (T eleman : liste) {
        toplam += eleman.doubleValue(); // Number'ın metodu
    }
    return toplam;
}
List<Integer> intListe = List.of(1, 2, 3);
List<Double> dblListe = List.of(1.5, 2.5, 3.5);

System.out.println(topla(intListe)); // 6.0
System.out.println(topla(dblListe)); // 7.5

List<String> strListe = List.of("a", "b");
// topla(strListe); // DERLEME HATASI! String, Number değil.

T extends Number dediğinde, T üzerinde Number sınıfının tüm metodlarını (doubleValue, intValue vs.) kullanabilirsin. Derleyici biliyor ki T en azından bir Number.

Birden Fazla Bound

Bir tip hem bir sınıfı extend etsin hem de bir interface'i implement etsin istiyorsan:

// T hem Comparable hem de Serializable olmalı
public static <T extends Comparable<T> & Serializable> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

Kurallar:

  • Sınıf varsa önce yazılır

  • Interface'ler & ile eklenir

  • Birden fazla sınıf extend edilemez (Java'da çoklu kalıtım yok)

// DOĞRU: Sınıf önce, interface sonra
<T extends Number & Comparable<T>>

// YANLIŞ: Interface önce yazılamaz (sınıf varsa)
<T extends Comparable<T> & Number> // Derleme hatası

Wildcard Nedir?

Wildcard, ? sembolü ile gösterilir ve "bilmiyorum, herhangi bir tip" anlamına gelir. Ama "herhangi" derken bile kurallar koyabilirsin.

Üç çeşit wildcard var:

WildcardSyntaxAnlam
Unbounded<?>Herhangi bir tip
Upper bounded<? extends T>T veya T'nin alt sınıfları
Lower bounded<? super T>T veya T'nin üst sınıfları

Unbounded Wildcard — <?>

"Ne gelirse gelsin, bana farketmez" dediğin durumlarda kullanılır. Genellikle sadece okuma yapacağın ve tipin ne olduğunun önemli olmadığı durumlarda:

public static void listeYazdir(List<?> liste) {
    for (Object eleman : liste) {
        System.out.println(eleman);
    }
}
List<String> isimler = List.of("Ali", "Veli");
List<Integer> sayilar = List.of(1, 2, 3);
List<Boolean> bayraklar = List.of(true, false);

listeYazdir(isimler);    // Çalışır
listeYazdir(sayilar);    // Çalışır
listeYazdir(bayraklar);  // Çalışır

⚠️ `List<?>` ile `List<Object>` aynı şey DEĞİLDİR! List<?> her türlü listeyi kabul eder. List<Object> sadece List<Object> kabul eder. Hatırla: generics invariant'tır.

List<String> isimler = List.of("Ali");

// ÇALIŞIR — <?> her listeyi kabul eder
listeYazdir(isimler);

// DERLEME HATASI — List<String>, List<Object> değildir!
public static void yazdir(List<Object> liste) { }
yazdir(isimler); // Hata!

Unbounded Wildcard'a Yazma Kısıtı

List<?>'e eleman ekleyemezsin (null hariç):

List<?> liste = new ArrayList<String>();
// liste.add("test"); // DERLEME HATASI!
// liste.add(42);     // DERLEME HATASI!
liste.add(null);       // Sadece null olur

Neden? Derleyici listenin gerçek tipini bilmiyor. Belki List<Integer>'dır, belki List<String>. Yanlış tip eklememek için engeller.


Upper Bounded Wildcard — <? extends T>

"T veya T'nin alt sınıfları olsun" demek. Üst sınır koyuyorsun.

public static double topla(List<? extends Number> liste) {
    double toplam = 0;
    for (Number n : liste) {
        toplam += n.doubleValue();
    }
    return toplam;
}
List<Integer> intListe = List.of(1, 2, 3);
List<Double> dblListe = List.of(1.5, 2.5);
List<Long> longListe = List.of(100L, 200L);

topla(intListe);  // ✅ Integer extends Number
topla(dblListe);  // ✅ Double extends Number
topla(longListe); // ✅ Long extends Number

extends ile Okuma ve Yazma

<? extends T> ile okuyabilirsin ama yazamazsın:

List<? extends Number> liste = new ArrayList<Integer>();

Number n = liste.get(0); // ✅ Okuma — en azından Number döner
// liste.add(42);        // ❌ Yazma — derleyici tipi bilmiyor

Neden yazamıyorsun? Çünkü liste aslında List<Integer> olabilir, List<Double> olabilir. Sen Double eklersen ve liste aslında List<Integer> ise, felaket olur.


Lower Bounded Wildcard — <? super T>

"T veya T'nin üst sınıfları olsun" demek. Alt sınır koyuyorsun.

public static void integerEkle(List<? super Integer> liste) {
    liste.add(1);
    liste.add(2);
    liste.add(3);
}
List<Integer> intListe = new ArrayList<>();
List<Number> numListe = new ArrayList<>();
List<Object> objListe = new ArrayList<>();

integerEkle(intListe); // ✅ Integer, Integer'ın kendisi
integerEkle(numListe); // ✅ Number, Integer'ın üst sınıfı
integerEkle(objListe); // ✅ Object, Integer'ın üst sınıfı

super ile Okuma ve Yazma

<? super T> ile yazabilirsin ama okuma sınırlıdır:

List<? super Integer> liste = new ArrayList<Number>();

liste.add(42);           // ✅ Yazma — Integer kesin uyar
liste.add(100);          // ✅ Yazma — Integer kesin uyar

Object obj = liste.get(0); // Okuma — sadece Object olarak alabilirsin
// Integer i = liste.get(0); // ❌ Derleyici Integer garantisi vermez

super kullandığında listeye Integer ekleyebilirsin çünkü liste en azından Integer'ı kabul edecek kapasitede. Ama okurken ne çıkacağını bilemezsin — Object olarak alman gerekir.


PECS Prensibi — Producer Extends, Consumer Super

Bu, wildcard'ların en önemli kuralı. Joshua Bloch'un "Effective Java" kitabından gelen altın kural:

  • Producer (üretici, veri sağlayan) → extends kullan

  • Consumer (tüketici, veri alan) → super kullan

Bir koleksiyondan okuyacaksan (produce) → <? extends T> Bir koleksiyona yazacaksan (consume) → <? super T> İkisini de yapacaksan → wildcard kullanma, düz T kullan.

PECS Örneği

public static <T> void kopyala(
    List<? extends T> kaynak,  // Producer — veri üretir, okuruz
    List<? super T> hedef      // Consumer — veri tüketir, yazarız
) {
    for (T eleman : kaynak) {
        hedef.add(eleman);
    }
}
List<Integer> intListe = List.of(1, 2, 3);
List<Number> numListe = new ArrayList<>();

kopyala(intListe, numListe); // Integer → Number kopyalama
System.out.println(numListe); // [1, 2, 3]

kaynak listesinden okuyoruz → extends (producer) hedef listesine yazıyoruz → super (consumer)

Collections.copy() Tam Böyle Çalışır

// Java'nın kendi implementasyonu
public static <T> void copy(
    List<? super T> dest,    // Consumer
    List<? extends T> src    // Producer
) { ... }

💡 PECS'i hatırlamanın kolay yolu: "PE-CS" → "Producer Extends, Consumer Super". Üretici extends, tüketici super. Veri üretiyorsa (okuyorsun) extends, veri tüketiyorsa (yazıyorsun) super.


Wildcard ve Bounded Type Arasındaki Fark

Bazen ikisi de aynı işi yapar gibi görünür. Ama farkları var:

// Bounded type parameter — T'yi metod içinde kullanabilirsin
public static <T extends Number> T ilkEleman(List<T> liste) {
    return liste.get(0); // T döner
}

// Wildcard — ? bir isme sahip değil, kullanılamaz
public static Number ilkEleman2(List<? extends Number> liste) {
    return liste.get(0); // Number döner (T yok)
}

Bounded type kullan:

  • Tip parametresini metodun başka yerinde kullanacaksan

  • Dönüş tipinde kullanacaksan

  • İki parametre arasında ilişki kuracaksan

Wildcard kullan:

  • Tip parametresini sadece bir yerde kullanacaksan

  • Daha esnek API istiyorsan

// İki liste aynı tip olmalı — bounded type gerekir
public static <T> boolean esitMi(List<T> a, List<T> b) { ... }

// Tip önemli değil, sadece boyut — wildcard yeterli
public static int boyut(List<?> liste) {
    return liste.size();
}

Çoklu Wildcard Örneği

// Herhangi bir Map'i yazdır
public static void mapYazdir(Map<?, ?> map) {
    for (Map.Entry<?, ?> entry : map.entrySet()) {
        System.out.println(entry.getKey() + " → " + entry.getValue());
    }
}
Map<String, Integer> yaslar = Map.of("Ali", 25, "Veli", 30);
Map<Integer, Boolean> sonuclar = Map.of(1, true, 2, false);

mapYazdir(yaslar);    // ✅
mapYazdir(sonuclar);  // ✅

Recursive Bound — Kendi Kendine Referans

Bazen bir tip, kendisiyle karşılaştırılabilir olsun istersin. Bu kalıba recursive type bound denir:

// T, Comparable<T> implement etmeli — yani kendisiyle karşılaştırılabilmeli
public static <T extends Comparable<T>> T max(List<T> liste) {
    T enBuyuk = liste.get(0);
    for (T eleman : liste) {
        if (eleman.compareTo(enBuyuk) > 0) {
            enBuyuk = eleman;
        }
    }
    return enBuyuk;
}
List<Integer> sayilar = List.of(3, 1, 4, 1, 5, 9);
System.out.println(max(sayilar)); // 9

List<String> isimler = List.of("Ali", "Zeynep", "Burak");
System.out.println(max(isimler)); // Zeynep (alfabetik)

<T extends Comparable<T>> — "T, kendisiyle karşılaştırılabilir bir tip olmalı" demek. String ve Integer zaten Comparable implement eder.


Wildcard Capture

Bazen derleyici ?'nin gerçek tipini yakalamak ister. Buna wildcard capture denir:

public static void swap(List<?> liste, int i, int j) {
    // DERLEME HATASI — ? tipini set'e veremeyiz
    // liste.set(i, liste.get(j));
    
    // Çözüm: helper metod ile capture
    swapHelper(liste, i, j);
}

// Helper — derleyici ?'yi T olarak yakalar
private static <T> void swapHelper(List<T> liste, int i, int j) {
    T temp = liste.get(i);
    liste.set(i, liste.get(j));
    liste.set(j, temp);
}

Bu pattern'e capture helper denir ve Java kütüphanesinin içinde sıkça kullanılır.


Gerçek Hayat Örneği — Esnek API Tasarımı

Diyelim ki bir event sistemi yapıyorsun:

public interface Event { }
public class UserEvent implements Event {
    String username;
}
public class OrderEvent implements Event {
    int orderId;
}
public class EventBus {
    private Map<Class<?>, List<Consumer<? super Event>>> handlers = new HashMap<>();
    
    // PECS: handler event'i consume eder → super
    public <T extends Event> void register(
        Class<T> eventType, 
        Consumer<? super T> handler
    ) {
        // ...
    }
    
    // PECS: source event üretir → extends
    public void processAll(List<? extends Event> events) {
        for (Event e : events) {
            System.out.println("Processing: " + e);
        }
    }
}
EventBus bus = new EventBus();

List<UserEvent> userEvents = List.of(new UserEvent());
List<OrderEvent> orderEvents = List.of(new OrderEvent());

bus.processAll(userEvents);  // ✅ UserEvent extends Event
bus.processAll(orderEvents); // ✅ OrderEvent extends Event

Yaygın Hatalar

1. extends vs super Karıştırmak

// YANLIŞ — extends ile yazamazsın
public void ekle(List<? extends Number> liste) {
    liste.add(42); // DERLEME HATASI!
}

// DOĞRU — yazmak istiyorsan super kullan
public void ekle(List<? super Number> liste) {
    liste.add(42); // ✅
}

2. Wildcard'ı Return Type'da Kullanmak

// KÖTÜ — çağıran ne yapacağını bilemez
public List<?> getList() { ... }

// İYİ — belirli tip döndür
public <T> List<T> getList(Class<T> type) { ... }

⚠️ Wildcard'ı return type'da kullanmak genellikle kötü tasarımdır. Çağıran taraf dönen tipi bilemez ve cast etmek zorunda kalır — generics'in amacına ters düşer.

3. Gereksiz Wildcard Kullanmak

// GEREKSIZ — tip ilişkisi yoksa T yeter
public <T> void islem(List<T> liste) { ... }

// YETERLI — tip parametresine başka yerde ihtiyaç yoksa
public void islem(List<?> liste) { ... }

Eğer T'yi sadece bir yerde kullanıyorsan, wildcard daha temiz. Birden fazla yerde kullanıyorsan, named type parameter daha iyi.


Özet Tablosu — Ne Zaman Ne Kullanılır?

SenaryoKullanım
Okuma (produce)<? extends T>
Yazma (consume)<? super T>
Hem okuma hem yazma<T> (wildcard kullanma)
Tip farketmez<?>
Tipi sınırla (class)<T extends Sınıf>
Tipi sınırla (method)<T extends Sınıf & Interface>

Özet

  • Bounded type (<T extends X>) tip parametresini sınırlar — sadece belirli tipler kabul edilir

  • Unbounded wildcard (<?>) her tipi kabul eder ama yazma kısıtı vardır

  • Upper bounded wildcard (<? extends T>) okuma için, T ve alt sınıflarını kabul eder

  • Lower bounded wildcard (<? super T>) yazma için, T ve üst sınıflarını kabul eder

  • PECS prensibi: Producer Extends, Consumer Super — veri okuyorsan extends, yazıyorsan super

  • Wildcard capture ile ?'yi helper metod aracılığıyla adlandırabilirsin