Roadmap’e dön
Ders 4 · Java’nın yapı taşları

Wildcards, PECS ve Type Erasure

Generic tiplerin neden invariant olduğunu, extends ve super ile güvenli API tasarımını ve compiler’ın generic bilgiyi bytecode’a nasıl dönüştürdüğünü öğren.

Java Core2 Eylül 2026~24 dk okuma
Wildcard

Generic tipin kabul edebileceği tip ailesini ifade eder.

PECS

Hangi wildcard’ın nerede kullanılacağını söyler.

Type erasure

Generic tip bilgisinin JVM seviyesine nasıl dönüştüğünü açıklar.

01 Önce invariance problemini hatırlayalım

JAVAInteger number = 10;
Number value = number;

Bu geçerlidir çünkü Integer, Number’ın alt tipidir. Generic tiplerde ise:

JAVAList<Integer> integers = new ArrayList<>();

List<Number> numbers = integers;
// Compile error

İzin verilseydi tip güvenliği bozulurdu:

JAVAList<Integer> integers = new ArrayList<>();

// Varsayalım geçerli olsaydı:
List<Number> numbers = integers;
numbers.add(3.14);

// List<Integer> içine Double eklenmiş olurdu
Integer <: Number

ama:

List<Integer> </: List<Number>
Box<Integer>  </: Box<Number>
Generic tipler varsayılan olarak invariant’tır. Wildcard’lar bu katılığı kontrollü biçimde esnetir.

02 Wildcard ? nedir?

JAVAList<?> values

Soru işareti “Eleman tipi var fakat şu anda tam olarak hangi tip olduğunu bilmiyorum” anlamına gelir. Bu liste String, Integer, User veya LocalDate listesi olabilir.

JAVApublic static void printAll(List<?> values) {
    for (Object value : values) {
        System.out.println(value);
    }
}

printAll(List.of("Java", "Spring"));
printAll(List.of(10, 20, 30));
printAll(List.of(new User("Ali")));

List<Object>

JAVAList<Object> values = new ArrayList<>();

values.add("Java");
values.add(42);
values.add(new User("Ali"));

Eleman tipinin tam olarak Object olduğu bilinir; her nesne eklenebilir.

List<?>

JAVAList<?> values = new ArrayList<String>();

values.add("Java"); // Compile error
values.add(42);     // Compile error

Object value = values.get(0); // Geçerli
List<Object>

Eleman tipi kesin olarak Object.

List<?>

Eleman tipi var fakat bizim için bilinmiyor.

03 Neden List<?> içine eleman ekleyemiyoruz?

JAVApublic static void addSomething(List<?> values) {
    values.add("Java");
    // Compile error
}

Bu metot bir Integer listesiyle çağrılabilir:

JAVAList<Integer> numbers = new ArrayList<>();
addSomething(numbers);

Metot “Java” ekleyebilseydi List<Integer> bozulurdu. Compiler gerçek tipi bilmediği için hiçbir non-null değerin güvenli olduğunu garanti edemez. Teknik istisna null’dır ancak pratik bir çözüm değildir.

JAVAvalues.add(null); // Teknik olarak mümkün

values.clear();
values.remove(0); // Tipten bağımsız işlemler mümkün olabilir
Doğru ifade “read-only liste” değil; bilinmeyen tipteki listeye güvenli biçimde yeni bir non-null eleman ekleyemeyiz.

04 Upper-bounded wildcard: ? extends

JAVAList<? extends Number>

Eleman tipi Number veya Number’ın bilinmeyen bir alt tipidir. List<Number>, List<Integer>, List<Double> ve List<Long> kabul edilir.

JAVApublic static double sum(
        List<? extends Number> numbers
) {
    double total = 0;

    for (Number number : numbers) {
        total += number.doubleValue();
    }

    return total;
}

double first = sum(List.of(1, 2, 3));
double second = sum(List.of(1.5, 2.5));
double third = sum(List.of(1L, 2L));

Parametre List<Number> olsaydı List<Integer> gönderilemezdi. extends Number, Number ailesindeki listeleri kabul eder.

Extends listesinden ne okuyabiliriz?

JAVAList<? extends Number> numbers = List.of(1, 2, 3);

Number number = numbers.get(0); // Güvenli
Object value = numbers.get(0);  // Güvenli

