Generic tipin kabul edebileceği tip ailesini ifade eder.
Hangi wildcard’ın nerede kullanılacağını söyler.
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ş olurduInteger <: Number ama: List<Integer> </: List<Number> Box<Integer> </: Box<Number>
02 Wildcard ? nedir?
JAVAList<?> valuesSoru 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çerliList<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 olabilir04 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 olabilirExtends 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ı
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);
}
}? extends T? super TT 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 gerekmezObject first(List<?>) yazsaydık spesifik tip bilgisi kaybolur ve cast gerekirdi.
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:
- Generic tipleri kontrol eder.
- Gerekli cast’leri ekler.
- Type parameter’ları üst sınırlarıyla değiştirir.
- 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: NumberClass 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()
);
// trueCompile-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 error15 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 ArrayStoreExceptionString 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 error16 Aynı erasure’a sahip metotlar overload edilemez
JAVApublic void process(List<String> values) { }
public void process(List<Integer> values) { }
// Name clashErasure 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);
// ClassCastExceptionCompiler 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?
void print(List<?> values)double sum(List<? extends Number> values)void addDefaults(List<? super Integer> destination)<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.
Okuma ve yazma yönünü imzaya taşı
Tip tam olarak T.
Tip bilinmiyor; Object okunabilir, güvenli değer eklenemez.
T olarak okunur; producer.
T eklenir; consumer.
Producer Extends, Consumer Super.
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.Wildcard’dan heap pollution’a ilerle
printAll(List<?>)sum(List<? extends Number>)addIntegers(List<? super Integer>)copy(List<? extends T>, List<? super T>)- Wildcard capture kullanan
swap(List<?>) - Raw type ile bilinçli heap pollution örneği
- 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);