Ders 09 / 13
Genel Tipler
Tip parametresi sınıf dosyasına iki ayrı yerde yazılır: sanal makinenin okuduğu tanımda silinmiştir, imza özniteliğinde durur. Ölçüm bunun sonucunu gösterir — denetim yalnız derleme zamanındadır, iki genel tip çalışma zamanında aynı sınıftır ve ham tiple yapılan yazma okuyan yerde düşer.
İçindekiler
Önceki ders bir kayıt için derleyicinin ürettiği üyeleri saydı: erişimciler, eşitlik, karma ve metne çevirme kaynakta yazılmadan sınıf dosyasına girdi. Derleyici üye üretebiliyorsa, elindeki tip bilgisiyle ne yapıyor — onu da bir yere yazıyor mu, yoksa kullanıp bırakıyor mu?
Genellikler (generics) TypeScript kursunda tip parametreleri ve kısıtlarla, ifade gücü
açısından kuruldu; bu ders onu tekrarlamaz. Buradaki soru kursun sorusudur: Depo<Ust>
yazımındaki Ust bilgisiyle kararı kim ve ne zaman veriyor? Yanıt bu konuda uçtadır —
kararın tamamı bildirilen tiptedir ve çalışma zamanı tipinin söyleyecek hiçbir şeyi yoktur.
Ölçüm bunun üç sonucunu gösterecek: iki ayrı tip parametresiyle yazılmış iki kap çalışma
zamanında ayırt edilebiliyor mu, denetim nereye konuyor, ve denetimi yapan taraf ortadan
kalktığında yanlış yazmanın bedelini kim ödüyor?
Tip Parametresi Sınıf Dosyasına İki Kez Yazılır
- GN1 — Ölçülen kaynak metni dersin içinde yazılır. Ne yazdığımızı bildiğimiz için, sınıf dosyasında bulunan her fark derleyicinin kararıdır.
- GN2 —
Olcekçekirdeği ortak tanımdan gelir. Bu ders ona tek bir ölçüm ekler: bir öğenin imza özniteliğini döndürenimza. Var olan ölçümlerin davranışı değişmez. - GN3 — Tanımlayıcı ve imza metinleri sınıf dosyasındaki ham biçimleriyle basılır; kısaltılmaz ve çevrilmez.
// Olcek.java — ortak ölçüm çekirdeği: sınıf dosyasını derleyip okur
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Olcek {
static ClassModel derle(String kaynak, String sinif) throws Exception {
Path d = Files.createTempDirectory("olcek");
Path k = d.resolve(sinif + ".java");
Files.writeString(k, kaynak);
PrintWriter bos = new PrintWriter(Writer.nullWriter());
if (ToolProvider.findFirst("javac").orElseThrow()
.run(bos, bos, "-d", d.toString(), k.toString()) != 0)
throw new IllegalStateException("derlenmedi: " + sinif);
return ClassFile.of().parse(d.resolve(sinif + ".class"));
}
static List<String> komutlar(String kaynak, String sinif, String yontem) throws Exception {
List<String> adlar = new ArrayList<>();
for (MethodModel m : derle(kaynak, sinif).methods())
if (m.methodName().stringValue().equals(yontem))
for (CodeElement e : m.code().orElseThrow())
if (e instanceof Instruction i)
adlar.add(i.opcode().name().toLowerCase(Locale.ROOT));
return adlar;
}
static String imza(AttributedElement e) {
return e.findAttribute(Attributes.signature())
.map(a -> a.signature().stringValue()).orElse("-");
}
}
// Imza.java — tip parametresi sınıf dosyasında nerede duruyor
import java.lang.classfile.*;
public class Imza {
static final String DEPO = """
class Depo<T> {
private T oge;
void koy(T oge) { this.oge = oge; }
T al() { return oge; }
}
""";
public static void main(String[] args) throws Exception {
ClassModel cm = Olcek.derle(DEPO, "Depo");
System.out.printf("%-14s %-26s %s%n", "uye", "sanal makinenin gordugu", "imza ozniteligi");
System.out.printf("%-14s %-26s %s%n", "Depo sinifi", "-", Olcek.imza(cm));
for (FieldModel f : cm.fields())
System.out.printf("%-14s %-26s %s%n", "alan " + f.fieldName().stringValue(),
f.fieldType().stringValue(), Olcek.imza(f));
for (MethodModel m : cm.methods())
System.out.printf("%-14s %-26s %s%n", "yontem " + m.methodName().stringValue(),
m.methodType().stringValue(), Olcek.imza(m));
}
}
uye sanal makinenin gordugu imza ozniteligi Depo sinifi - <T:Ljava/lang/Object;>Ljava/lang/Object; alan oge Ljava/lang/Object; TT; yontem <init> ()V - yontem koy (Ljava/lang/Object;)V (TT;)V yontem al ()Ljava/lang/Object; ()TT;
Orta sütun tip silmedir (type erasure). Kaynakta T yazan her yer sınıf dosyasının
tanımlayıcısında Object olmuştur: alan Ljava/lang/Object;, koy yöntemi bir Object
alır, al yöntemi bir Object döndürür. Sanal makine bir yöntemi çağırırken bu sütuna
bakar; onun için Depo tek parametreli sıradan bir sınıftır ve T diye bir şey yoktur.
Sağ sütun ise silinmemiştir. Sınıfın imzası T’nin var olduğunu ve üst sınırının
Object olduğunu yazıyor; alanın imzası TT;, yani “tip değişkeni T”; koy ile al
imzalarında da T adıyla duruyor. Kaynakta yazılan bilgi kaybolmadı, ikinci bir yere
yazıldı. Yapıcının imzası yok, çünkü yapıcının tanımında tip parametresi geçmiyor.
Buradan kursun sorusunun bu konudaki yanıtı çıkıyor. Sağ sütunu yalnız derleyici okur; sol sütunu sanal makine yürütür. Tip parametresiyle ilgili her karar, çalışma zamanı tipinin hiç bakmadığı bir metne dayanır.
Denetim Okuyan Yere Konur, Yazana Konmaz
- GN4 — Aynı yazma üç ayrı kaynakta denenir; ikisi derlenir, biri derlenmez. Derleyicinin ileti metni basılmaz, yalnız derlenip derlenmediği bildirilir.
// Denetim.java — genel çağrı sınıf dosyasında hangi komutu bırakıyor
public class Denetim {
static final String TIPLER = """
class Ust { String ad() { return "ust yontem"; } }
class Alt extends Ust { @Override String ad() { return "alt yontem"; } }
class Depo<T> { private T oge; void koy(T o) { oge = o; } T al() { return oge; } }
""";
static final String KULLANIM = TIPLER + """
class Kullanim {
static Ust oku(Depo<Ust> d) { return d.al(); }
@SuppressWarnings("rawtypes")
static Object hamOku(Depo d) { return d.al(); }
static void yaz(Depo<Ust> d, Alt a) { d.koy(a); }
}
""";
static final String YAZMA =
TIPLER + "class Yazma { static void y(%s d) { d.koy(new Alt()); } }";
static void dene(String ad, String kaynak) {
try {
Olcek.derle(kaynak, "Yazma");
System.out.printf("%-30s -> derlendi%n", ad);
} catch (Exception e) {
System.out.printf("%-30s -> %s%n", ad, e.getMessage());
}
}
public static void main(String[] args) throws Exception {
System.out.println("okuma Depo<Ust> : " + Olcek.komutlar(KULLANIM, "Kullanim", "oku"));
System.out.println("okuma ham Depo : " + Olcek.komutlar(KULLANIM, "Kullanim", "hamOku"));
System.out.println("yazma Depo<Ust> : " + Olcek.komutlar(KULLANIM, "Kullanim", "yaz"));
System.out.println();
dene("Depo<Ust> d; d.koy(Alt)", YAZMA.formatted("Depo<Ust>"));
dene("Depo<String> d; d.koy(Alt)", YAZMA.formatted("Depo<String>"));
dene("Depo d (ham); d.koy(Alt)", YAZMA.formatted("Depo"));
}
}
okuma Depo<Ust> : [aload_0, invokevirtual, checkcast, areturn] okuma ham Depo : [aload_0, invokevirtual, areturn] yazma Depo<Ust> : [aload_0, aload_1, invokevirtual, return] Depo<Ust> d; d.koy(Alt) -> derlendi Depo<String> d; d.koy(Alt) -> derlenmedi: Yazma Depo d (ham); d.koy(Alt) -> derlendi
İlk iki satır tek bir komutla ayrılıyor. Depo<Ust> üzerinden yapılan okuma dört
komuttur ve dördüncüsü checkcast; ham tip üzerinden yapılan aynı okuma üç komuttur ve
denetim yoktur. Çağrılan yöntem ikisinde de aynı yöntemdir — sınıf dosyasında Object
döndüren tek bir al vardır. Farkı yapan şey çağrılan taraf değil, çağıran taraftır:
derleyici okunan değeri bildirilen tipe uydurmak için bir denetim koyar.
Üçüncü satır dersin en sessiz ölçümüdür. Yazma üç komuttur ve içinde hiçbir denetim
yoktur: referansı yükle, değeri yükle, çağır. Okuma yerine denetim konur, yazma yerine
konmaz. Bunun nedeni alt tablodadır: yazma zaten derleme zamanında denetlenmiştir.
Depo<String> olarak adlandırılmış bir depoya Alt yazan kaynak derlenmiyor. Karar
tümüyle bildirilen tipten geliyor ve derleme durduğu için çalışma zamanına hiçbir iş
kalmıyor.
Alt tablonun üçüncü satırı ise ödenen bedeli yazıyor. Aynı depo ham tiple (raw type) adlandırıldığında aynı yazma derleniyor. Derleyici burada bir uyarı üretir, ama uyarı derlemeyi durdurmaz; bildirilen tip ortadan kalkınca denetleyecek taraf da kalmaz.
Kısıt Silinen Tipi Değiştirir
- GN5 — Beş denemenin hepsi aynı kısıtlı sınıf üzerine kurulur; yalnız denenen satır değişir, çevresindeki tipler sabit kalır.
// Sinir.java — kısıt silinen tipi değiştirir
import java.lang.classfile.*;
public class Sinir {
static final String SINIRLI = """
class Ust { String ad() { return "ust yontem"; } }
class Alt extends Ust { @Override String ad() { return "alt yontem"; } }
class Sinirli<T extends Ust> {
private T oge;
void koy(T oge) { this.oge = oge; }
T al() { return oge; }
}
""";
static final String OKUMA = SINIRLI + """
class Okuma {
static Ust ustOku(Sinirli<Ust> d) { return d.al(); }
static Alt altOku(Sinirli<Alt> d) { return d.al(); }
}
""";
static void dene(String ad, String bildirim, String govde) {
try {
Olcek.derle(SINIRLI + "class Yasak" + bildirim + " { " + govde + " }", "Yasak");
System.out.printf("%-38s -> derlendi%n", ad);
} catch (Exception e) {
System.out.printf("%-38s -> %s%n", ad, e.getMessage());
}
}
public static void main(String[] args) throws Exception {
ClassModel cm = Olcek.derle(SINIRLI, "Sinirli");
System.out.printf("%-14s %-26s %s%n", "uye", "sanal makinenin gordugu", "imza ozniteligi");
System.out.printf("%-14s %-26s %s%n", "Sinirli sinifi", "-", Olcek.imza(cm));
for (FieldModel f : cm.fields())
System.out.printf("%-14s %-26s %s%n", "alan " + f.fieldName().stringValue(),
f.fieldType().stringValue(), Olcek.imza(f));
for (MethodModel m : cm.methods())
if (!m.methodName().stringValue().equals("<init>"))
System.out.printf("%-14s %-26s %s%n", "yontem " + m.methodName().stringValue(),
m.methodType().stringValue(), Olcek.imza(m));
System.out.println();
System.out.println("Sinirli<Ust> okuma : " + Olcek.komutlar(OKUMA, "Okuma", "ustOku"));
System.out.println("Sinirli<Alt> okuma : " + Olcek.komutlar(OKUMA, "Okuma", "altOku"));
System.out.println();
dene("<T> icinde o.ad()", "<T>", "String s(T o) { return o.ad(); }");
dene("<T extends Ust> icinde o.ad()", "<T extends Ust>", "String s(T o) { return o.ad(); }");
dene("new T[3]", "<T extends Ust>", "Object[] d() { return new T[3]; }");
dene("x instanceof Sinirli<Ust>", "",
"boolean t(Object x) { return x instanceof Sinirli<Ust>; }");
dene("x instanceof Sinirli<?>", "",
"boolean t(Object x) { return x instanceof Sinirli<?>; }");
}
}
uye sanal makinenin gordugu imza ozniteligi Sinirli sinifi - <T:LUst;>Ljava/lang/Object; alan oge LUst; TT; yontem koy (LUst;)V (TT;)V yontem al ()LUst; ()TT; Sinirli<Ust> okuma : [aload_0, invokevirtual, areturn] Sinirli<Alt> okuma : [aload_0, invokevirtual, checkcast, areturn] <T> icinde o.ad() -> derlenmedi: Yasak <T extends Ust> icinde o.ad() -> derlendi new T[3] -> derlenmedi: Yasak x instanceof Sinirli<Ust> -> derlenmedi: Yasak x instanceof Sinirli<?> -> derlendi
Tanımlayıcı sütunu değişti: kısıtlı bir tip parametresi Object’e değil, sınırına
siliniyor. Alan LUst;, koy bir Ust alıyor, al bir Ust döndürüyor. İmza sütunu ise
aynı kaldı — orada hâlâ TT; yazıyor ve sınıf imzası sınırı da taşıyor.
Bunun ilk sonucu alt tablonun ilk iki satırında. Sınırsız bir tip parametresi üzerinden
o.ad() çağıran kaynak derlenmiyor, çünkü silinen tip Object’tir ve Object üzerinde
böyle bir yöntem yoktur. Aynı satır sınır konduğunda derleniyor. Kısıt bir belgelendirme
değildir; sınıf dosyasındaki tanımlayıcıyı değiştirir ve çağrının bağlanabileceği yöntem
kümesini genişletir.
İkinci sonuç okuma satırlarında ve önceki bölümün ölçümünü tamamlıyor. Sinirli<Ust>
üzerinden yapılan okumada checkcast yok, Sinirli<Alt> üzerinden yapılanda var.
Denetim her genel okumaya eklenmez; yalnız bildirilen tip ile silinen tip ayrıştığında
eklenir. İlk ölçümde Depo<T> Object’e siliniyordu ve Ust okuması ayrışıyordu; burada
silinen tip zaten Ust olduğu için eklenecek bir şey kalmıyor.
Son üç satır silmenin doğrudan yasaklarıdır. new T[3] derlenmiyor: bir dizi öğe tipini
kendi içinde taşır ve her yazmada onu denetler — Java Temelleri kursunda ölçülmüştü — ama
çalışma zamanında T diye bir tip bulunmadığı için taşınacak bir şey yoktur. x instanceof Sinirli<Ust> de derlenmiyor, oysa x instanceof Sinirli<?> derleniyor. Sanal makineye
sorulabilecek tek soru ham sınıftır; joker karakter (wildcard) tam olarak “tip parametresini
sorma” demektir.
İki Genel Tip Çalışma Zamanında Aynı Sınıftır
- GN6 — Kimlik yalnız
==ile sorulur, hiçbir kimlik sayısı basılmaz. - GN7 — Ham tipli yazmanın sonucu iddia olarak yazılmaz,
instanceofile ölçülür; yakalanan istisnadan yalnız sınıf adı basılır.
// Silme.java — tip silme çalışma zamanında ne bırakıyor
import java.util.Arrays;
class Ust {
String ad() { return "ust yontem"; }
}
class Alt extends Ust {
@Override String ad() { return "alt yontem"; }
}
class Depo<T> {
private T oge;
void koy(T oge) { this.oge = oge; }
T al() { return oge; }
}
public class Silme {
static Depo<Ust> alan;
@SuppressWarnings({"unchecked", "rawtypes"})
public static void main(String[] args) throws Exception {
Depo<Ust> tasiyicilar = new Depo<>();
Depo<String> metinler = new Depo<>();
System.out.println("Depo<Ust> ile Depo<String> ayni sinif mi : "
+ (tasiyicilar.getClass() == metinler.getClass()));
System.out.println("calisma zamaninda tip parametreleri : "
+ Arrays.toString(Depo.class.getTypeParameters()));
tasiyicilar.koy(new Alt());
System.out.println("okunan ogenin calisma zamani tipi : "
+ tasiyicilar.al().getClass().getSimpleName());
Depo ham = tasiyicilar;
ham.koy("kuzey");
System.out.println("ham tiple yazma durduruldu mu : "
+ (tasiyicilar.al() instanceof Ust ? "evet" : "hayir"));
try {
Ust u = tasiyicilar.al();
System.out.println("okuma sonucu : " + u.ad());
} catch (ClassCastException e) {
System.out.println("hata yazan yerde degil, OKUYAN yerde : "
+ e.getClass().getSimpleName());
}
System.out.println("alanin imza ozniteliginden okunan tipi : "
+ Silme.class.getDeclaredField("alan").getGenericType());
}
}
Depo<Ust> ile Depo<String> ayni sinif mi : true calisma zamaninda tip parametreleri : [T] okunan ogenin calisma zamani tipi : Alt ham tiple yazma durduruldu mu : hayir hata yazan yerde degil, OKUYAN yerde : ClassCastException alanin imza ozniteliginden okunan tipi : Depo<Ust>
İlk iki satır tanımlayıcı sütununun sonucudur. Depo<Ust> ile Depo<String> çalışma
zamanında aynı sınıftır ve o sınıfın tip parametresi yalnız T adıyla, bir değeri
olmadan durur. Kaynakta iki ayrı tip yazılmıştı; öbekte tek bir sınıf var.
Üçüncü satır ayrımı keskinleştiriyor. Depodan okunan nesnenin çalışma zamanı tipi Alt —
yani nesnenin kendi sınıfı bilinir ve gizli değildir. Bilinmeyen şey kabın ne tuttuğudur.
Nesnenin tipi çalışma zamanında vardır, kabın tip parametresi yoktur.
Dördüncü ve beşinci satırlar bedeli ölçüyor. Ham tiple yazılan dizgi hiçbir yerde
durdurulmadı: depo onu kabul etti ve depodaki öğe artık bir Ust değil. Hata, yazmanın
yapıldığı satırda değil, okumanın yapıldığı satırda düştü — çünkü tek denetim oradaki
checkcast komutudur. Yanlış tarafa bakan bir programcının ödediği bedel budur: kusuru
üreten satır ile kusurun göründüğü satır ayrı yerlerdedir, ve aradaki uzaklık programın
uzunluğu kadar olabilir.
Silinen Bilgi Değil, Denetimdir
Son satır dersin sınırlayıcı ölçümüdür ve tezi tersine çeviriyor. Depo<Ust> diye
bildirilmiş bir alanın genel tipi çalışma zamanında okunabiliyor: Depo<Ust>. Bilgi
silinmemiştir, imza özniteliğinde durmaktadır ve oradan geri alınabilir.
O halde silinen nedir? İlk ölçümün iki sütunu yanıtı veriyor. Sanal makinenin yürüttüğü
tanımlayıcıda Object yazar; sanal makine bir çağrıyı denetlerken imza özniteliğine
bakmaz. Tip bilgisi sınıf dosyasında veri olarak durur, ama denetim olarak
kullanılmaz. Bu yüzden “tip silme” adı, bilginin değil, o bilgiye dayanan denetimin
kaybolmasını anlatır. Bir alanın imzasını okumakla bir yazmanın engellenmesi ayrı şeylerdir:
birincisi bir metni geri okumaktır, ikincisi ise çalışma zamanında hiç yapılmayan bir iştir.
Özet
- Tip parametresi sınıf dosyasına iki ayrı yere yazılır: sanal makinenin okuduğu
tanımlayıcıda
Objectolarak silinmiş, imza özniteliğindeTadıyla yazılı durur. - Genel bir okuma çağrısı sınıf dosyasında bir
checkcastkomutu ekler; aynı okuma ham tip üzerinden yapıldığında o komut yoktur. Denetim çağrılan tarafta değil, çağıran tarafta bulunur. - Genel bir yazma çağrısı hiçbir denetim komutu bırakmaz, çünkü yazma derleme zamanında
denetlenmiştir:
Depo<String>deposunaAltyazan kaynak derlenmez. - Kısıtlı bir tip parametresi
Object’e değil sınırına silinir; bu hemTüzerinden yöntem çağrılmasını açar hem de bildirilen tiple silinen tip çakıştığındacheckcastkomutunu ortadan kaldırır. - Çalışma zamanında
Depo<Ust>ileDepo<String>aynı sınıftır; ham tiple yapılan yazma durdurulmaz ve hata yazan yerde değil, okuyan yerde düşer. - Sınırlayıcı ölçüm: alanın genel tipi çalışma zamanında imza özniteliğinden geri okunabilir. Silinen şey bilgi değil, o bilgiye dayanan denetimdir.
Sonraki Adım
Bu derste bir depo tek bir tiple adlandırıldı ve o tipe yazmakla o tipten okumak arasında
denetim bakımından bir fark çıktı: okumaya bir komut eklendi, yazmaya eklenmedi. Aynı fark
tip düzeyinde de sorulabilir. Depo<Ust> yerine “Ust ya da altındaki herhangi bir tipi
tutan depo” yazmak istersek, o depodan okumak ile ona yazmak aynı ölçüde açık kalır mı?
Sıradaki ders dört ayrı kap biçimini iki işlemle deneyip kaç çiftin derlendiğini sayar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.