Integer integer = numbers.get(0);
// Compile error: Gerçek liste Double olabilir

Extends listesine neden eleman ekleyemiyoruz?

JAVAList<? extends Number> numbers;

numbers.add(10);   // Gerçek liste Double olabilir
numbers.add(3.14); // Gerçek liste Integer olabilir
// İkisi de compile error
? extends Number: Okurken Number olarak güvenli; yazarken gerçek alt tip bilinmediği için güvenli değildir.

05 Lower-bounded wildcard: ? super

JAVAList<? super Integer>

Eleman tipi Integer veya Integer’ın bilinmeyen bir üst tipidir. List<Integer>, List<Number> ve List<Object> kabul edilir.

JAVApublic static void addNumbers(
        List<? super Integer> destination
) {
    destination.add(10);
    destination.add(20);
}

List<Integer> integers = new ArrayList<>();
addNumbers(integers);

List<Number> numbers = new ArrayList<>();
addNumbers(numbers);

List<Object> objects = new ArrayList<>();
addNumbers(objects);

Bu listelere Integer eklemek her durumda güvenlidir.

List<Integer> → Integer kabul eder
List<Number>  → Integer bir Number'dır
List<Object>  → Integer bir Object'tir

Super listesinden ne okuyabiliriz?

JAVAList<? super Integer> values =
        new ArrayList<Number>();

Integer value = values.get(0);
// Compile error: Gerçek liste Object olabilir

Object safeValue = values.get(0); // Güvenli
? super Integer: Integer yazmak güvenlidir; okurken yalnızca Object garanti edilir.

06 PECS kuralı

Producer Extends, Consumer Super. Bir yapı sana veri üretiyorsa extends; senden veri tüketiyorsa super kullan.
List<? extends Number>

Number değerleri üretir; producer.

List<? super Integer>

Integer değerlerini tüketir; consumer.

Birlikte kullanım

JAVApublic static <T> void copy(
        List<? extends T> source,
        List<? super T> destination
) {
    for (T element : source) {
        destination.add(element);
    }
}
JAVAList<Integer> source = List.of(10, 20, 30);
List<Number> destination = new ArrayList<>();

copy(source, destination);

System.out.println(destination);
// [10, 20, 30]

Source T değerleri üretir, destination T değerlerini tüketir. Standart kütüphanedeki Collections.copy benzer mantık kullanır.

07 PECS her zaman kullanılmaz

Listeyi hem okuyup hem tam olarak aynı tipte değer yazacaksan çoğunlukla wildcard yerine doğrudan type parameter kullanmalısın.

JAVApublic static <T> void replaceFirst(
        List<T> values,
        T newValue
) {
    if (!values.isEmpty()) {
        values.set(0, newValue);
    }
}
Sadece okuyorsan? extends T
Sadece yazıyorsan? super T
Hem okuyup hem yazıyorsanT veya List<T>

“Sadece” kelimesini API’nin rolü olarak düşün.

08 Wildcard mı, generic method mu?

İlişki kurmaya gerek yoksa wildcard

JAVApublic static void printAll(List<?> values) {
    for (Object value : values) {
        System.out.println(value);
    }
}

Elemanın gerçek tipini isimlendirmemize gerek yoktur.

Girdi ve çıktı arasında ilişki varsa generic method

JAVApublic static <T> T first(List<T> values) {
    return values.get(0);
}

String value = first(names); // Cast gerekmez

Object first(List<?>) yazsaydık spesifik tip bilgisi kaybolur ve cast gerekirdi.

Tipi yalnızca kabul etmek istiyorsan wildcard; farklı noktalar arasında aynı tipi ilişkilendirmek istiyorsan type parameter kullan.

09 Wildcard capture

JAVApublic static void swap(
        List<?> list,
        int first,
        int second
) {
    Object temp = list.get(first);

    list.set(first, list.get(second));
    list.set(second, temp); // Compile error
}

Temp Object olarak tutulur ancak liste List<String> olabilir. Listedeki iki elemanın gerçek tipi aynıdır fakat soru işareti yüzünden bu bilinmeyen tipi isimlendiremeyiz. Çözüm helper metottur.

JAVApublic static void swap(
        List<?> list,
        int first,
        int second
) {
    swapHelper(list, first, second);
}

