Ders 10 / 13
Sınırlı Joker Karakterler
Dört kap biçimi ve iki işlem sekiz çift verir; bunların yalnız dördü derlenir. Kovaryans okumayı, kontravaryans yazmayı açar ve ikisi bir arada açılmaz; sınırsız joker karakter ise yalnız boş değer yazmaya izin verir. Yön kısıtının tamamı imza özniteliğindedir ve çalışma zamanında hiçbir izi yoktur.
İçindekiler
Önceki ders bir depoyu tek bir tiple adlandırdı ve okumaya bir denetim komutu eklendiğini,
yazmaya eklenmediğini ölçtü. Yazma zaten derleme zamanında kapatılmıştı. Aynı soru tip
düzeyinde de sorulabilir: Depo<Ust> yerine “Ust ya da altındaki herhangi bir tipi tutan
depo” yazmak istersek, o depodan okumakla ona yazmak aynı ölçüde açık kalır mı?
Bu ders dört kap biçimini iki işlemle deneyip kaç çiftin derlendiğini sayar. Kursun sorusu burada da geçerlidir, ama yanıtın tarafı önceden bellidir: joker karakterin koyduğu kısıt yalnız bildirilen tipe aittir. Ölçüm hem bu kısıtın kaç kapıyı kapattığını hem de çalışma zamanında ondan geriye ne kaldığını gösterir.
Dört Kap Biçimi, İki İşlem
- GN8 — Ölçülen kaynak dersin içinde yazılır; yalnız kap biçimi ve denenen satır değişir, çevresindeki tipler her denemede aynıdır.
- GN9 —
Olcekçekirdeği ortak tanımdan gelir; önceki dersteki iki ölçüm, komut adları ile imza özniteliği, olduğu gibi kullanılır. - GN10 — Bir deneme ya derlenir ya derlenmez; derleyicinin ileti metni basılmaz, çünkü ölçülen şey iletinin içeriği değil, kapının açık olup olmadığıdır.
// 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("-");
}
}
// Yon.java — dört kap biçimi, iki işlem: kaç çift derleniyor
import java.lang.classfile.*;
public class Yon {
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 KALIP = TIPLER + "class Deneme { static void y(%s d) { %s } }";
static final String[] KAPLAR =
{"Depo<Ust>", "Depo<? extends Ust>", "Depo<? super Alt>", "Depo<?>"};
static final String OKU = "Ust u = d.al();";
static final String YAZ = "d.koy(new Alt());";
static boolean derlenirMi(String kap, String islem) {
try {
Olcek.derle(KALIP.formatted(kap, islem), "Deneme");
return true;
} catch (Exception e) {
return false;
}
}
public static void main(String[] args) throws Exception {
System.out.printf("%-22s %-18s %s%n", "kap bicimi", "Ust u = d.al()", "d.koy(new Alt())");
int gecen = 0;
for (String kap : KAPLAR) {
boolean o = derlenirMi(kap, OKU), y = derlenirMi(kap, YAZ);
gecen += (o ? 1 : 0) + (y ? 1 : 0);
System.out.printf("%-22s %-18s %s%n", kap,
o ? "derlendi" : "derlenmedi", y ? "derlendi" : "derlenmedi");
}
System.out.println("sekiz ciftin derlenen sayisi: " + gecen);
}
}
kap bicimi Ust u = d.al() d.koy(new Alt()) Depo<Ust> derlendi derlendi Depo<? extends Ust> derlendi derlenmedi Depo<? super Alt> derlenmedi derlendi Depo<?> derlenmedi derlenmedi sekiz ciftin derlenen sayisi: 4
Sekiz çiftin dördü derleniyor, dördü derlenmiyor. İlk satır ölçüt satırıdır: tam olarak adlandırılmış bir depoda iki kapı da açıktır. Sonraki üç satır kapıları teker teker kapatıyor.
Depo<? extends Ust> yazımı kovaryans (covariance) kurar: Ust’un altındaki her tipin
deposu bu ada geçer. Okuma açıktır, çünkü kapta ne varsa en azından bir Ust’tur. Yazma
kapalıdır, çünkü kabın gerçekte hangi tipin deposu olduğu bilinmez; Depo<Alt> da olabilir,
Ust’un başka bir alt tipinin deposu da. Bir Alt yazmak ikinci durumda yanlış olurdu ve
derleyici hangisi olduğunu bilmediği için ikisini birden kapatır.
Depo<? super Alt> yazımı kontravaryans (contravariance) kurar ve iki kapıyı yer
değiştirir. Yazma açıktır: kap Alt’ın bir üst tipinin deposudur ve bir Alt her üst
tipin deposuna yazılabilir. Okuma kapalıdır, çünkü kap Depo<Object> de olabilir ve okunan
değerin bir Ust olduğu güvence altında değildir.
Buradan ölçülmüş bir kural çıkıyor: kaptan okuyacaksanız üst sınır, kaba yazacaksanız alt sınır yazılır; ikisi bir arada açılmaz. Joker karakterin kazandırdığı şey, kabul edilen tip kümesinin genişlemesidir; ödettiği şey, iki işlemden birinin kapanmasıdır.
Yön Kısıtı Yalnız İmzada Durur
// Iz.java — joker karakter sınıf dosyasında iz bırakıyor mu
import java.lang.classfile.*;
public class Iz {
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 KALIP = TIPLER + "class Deneme { static void y(%s d) { %s } }";
static final String KOVARYANT = KALIP.formatted("Depo<? extends Ust>", "Ust u = d.al();");
static final String SINIRSIZ = KALIP.formatted("Depo<?>", "Object o = d.al();");
public static void main(String[] args) throws Exception {
for (MethodModel m : Olcek.derle(KOVARYANT, "Deneme").methods())
if (m.methodName().stringValue().equals("y"))
System.out.println("y tanimi : " + m.methodType().stringValue()
+ " imzasi: " + Olcek.imza(m));
System.out.println("okuma Depo<? extends Ust> : "
+ Olcek.komutlar(KOVARYANT, "Deneme", "y"));
System.out.println("okuma Depo<?> : "
+ Olcek.komutlar(SINIRSIZ, "Deneme", "y"));
}
}
y tanimi : (LDepo;)V imzasi: (LDepo<+LUst;>;)V okuma Depo<? extends Ust> : [aload_0, invokevirtual, checkcast, astore_1, return] okuma Depo<?> : [aload_0, invokevirtual, astore_1, return]
Bu ölçüm kısıtın nerede yaşadığını söylüyor. Sanal makinenin okuduğu
tanımlayıcı (LDepo;)V: parametrede ne tip parametresi var ne de joker karakter. İmza
özniteliğinde ise kısıtın tamamı yazılı — +LUst; işaretindeki artı, üst sınırlı joker
karakteri gösteriyor. Önceki dersin ayrımı burada bir kez daha çıkıyor: bilgi sınıf
dosyasında durur, ama sanal makine ona bakmaz.
Komut satırları da aynı yöne bakıyor. Kovaryant görünümden yapılan okuma bir checkcast
komutu taşıyor, çünkü bildirilen tip Ust ile silinen tip Object ayrışıyor. Sınırsız
görünümden Object olarak yapılan okumada ise denetim yok. Yön kısıtına ait tek bir komut
bulunmuyor; sınıf dosyasında “buraya yazılamaz” diyen bir işaret yoktur, çünkü o karar
derlemede verilmiş ve orada bitmiştir.
Sınırsız Joker Karakter Yalnız Boş Değer Kabul Eder
- GN11 — İkinci tabloda okuma
Objectolarak yapılır ve yazılan değer boş değerdir; kap biçimleri birinci tablodakiyle aynıdır.
// Bos.java — sınırsız joker karakter neye izin veriyor
public class Bos {
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 KALIP = TIPLER + "class Deneme { static void y(%s d) { %s } }";
static final String[] KAPLAR =
{"Depo<Ust>", "Depo<? extends Ust>", "Depo<? super Alt>", "Depo<?>"};
static String dene(String kap, String islem) {
try {
Olcek.derle(KALIP.formatted(kap, islem), "Deneme");
return "derlendi";
} catch (Exception e) {
return "derlenmedi";
}
}
public static void main(String[] args) {
System.out.printf("%-24s %-18s %s%n", "kap bicimi", "Object o = d.al()", "d.koy(null)");
for (String kap : KAPLAR)
System.out.printf("%-24s %-18s %s%n", kap,
dene(kap, "Object o = d.al();"), dene(kap, "d.koy(null);"));
System.out.println();
System.out.printf("%-24s %s%n", "Depo<?> d.koy(d.al())", dene("Depo<?>", "d.koy(d.al());"));
System.out.printf("%-24s %s%n", "Depo<Ust> d.koy(d.al())", dene("Depo<Ust>", "d.koy(d.al());"));
}
}
kap bicimi Object o = d.al() d.koy(null) Depo<Ust> derlendi derlendi Depo<? extends Ust> derlendi derlendi Depo<? super Alt> derlendi derlendi Depo<?> derlendi derlendi Depo<?> d.koy(d.al()) derlenmedi Depo<Ust> d.koy(d.al()) derlendi
İkinci tabloda sekiz çiftin sekizi derleniyor, ve bu dersin sınırlayıcı ölçümüdür. İlk tablodaki dört kapalı kapının hiçbiri gerçekte kilitli değildi; ikisi yalnız daraltılmış, ikisi de yalnız belirli bir değer için kapanmıştı.
Okuma hiçbir biçimde kapanmaz. Depo<? super Alt> ve Depo<?> üzerinden okuma ilk tabloda
düşmüştü, çünkü sonuç bir Ust’a atanıyordu. Object olarak okunduğunda dördü de
derleniyor. Sınırsız joker karakter okumayı yasaklamaz, okunan değerin bildirilen tipini
Object’e indirir.
Yazma da tümüyle kapanmaz. Dört kap biçiminin dördü de boş değer kabul eder, çünkü boş değer
her referans tipine uyar ve hangi tipin deposu olduğu sorusunu doğurmaz. Bu yüzden Depo<?>
biçiminin ölçülmüş tanımı şudur: okuma yalnız Object olarak, yazma yalnız boş değerle.
Sınırsız joker karakter bir tip söylemez, tip sorusunu sormayacağını söyler.
Son iki satır aynı kuralın en dar noktasıdır. Depo<Ust> üzerinde d.koy(d.al()) yazımı,
yani kaptan okuyup aynı kaba geri yazmak, derleniyor. Aynı satır Depo<?> üzerinde
derlenmiyor — okunan değerin tipi bilinmediği için, aynı kaptan gelmiş olması bile
yazmayı geçerli kılmıyor. Derleyici burada tipin kimliğini değil, yalnız adını izler.
Yönü Ayırmanın Çağıran Tarafta Karşılığı
- GN12 — İki imza için dokuzar çağrı denenir; deneme kümesi ikisinde de aynıdır ve yalnız çağrılan yöntemin parametre biçimi değişir.
// Kopya.java — yönü ayırmak çağıran tarafta kaç çifti açıyor
public class Kopya {
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 TAM = "static void kopyala(Depo<Ust> h, Depo<Ust> k) { h.koy(k.al()); }";
static final String JOKER =
"static void kopyala(Depo<? super Ust> h, Depo<? extends Ust> k) { h.koy(k.al()); }";
static final String[] DEPOLAR = {"Depo<Object>", "Depo<Ust>", "Depo<Alt>"};
static int say(String imza) {
int gecen = 0;
System.out.printf("%-14s %-12s %-12s %s%n", "hedef \\ kaynak",
DEPOLAR[0], DEPOLAR[1], DEPOLAR[2]);
for (String h : DEPOLAR) {
String[] hucre = new String[DEPOLAR.length];
for (int i = 0; i < DEPOLAR.length; i++) {
String kaynak = TIPLER + "class C { " + imza + " static void c("
+ h + " h, " + DEPOLAR[i] + " k) { kopyala(h, k); } }";
try {
Olcek.derle(kaynak, "C");
hucre[i] = "derlendi";
gecen++;
} catch (Exception e) {
hucre[i] = "-";
}
}
System.out.printf("%-14s %-12s %-12s %s%n", h, hucre[0], hucre[1], hucre[2]);
}
return gecen;
}
public static void main(String[] args) {
System.out.println("kopyala(Depo<Ust>, Depo<Ust>)");
System.out.println("dokuz ciftin derlenen sayisi: " + say(TAM));
System.out.println();
System.out.println("kopyala(Depo<? super Ust>, Depo<? extends Ust>)");
System.out.println("dokuz ciftin derlenen sayisi: " + say(JOKER));
}
}
kopyala(Depo<Ust>, Depo<Ust>) hedef \ kaynak Depo<Object> Depo<Ust> Depo<Alt> Depo<Object> - - - Depo<Ust> - derlendi - Depo<Alt> - - - dokuz ciftin derlenen sayisi: 1 kopyala(Depo<? super Ust>, Depo<? extends Ust>) hedef \ kaynak Depo<Object> Depo<Ust> Depo<Alt> Depo<Object> - derlendi derlendi Depo<Ust> - derlendi derlendi Depo<Alt> - - - dokuz ciftin derlenen sayisi: 4
Aynı gövde, iki ayrı parametre biçimi: birincisi dokuz çağrı çiftinin birini kabul ediyor, ikincisi dördünü. Kapatılan iki işlem burada geri ödeniyor. Kaynaktan yalnız okunduğu için ona üst sınır, hedefe yalnız yazıldığı için ona alt sınır konabiliyor ve yöntem dört ayrı çağrıya açılıyor.
Boş kalan hücreler de aynı kuralın ölçüsüdür. Depo<Alt> hedef olamıyor, çünkü oraya bir
Ust yazılabilirdi; Depo<Object> kaynak olamıyor, çünkü oradan gelen değerin bir Ust
olduğu bilinmiyor. Genişleme rastgele değil, işlemin yönüyle tam olarak eşleşiyor.
Kısıt Çalışma Zamanında Yoktur
- GN13 — Nesne kimliği yalnız
==ile sorulur; hiçbir kimlik sayısı basılmaz. - GN14 — Ham tiple yazmanın sonucu iddia edilmez,
instanceofile ölçülür.
// Akis.java — yön kısıtı çalışma zamanında duruyor mu
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 Akis {
@SuppressWarnings({"unchecked", "rawtypes"})
public static void main(String[] args) {
Depo<Alt> altDepo = new Depo<>();
altDepo.koy(new Alt());
Depo<? extends Ust> okunur = altDepo;
Depo<? super Alt> yazilir = altDepo;
Depo<?> kapali = altDepo;
System.out.println("dort bildirilen tip, kac nesne : "
+ ((okunur == yazilir) && (yazilir == kapali) && (kapali == altDepo) ? 1 : 2));
System.out.println("nesnenin calisma zamani sinifi : "
+ altDepo.getClass().getSimpleName());
System.out.println("kovaryant gorunumden okuma : " + okunur.al().ad());
yazilir.koy(new Alt());
System.out.println("kontravaryant gorunume yazma : " + altDepo.al().ad());
Depo ham = okunur;
ham.koy(new Ust());
System.out.println("ham tiple Ust yazildi mi : "
+ (altDepo.al() instanceof Alt ? "hayir" : "evet"));
try {
Alt a = altDepo.al();
System.out.println("okuma sonucu : " + a.ad());
} catch (ClassCastException e) {
System.out.println("hata OKUYAN yerde : "
+ e.getClass().getSimpleName());
}
}
}
dort bildirilen tip, kac nesne : 1 nesnenin calisma zamani sinifi : Depo kovaryant gorunumden okuma : alt yontem kontravaryant gorunume yazma : alt yontem ham tiple Ust yazildi mi : evet hata OKUYAN yerde : ClassCastException
İlk iki satır dersin bütün tablosunu tek bir cümleye indiriyor: dört ayrı bildirilen tip,
tek bir nesne. Kabın çalışma zamanı sınıfı yalnız Depo; ne tip parametresi ne de yön
kısıtı orada. Okuma ve yazma kapıları nesnenin üzerinde değil, ona bakan adın üzerinde
açılıp kapanıyor.
Üçüncü ve dördüncü satırlar açık kapıların çalıştığını doğruluyor. Beşinci satır ise kapalı
kapının neyle kapatıldığını gösteriyor. Kovaryant görünüme bir Ust yazmak derlenmiyordu;
aynı görünüm ham tiple adlandırıldığında yazma geçiyor ve depodaki öğe artık bir Alt
değil. Yön kısıtını uygulayan tek taraf derleyicidir; o taraf devre dışı bırakıldığında
kısıtı yeniden uygulayacak kimse yoktur.
Son satır bedelin nereye düştüğünü yazıyor. Kusur, yanlış tipin yazıldığı satırda değil,
Alt bekleyen okumada göründü — ve bu kez arada bir de görünüm değişimi var: yazan kod kabı
kovaryant görünümüyle tanıyordu, düşen kod ise gerçek tipiyle.
Özet
- Dört kap biçimi ve iki işlemin sekiz çiftinden dördü derlenir. Joker karakter kabul edilen tip kümesini genişletir, karşılığında iki işlemden birini kapatır.
- Kovaryans (
? extends) okumayı açar, yazmayı kapatır; kontravaryans (? super) yazmayı açar, okumayıObjectdüzeyine indirir. İkisi bir arada açılmaz. - Sınırlayıcı ölçüm: hiçbir kapı gerçekte kilitli değildir. Dört biçimin dördünden de
Objectolarak okunabilir ve dördüne de boş değer yazılabilir; sınırsız joker karakterin yazmaya izin verdiği tek değer budur. Depo<?>üzerinde kaptan okunanı aynı kaba geri yazmak derlenmez, oysaDepo<Ust>üzerinde derlenir: derleyici tipin kimliğini değil adını izler.- Yön kısıtının tamamı imza özniteliğindedir. Tanımlayıcıda joker karakterin izi yoktur, ona ait tek bir komut üretilmez, ve dört ayrı bildirilen tip çalışma zamanında tek bir nesnedir.
Sonraki Adım
İki ders boyunca derleyicinin kapattığı kapılar sayıldı: yanlış tipe yazma derlenmedi, kovaryant görünüme yazma derlenmedi, kaptan okunanı geri yazma derlenmedi. Bunların hepsinde derleyici bir yasak koydu. Java’da derleyicinin kaynağa dayattığı bir de zorunluluk vardır: kimi olayları bildirmeden ya da işlemeden bir yöntem yazılamaz. Sıradaki ders o zorunluluğu ölçer — denetlenen ile denetlenmeyen istisna arasındaki ayrım derleme zamanında tam olarak neyi zorluyor, ve bu zorunluluğun sınıf dosyasındaki karşılığını sanal makine denetliyor mu?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.