İçeriğe geç
academia.sh

Ders 12 / 13

Kaynakla Deneme

Gövde ile kapatma aynı anda düştüğünde elle yazılan biçimde iki olaydan birine ulaşılamaz, kaynakla denemede ikisine de ulaşılır. Ölçüm kaybolan ve korunan istisna sayısını, kapatmanın ters sırasını ve derleyicinin ürettiği fazladan beş komutu birlikte gösterir.

İçindekiler

Önceki derste her ölçümde tek bir olay vardı: bir istisna fırlatıldı, bildirildi ve yakalandı. Gerçek bir gövdede olay genellikle yalnız değildir. Bir kaynak açılmışsa gövde düşerken o kaynağın kapatılması gerekir, ve kapatma da düşebilir. Bu durumda iki istisna aynı anda ortaya çıkar ve ikisi de aynı yoldan yukarı gitmek zorundadır.

Bağlam yöneticisiyle kaynağın serbest bırakılması Python Temelleri kursunda kuruldu; o düzenek burada tekrarlanmıyor. Buradaki soru sayısaldır: iki olay çıktığında kaça ulaşılabiliyor, hangisi ulaşılamaz oluyor, ve kapatma hangi sırayla yapılıyor? Kursun sorusu bu derste biçim değiştirir: kararı veren taraf bir tip değil, kodu yazan taraftır — elle yazılan kapatma ile derleyicinin ürettiği kapatma aynı gövdede ayrı sonuçlar verir.

Kaybolan İstisna ile Taşınan İstisna

  • GN21 — Kaynak sınıfı dersin içinde tanımlanır; her açılış ve kapanış bir günlüğe yazılır ve günlük her ölçümden önce boşaltılır.
  • GN22 — Her ölçümün kaç olay ürettiği kurgudan bilinir ve tabloda payda olarak durur; ulaşılan sayı, çıkan istisnadan ve onun taşıdığı bastırılan istisnalardan sayılır.
  • GN23 — İstisnalardan yalnız ileti metni basılır; yığıt izi ve konum basılmaz.
// Kapatma.java — elle yazılan kapatma ile kaynakla deneme yan yana
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Olay extends Exception {
    Olay(String m) { super(m); }
}

class Kaynak implements AutoCloseable {
    static final List<String> gunluk = new ArrayList<>();
    private final String ad;
    private final boolean kapanistaDuser;

    Kaynak(String ad, boolean kapanistaDuser) {
        this.ad = ad;
        this.kapanistaDuser = kapanistaDuser;
        gunluk.add("acildi " + ad);
    }

    @Override public void close() throws Olay {
        gunluk.add("kapandi " + ad);
        if (kapanistaDuser) throw new Olay("kapanis " + ad);
    }
}

interface Deneme {
    void calistir() throws Olay;
}

public class Kapatma {
    static void elle() throws Olay {
        Kaynak k = new Kaynak("A", true);
        try {
            throw new Olay("govde");
        } finally {
            k.close();
        }
    }

    static void kaynakla() throws Olay {
        try (Kaynak k = new Kaynak("A", true)) {
            throw new Olay("govde");
        }
    }

    static void yalnizKapanis() throws Olay {
        try (Kaynak k = new Kaynak("A", true)) {
            Kaynak.gunluk.add("govde saglam");
        }
    }

    static void ikiKapanis() throws Olay {
        try (Kaynak a = new Kaynak("A", true); Kaynak b = new Kaynak("B", true)) {
            Kaynak.gunluk.add("govde saglam");
        }
    }

    static void ucKaynak() throws Olay {
        try (Kaynak a = new Kaynak("A", false);
             Kaynak b = new Kaynak("B", false);
             Kaynak c = new Kaynak("C", false)) {
            Kaynak.gunluk.add("govde saglam");
        }
    }

    static void bildir(String ad, int olay, Deneme d) {
        Kaynak.gunluk.clear();
        String cikan = "-";
        int ulasilan = 0;
        try {
            d.calistir();
        } catch (Olay e) {
            cikan = e.getMessage();
            ulasilan = 1 + e.getSuppressed().length;
            List<String> bastirilan =
                    Arrays.stream(e.getSuppressed()).map(Throwable::getMessage).toList();
            if (!bastirilan.isEmpty()) cikan += " + " + bastirilan;
        }
        System.out.printf("%-16s %-26s %-9s %s%n", ad, cikan, ulasilan + " / " + olay,
                Kaynak.gunluk);
    }

    public static void main(String[] args) {
        System.out.printf("%-16s %-26s %-9s %s%n", "olcum", "cikan (+ bastirilan)",
                "ulasilan", "gunluk");
        bildir("elle", 2, Kapatma::elle);
        bildir("kaynakla", 2, Kapatma::kaynakla);
        bildir("yalniz kapanis", 1, Kapatma::yalnizKapanis);
        bildir("iki kapanis", 2, Kapatma::ikiKapanis);
        bildir("uc kaynak", 0, Kapatma::ucKaynak);
    }
}
olcum            cikan (+ bastirilan)       ulasilan  gunluk
elle             kapanis A                  1 / 2     [acildi A, kapandi A]
kaynakla         govde + [kapanis A]        2 / 2     [acildi A, kapandi A]
yalniz kapanis   kapanis A                  1 / 1     [acildi A, govde saglam, kapandi A]
iki kapanis      kapanis B + [kapanis A]    2 / 2     [acildi A, acildi B, govde saglam, kapandi B, kapandi A]
uc kaynak        -                          0 / 0     [acildi A, acildi B, acildi C, govde saglam, kapandi C, kapandi B, kapandi A]

İlk iki satır dersin ölçümüdür. Aynı gövde, aynı kaynak, aynı iki olay: gövdede bir istisna, kapatmada bir istisna. Elle yazılan biçimde çağırana ulaşan tek istisna kapanış istisnasıdır; gövdedeki istisna hiçbir yerde durmaz ve kaybolur. Ulaşılan sayı ikide birdir. Kaynakla denemede ise ulaşılan sayı ikide ikidir: çıkan istisna gövdedeki, ve kapanış istisnası onun bastırılan istisnası olarak taşınıyor.

Kaybın nedeni finally bloğunun anlamındadır. finally içinden bir istisna çıkarsa o istisna, o sırada yolda olan istisnanın yerini alır; iki istisna aynı anda yukarı gidemez, ve dilin elle yazılan biçiminde ikincisini saklayacak bir yer yoktur. Kaynakla deneme tam olarak bu yeri açar: gövdenin istisnası yolda kalır, kapatmanınki ona iliştirilir.

Kaybın ölçülen bedeli, kusurun hangi olayla görüneceğidir. Elle yazılan biçimde çağıran “kapanis A” iletisini görür; oysa programı durduran asıl olay gövdededir ve o olayın izi hiçbir yerde yoktur. Kapatma sırasında düşen bir kaynak, kendisinden önce düşen her şeyin üstünü örter.

Kapatma Sırası

Üçüncü, dördüncü ve beşinci satırların günlükleri kapatmanın sırasını yazıyor. Üç kaynaklı ölçümde açılış sırası A, B, C, kapanış sırası C, B, A. Kaynaklar açılışın tersi sırayla kapatılır, ve bu keyfi değildir: sonra açılan kaynak önce açılana dayanmış olabilir, tersi olamaz.

Dördüncü satır aynı kuralı iki olayla birlikte gösteriyor. A ile B açılıyor, gövde sağlam bitiyor, ikisi de kapanışta düşüyor. Çağırana ulaşan istisna B’nin kapanış istisnasıdır — yani ilk kapatılanın, sonra açılanın. A’nınki ona bastırılan olarak yazılıyor. Sıra burada kimin çıkacağını da belirliyor: yolda olan ilk istisna kim ise sonrakiler ona iliştirilir.

Beşinci satırda hiçbir kaynak kapanışta düşmüyor ve ulaşılan olay sayısı sıfır; günlük yine de üç açılış ve üç kapanış gösteriyor. Kapatma, istisna olsun olmasın her durumda yapılır.

Kaynak Olma Kararını Bildirilen Tip Verir

  • GN24Olcek çekirdeğinin bu derste kullanılan üç parçası yazılır: derleme, bir yöntemin komut sayısı ve çağırdığı yöntemlerin adları.
  • GN25 — Altı denemede tipler ve gövdeler sabittir; yalnız kaynağın bildirildiği tip, gövdedeki satır ve çevreleyen yöntemin bildirimi değişir.
// 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.lang.classfile.instruction.InvokeInstruction;
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 int komutSayisi(String kaynak, String sinif, String yontem) throws Exception {
        int sayi = 0;
        for (MethodModel m : derle(kaynak, sinif).methods())
            if (m.methodName().stringValue().equals(yontem))
                for (CodeElement e : m.code().orElseThrow())
                    if (e instanceof Instruction) sayi++;
        return sayi;
    }

    static List<String> cagrilar(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 InvokeInstruction iv)
                        adlar.add(kisa(iv.owner().asInternalName()) + "." + iv.name().stringValue());
        return adlar;
    }

    static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}
// Uygun.java — bir ad ne zaman kaynak olabilir
public class Uygun {
    static final String TIPLER = """
        class Olay extends Exception { Olay(String m) { super(m); } }
        class Kapanan implements AutoCloseable { @Override public void close() throws Olay { } }
        class Sessiz implements AutoCloseable { @Override public void close() { } }
        class Kapanmayan { public void close() { } }
        """;

    static final String BILDIREN = TIPLER + "class C { static void y() throws Olay { %s } }";
    static final String BILDIRMEYEN = TIPLER + "class C { static void y() { %s } }";

    static void dene(String kalip, String ad, String govde) {
        try {
            Olcek.derle(kalip.formatted(govde), "C");
            System.out.printf("%-42s -> derlendi%n", ad);
        } catch (Exception e) {
            System.out.printf("%-42s -> derlenmedi%n", ad);
        }
    }

    public static void main(String[] args) {
        System.out.println("cevreleyen yontem Olay bildiriyor");
        dene(BILDIREN, "try (Kapanan k = new Kapanan())", "try (Kapanan k = new Kapanan()) { }");
        dene(BILDIREN, "try (Object k = new Kapanan())", "try (Object k = new Kapanan()) { }");
        dene(BILDIREN, "try (Kapanmayan k = new Kapanmayan())",
                "try (Kapanmayan k = new Kapanmayan()) { }");
        dene(BILDIREN, "govde icinde k = new Kapanan()",
                "try (Kapanan k = new Kapanan()) { k = new Kapanan(); }");
        System.out.println();
        System.out.println("cevreleyen yontem hicbir sey bildirmiyor");
        dene(BILDIRMEYEN, "try (Kapanan k = new Kapanan())", "try (Kapanan k = new Kapanan()) { }");
        dene(BILDIRMEYEN, "try (Sessiz k = new Sessiz())", "try (Sessiz k = new Sessiz()) { }");
    }
}
cevreleyen yontem Olay bildiriyor
try (Kapanan k = new Kapanan())            -> derlendi
try (Object k = new Kapanan())             -> derlenmedi
try (Kapanmayan k = new Kapanmayan())      -> derlenmedi
govde icinde k = new Kapanan()             -> derlenmedi

cevreleyen yontem hicbir sey bildirmiyor
try (Kapanan k = new Kapanan())            -> derlenmedi
try (Sessiz k = new Sessiz())              -> derlendi

İkinci satır bu dersi kursun eksenine bağlıyor. Aynı nesne — çalışma zamanı tipi Kapanan, yani kapatılabilir bir nesne — Object olarak adlandırıldığında kaynak olamıyor. Nesne kapatılabilir olmasına rağmen derleyici kapatma çağrısını üretmiyor, çünkü üretilecek çağrının var olup olmadığına bakarken bildirilen tipe bakıyor. Kaynakla denemenin bütün düzeneği derleme zamanında kurulur ve nesnenin gerçek sınıfına hiç sorulmaz.

Üçüncü satır aynı kuralın alt sınırıdır: close adında bir yöntemi olan ama kapatılabilirlik sözleşmesini üstlenmeyen bir sınıf kaynak olamıyor. Ad yeterli değildir; gereken şey bildirilen tipin o sözleşmeyi taşımasıdır.

Dördüncü satır kaynak değişkeninin gövde içinde yeniden bağlanamadığını gösteriyor. Bunun gerekçesi üretilen kodun kendisidir: derleyici kapatma çağrısını başlangıçta bağlanan nesne için üretir, ve değişken yeniden bağlanabilseydi kapatılan nesne ile açılan nesne ayrı düşerdi.

Son iki satır ise önceki dersin daraltma kuralının buradaki karşılığıdır. close yöntemi denetlenen bir istisna bildirdiğinde kaynakla deneme bloğu o istisnayı çağırana taşır ve çevreleyen yöntem bir seçim yapmak zorunda kalır. Aynı blok, close hiçbir şey bildirmediğinde hiçbir zorunluluk üretmiyor. Kaynakla denemenin çağırana yazdığı bedel, kaynağın kapatma bildirimiyle birebir aynıdır.

Derleyicinin Ürettiği Kod

  • GN26 — Ölçülen kaynak dersin içinde yazılır; iki yöntemin gövdesi aynı işi yapar, yalnız kapatmanın yazılış biçimi değişir.
// Uretilen.java — kaynakla deneme sınıf dosyasında ne üretiyor
public class Uretilen {
    static final String KAYNAK = """
        class Olay extends Exception { Olay(String m) { super(m); } }
        class Kaynak implements AutoCloseable {
            void is() { }
            @Override public void close() throws Olay { }
        }
        class C {
            static void elle() throws Olay {
                Kaynak k = new Kaynak();
                try { k.is(); } finally { k.close(); }
            }
            static void kaynakla() throws Olay {
                try (Kaynak k = new Kaynak()) { k.is(); }
            }
        }
        """;

    public static void main(String[] args) throws Exception {
        for (String y : new String[] {"elle", "kaynakla"})
            System.out.printf("%-10s komut %2d  cagri %s%n", y,
                    Olcek.komutSayisi(KAYNAK, "C", y), Olcek.cagrilar(KAYNAK, "C", y));
    }
}
elle       komut 15  cagri [Kaynak.<init>, Kaynak.is, Kaynak.close, Kaynak.close]
kaynakla   komut 20  cagri [Kaynak.<init>, Kaynak.is, Kaynak.close, Kaynak.close, Throwable.addSuppressed]

Kaynakta iki satır daha kısa olan biçim sınıf dosyasında beş komut daha uzundur: elle yazılan 15, kaynakla yazılan 20. Kaynakla deneme bir kısaltmadır, ama kısalttığı şey yazılan metindir; ürettiği kod fazladan iş yapar.

Çağrı listelerinde iki close görünüyor: biri gövde düzgün bittiğinde çalışan yol, biri gövde düştüğünde çalışan yol. Bu ikisi elle yazılan biçimde de vardır, çünkü derleyici finally bloğunu iki yola da yerleştirir. Farkı yapan beşinci çağrıdır ve yalnız kaynakla denemede görünür: bastırma çağrısı. Fazladan beş komut da bunun içindir — gövdeden çıkan istisnanın tutulması, kapatmanın ayrı bir kolda denenmesi ve ikinci istisnanın birincisine iliştirilmesi.

Ölçümün söylediği şudur: bastırılan istisna dilin çalışma zamanında kendiliğinden olan bir şey değildir. Kaynağı kapatan kodun içinde bastırmayı isteyen bir çağrı vardır ve o çağrıyı derleyici koymuştur; aynı çağrıyı elle yazan biri aynı sonucu alırdı. Kaynakla denemenin kazandırdığı şey yeni bir yetenek değil, o kodun her seferinde ve aynı biçimde yazılmasıdır.

Kapatma İstisnası Gövdedekini Bastırmaz

Bu dersin sınırlayıcı ölçümü ikinci ile üçüncü satırın karşılaştırılmasındadır ve bastırma yönünün tek olduğunu gösterir.

İkinci satırda hem gövde hem kapatma düşüyor ve çıkan istisna gövdedekidir; kapatmanınki bastırılan olarak taşınıyor. Yön hiçbir zaman tersine dönmez: kapatma sırasında düşen bir istisna gövdedekini bastıramaz, çünkü gövdedeki istisna zaten yoldadır ve yerini kimseye bırakmaz.

Üçüncü satır bu kuralın sınırını çiziyor. Gövde sağlam bittiğinde ortada bastırılacak bir şey yoktur; kapatma istisnası doğrudan çıkar ve bastırılan listesi boştur. Yani bastırma bir gizleme düzeneği değil, yalnız ikinci olayı taşıyan bir yerdir. Kapatmadaki kusur tek başınayken görünürlüğünden hiçbir şey kaybetmez.

Bu iki satırın birlikte söylediği kural şudur: kaynakla deneme hiçbir olayı yok etmez ve hiçbir olayın önceliğini değiştirmez. Elle yazılan biçimde kaybolan istisna, öncelik sırası yanlış olduğu için değil, ikinci olayı koyacak bir yer olmadığı için kayboluyordu.

Özet

  • Gövde ile kapatma aynı anda düştüğünde elle yazılan biçimde iki olaydan yalnız birine ulaşılır; kaynakla denemede ikisine de ulaşılır.
  • Elle yazılan biçimde finally içinden çıkan istisna yoldaki istisnanın yerini alır ve gövdedeki olayın izi kalmaz; çağıran, programı durduran olayı değil kapatma olayını görür.
  • Kaynaklar açılışın tersi sırayla kapatılır; birden çok kapanış düştüğünde ilk kapatılanın istisnası çıkar, sonrakiler ona bastırılan olarak yazılır.
  • Bir adın kaynak olup olamayacağına bildirilen tip karar verir: kapatılabilir bir nesne Object olarak adlandırıldığında kapatılmaz, ve blok çağırana ancak close bildiriminin taşıdığı kadar bedel yazar.
  • Kaynakla deneme sınıf dosyasında elle yazılan biçimden beş komut daha uzundur; kısalan şey yazılan metindir, üretilen kod değil.
  • Sınırlayıcı ölçüm: bastırma yönü tektir. Kapatma istisnası gövdedekini bastırmaz, tersi olur; gövde sağlam bittiğinde kapatma istisnası doğrudan ve tek başına çıkar.

Sonraki Adım

Üç ders boyunca istisna hep verili bir şey oldu: fırlatıldı, bildirildi, yakalandı, bastırıldı. Hiçbirinde bir olayın istisnayla mı bildirileceği sorulmadı. Oysa aynı iş üç ayrı sözleşmeyle yazılabilir: denetlenen istisna, denetlenmeyen istisna, ya da hiç istisna kullanmayıp sonucu bir tiple döndürmek. Kursun son dersi üçünü aynı işle kurup çağıran tarafta derleyicinin zorladığı karar noktası sayısını sayar ve sözleşme kırıldığında kusurun nerede göründüğünü ölçer.

İ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