private static <T> void swapHelper(
        List<T> list,
        int first,
        int second
) {
    T temp = list.get(first);

    list.set(first, list.get(second));
    list.set(second, temp);
}

Compiler, ? ile temsil edilen bilinmeyen tipi helper çağrısında geçici bir T olarak yakalar.

10 Daha gelişmiş bir bound örneği

<T extends Comparable<T>> yerine üretim kodunda daha esnek biçim sıkça kullanılır:

JAVA<T extends Comparable<? super T>>
JAVApublic static
<T extends Comparable<? super T>>
T max(List<? extends T> values) {
    if (values.isEmpty()) {
        throw new IllegalArgumentException(
                "Liste boş olamaz"
        );
    }

    T max = values.get(0);

    for (T value : values) {
        if (value.compareTo(max) > 0) {
            max = value;
        }
    }

    return max;
}
T extends Comparable<? super T>

T kendisiyle veya üst tipiyle karşılaştırılabilir.

List<? extends T>

Liste T değerleri üretir.

11 Type erasure nedir?

Java generics büyük ölçüde compile-time özelliğidir. Compiler:

  1. Generic tipleri kontrol eder.
  2. Gerekli cast’leri ekler.
  3. Type parameter’ları üst sınırlarıyla değiştirir.
  4. JVM’e uygun normal bytecode üretir.
JAVApublic class Box<T> {
    private T value;

    public T get() {
        return value;
    }

    public void set(T value) {
        this.value = value;
    }
}

Erasure sonrasında kavramsal olarak:

JAVApublic class Box {
    private Object value;

    public Object get() {
        return value;
    }

    public void set(Object value) {
        this.value = value;
    }
}
JAVABox<String> box = new Box<>();
String value = box.get();

// Kavramsal olarak compiler'ın ürettiği:
Box box = new Box();
String value = (String) box.get();

Cast’i biz yazmayız; compiler gerekli yerde ekler.

12 Bound varsa neye erase edilir?

Boundsuz type parameter genellikle Object’e erase edilir:

JAVAclass Box<T> {
    T value;
}

// Kavramsal sonuç:
class Box {
    Object value;
}

Bound varsa ilk bound kullanılır:

JAVAclass NumberBox<T extends Number> {
    T value;
}

// Kavramsal sonuç:
class NumberBox {
    Number value;
}

Multiple bound’da da ilk bound erasure için kullanılır:

JAVA<T extends Number
        & Comparable<T>
        & Serializable>

// Erasure: Number

Class bound varsa ilk sırada yazılmasının nedenlerinden biri budur.

13 Runtime’da ayrı generic sınıflar oluşmaz

JAVAList<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

System.out.println(
        strings.getClass() == integers.getClass()
);
// true
Compile-time:
ArrayList<String>
ArrayList<Integer>

Runtime class:
ArrayList

Runtime nesnesi “String listesi” etiketi taşımaz. Sınıf ve metot bildirimlerindeki generic metadata reflection ile kısmen incelenebilir; ancak sıradan ArrayList nesnesi somut tip argümanını taşımaz.

14 Type erasure’ın getirdiği kısıtlamalar

new T() kullanılamaz

JAVApublic static <T> T create() {
    return new T(); // Compile error
}

Çözüm olarak factory verilebilir:

JAVApublic static <T> T create(
        Supplier<T> supplier
) {
    return supplier.get();
}

User user = create(User::new);

T.class kullanılamaz

JAVApublic static <T> Class<T> type() {
    return T.class; // Compile error
}

Gerekliyse Class<T> dışarıdan alınır:

JAVApublic static <T> T create(
        Class<T> type
) throws ReflectiveOperationException {
    return type.getDeclaredConstructor()
               .newInstance();
}

instanceof T kullanılamaz

JAVAif (value instanceof T) { }            // Compile error
if (value instanceof List<String>) { } // Compile error

if (value instanceof List<?>) {
    // Runtime'da List olduğu bilinebilir
}

new T[] kullanılamaz

JAVAT[] values = new T[10];
// Compile error

15 Neden generic array oluşturulamıyor?

Java array’leri runtime’da eleman tipini bilir:

JAVAString[] names = new String[2];

Object[] objects = names;
objects[0] = 42;
// Runtime'da ArrayStoreException

