İçeriğe geç
academia.sh

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.
  • GN9Olcek ç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 Object olarak 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, instanceof ile ö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ı Object dü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 Object olarak 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, oysa Depo<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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat