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 eklenirBirden 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:
| Wildcard | Syntax | Anlam |
|---|---|---|
| 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>sadeceList<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 olurNeden? 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 Numberextends 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 bilmiyorNeden 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 vermezsuper 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) →
extendskullanConsumer (tüketici, veri alan) →
superkullan
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 EventYaygı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?
| Senaryo | Kullanı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 edilirUnbounded wildcard (
<?>) her tipi kabul eder ama yazma kısıtı vardırUpper bounded wildcard (
<? extends T>) okuma için, T ve alt sınıflarını kabul ederLower bounded wildcard (
<? super T>) yazma için, T ve üst sınıflarını kabul ederPECS prensibi: Producer Extends, Consumer Super — veri okuyorsan extends, yazıyorsan super
Wildcard capture ile
?'yi helper metod aracılığıyla adlandırabilirsin
AI Asistan
Sorularını yanıtlamaya hazır