String array’i runtime’da kendi tipini bilir. Generics ise erased’dır; List<String> ve List<Integer> runtime’da yalnızca List’tir.

JAVAnew List<String>[10];
// Compile error
Array’in reified tip modeli ile generics’in erased modeli uyumsuz olduğu için generic array oluşturmaya izin verilmez.

16 Aynı erasure’a sahip metotlar overload edilemez

JAVApublic void process(List<String> values) { }

public void process(List<Integer> values) { }
// Name clash

Erasure sonrasında iki imza da aynı olur:

JAVApublic void process(List values) { }

Çözüm isimleri ayırmak veya ortak davranış için generic method yazmaktır:

JAVAprocessStrings(List<String> values)
processIntegers(List<Integer> values)

public <T> void process(List<T> values) { }

17 Bridge methods

Type erasure bazen polymorphism’i korumak için compiler’ın sentetik bir metot üretmesini gerektirir.

JAVApublic final class User
        implements Comparable<User> {

    private final String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public int compareTo(User other) {
        return name.compareTo(other.name);
    }
}

Comparable içindeki compareTo(T), erasure sonrasında compareTo(Object) olur. Bizim metodumuz compareTo(User) olduğundan compiler kavramsal olarak bir bridge method üretir:

JAVApublic int compareTo(Object other) {
    return compareTo((User) other);
}

Kaynak kodda görünmez; bytecode’da synthetic bridge olarak bulunabilir.

18 Heap pollution

Generic sistemin söylediği tip ile runtime’daki gerçek içerik uyuşmazsa heap pollution oluşur. En yaygın neden raw type’tır.

JAVAList<String> strings = new ArrayList<>();

List raw = strings;
raw.add(42); // Unchecked warning

String value = strings.get(0);
// ClassCastException

Compiler get sonucuna otomatik String cast’i eklemiştir. Ancak raw type üzerinden listeye Integer konmuştur.

unchecked assignment
unchecked cast
unchecked invocation

→ Generic tip güvenliğinin delindiği
  bir nokta olabilir.
@SuppressWarnings("unchecked") problemi çözmez, yalnızca uyarıyı gizler. Güvenliği başka şekilde kanıtlayabildiğin dar bir alanda kullanılmalıdır.

19 API tasarlarken hangi imzayı seçmelisin?

Her tür listeyi yazdırvoid print(List<?> values)
Sayısal listeden okudouble sum(List<? extends Number> values)
Integer değerlerini hedefe yazvoid addDefaults(List<? super Integer> destination)
Aynı tipte sonuç döndür<T> T first(List<T> values)

Kaynak ve hedef arasında aktarım

JAVA<T> void copy(
        List<? extends T> source,
        List<? super T> destination
)

Hem oku hem aynı tipte değer yaz

JAVA<T> void replace(
        List<T> values,
        T oldValue,
        T newValue
)

Gereğinden katı Number listesi

sum(List<Number>)

Gereğinden katı; Integer listesi kabul etmez.

sum(List<? extends Number>)

Number ailesini kabul eden esnek imza.

Akılda kalacak özet

Okuma ve yazma yönünü imzaya taşı

List<T>

Tip tam olarak T.

List<?>

Tip bilinmiyor; Object okunabilir, güvenli değer eklenemez.

List<? extends T>

T olarak okunur; producer.

List<? super T>

T eklenir; consumer.

PECS

Producer Extends, Consumer Super.

Type erasure

Boundsuz T Object’e, bounded T ilk bound’a dönüşür.

extends bir kaynaktan güvenli okumayı, superbir hedefe güvenli yazmayı sağlar; type erasure ise bu güvenliğin büyük ölçüde compiler tarafından kurulduğunu açıklar.
Pratik çalışması

Wildcard’dan heap pollution’a ilerle

  1. printAll(List<?>)
  2. sum(List<? extends Number>)
  3. addIntegers(List<? super Integer>)
  4. copy(List<? extends T>, List<? super T>)
  5. Wildcard capture kullanan swap(List<?>)
  6. Raw type ile bilinçli heap pollution örneği
  7. Erasure kaynaklı derleme hatalarını açıklama
JAVAnew T();
T.class;
value instanceof T;
new T[10];

void process(List<String> values);
void process(List<Integer> values);