İçeriğe geç
academia.sh

Ders 07 / 13

İç ve Yerel Sınıflar

Tek bir kaynak dosyası altı sınıf dosyası üretir ve üretilenlerin dördü kaynakta hiç yazılmamış gizli alanlar taşır. İç sınıf dış örneğe bir alan üzerinden bağlanır ve o alanı canlı okur; yerel sınıf yakaladığı değişkeni yapıcı parametresiyle kopyalar, bu yüzden değişken bir daha atanamaz.

İçindekiler

Önceki dersin ölçümünde bir kısıt sınıf dosyasına hiç yazılmadan yok oldu: final bir yerel değişkenin kısıtı yalnız derleyicide durdu. Derleme bittiğinde o söz ortadan kalkıyorsa neye yarıyor? Java’da derleyicinin aynı sözü yazılmadığı hâlde kendiliğinden istediği bir yer vardır ve orada sözün karşılığı görünür hâle gelir.

O yer bir sınıfın başka bir sınıfın içine yazıldığı yerdir. Java’da bir sınıf bir başkasının gövdesinde, bir yöntemin gövdesinde ya da doğrudan bir new ifadesinin içinde tanımlanabilir. Kaynakta bunların hepsi tek bir dosyada, iç içe durur; sınıf dosyası ise iç içe olamaz. Sorular üç tanedir: bu iç içelik kaç ayrı dosyaya açılıyor, üretilen dosyalar dışarıyla nasıl bağlanıyor, ve bağlanma biçimi kararı hangi tarafa bırakıyor?

Ölçüm Çekirdeği

Ölçüm bir kaynağı derler ve üretilen bütün sınıf dosyalarını okur. Her dosya için iki şey sayılır: alanlar — kaynakta yazılmış olanlarla derleyicinin eklediği gizli olanlar birlikte — ve yapıcının imzası.

  • SN31 — Kâhin kaynağın kendisidir: hangi alanı yazdığımızı biliriz, dolayısıyla listede fazladan görünen her alan derleyicinin eklediğidir. Gizli alan tahminle değil, sınıf dosyasındaki bayrağıyla ayrılır.
  • SN32 — Bütün sınıf dosyaları tek bir kaynak dosyasından üretilir; sayım o dizindeki bütün dosyaları kapsar.
  • SN33 — Yakalama ölçümünde nesne, dış alanın değeri değiştirilmeden önce üretilir; böylece sonradan okunan değer canlı okumanın kanıtı olur.
  • SN34 — Derleme denemelerinde tek bir satır değişir ve her deneme kendi geçici dizininde derlenir.
  • SN35 — Ortama bağlı hiçbir veri okunmaz; sayılan şey dosya sayısı, alan sayısı ve yapıcı parametreleridir.
// Olcek.java — üretilen sınıf dosyaları, taşıdıkları gizli alanlar ve yapıcı imzaları
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.reflect.AccessFlag;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Olcek {
    static Path yeniDizin() throws Exception { return Files.createTempDirectory("olcek"); }

    static boolean derle(Path d, String ad, String kaynak) throws Exception {
        Path k = d.resolve(ad + ".java");
        Files.writeString(k, kaynak);
        PrintWriter bos = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(bos, bos, "-d", d.toString(), "-cp", d.toString(), k.toString()) == 0;
    }

    static List<String> sinifDosyalari(Path d) throws Exception {
        try (var s = Files.list(d)) {
            return s.map(p -> p.getFileName().toString())
                    .filter(n -> n.endsWith(".class")).sorted().toList();
        }
    }

    static String alanlar(Path d, String dosya) throws Exception {
        ClassModel cm = ClassFile.of().parse(d.resolve(dosya));
        List<String> a = new ArrayList<>();
        for (FieldModel f : cm.fields())
            a.add(f.fieldName().stringValue()
                    + (f.flags().has(AccessFlag.SYNTHETIC) ? " [gizli]" : ""));
        return a.isEmpty() ? "-" : String.join(", ", a);
    }

    static String yapici(Path d, String dosya) throws Exception {
        ClassModel cm = ClassFile.of().parse(d.resolve(dosya));
        for (MethodModel m : cm.methods())
            if (m.methodName().stringValue().equals("<init>")) {
                List<String> p = new ArrayList<>();
                for (var a : m.methodTypeSymbol().parameterList()) p.add(a.displayName());
                return "(" + String.join(", ", p) + ")";
            }
        return "-";
    }
}

Bir Kaynak Dosyası, Altı Sınıf Dosyası

Ölçülen kaynak tek bir dış sınıf içerir ve içine beş ayrı biçim yerleştirir: bir iç sınıf, bir statik iç içe sınıf, bir örnek yönteminde tanımlanan yerel sınıf, bir statik yöntemde tanımlanan yerel sınıf ve bir adsız sınıf.

// Uretim.java — iç içe yazılan sınıflar kaç dosya üretir
import java.nio.file.Path;

public class Uretim {
    static final String KAYNAK = """
        class Dis {
            int disAlan = 1;
            class Ic { int oku() { return disAlan; } }
            static class Yuva { int oku() { return 2; } }
            Runnable ornekYerel() {
                int y = 3;
                class Yerel implements Runnable { public void run() { say(disAlan + y); } }
                return new Yerel();
            }
            static Runnable statikYerel() {
                int y = 4;
                class SYerel implements Runnable { public void run() { say(y); } }
                return new SYerel();
            }
            Runnable isimsiz() {
                int y = 5;
                return new Runnable() { public void run() { say(disAlan + y); } };
            }
            static void say(int n) { }
        }
        """;

    public static void main(String[] args) throws Exception {
        Path d = Olcek.yeniDizin();
        Olcek.derle(d, "Dis", KAYNAK);
        System.out.printf("%-18s %-28s %s%n", "sinif dosyasi", "alanlar", "yapici imzasi");
        for (String f : Olcek.sinifDosyalari(d))
            System.out.printf("%-18s %-28s %s%n", f, Olcek.alanlar(d, f), Olcek.yapici(d, f));
        System.out.println("uretilen dosya sayisi: " + Olcek.sinifDosyalari(d).size());
    }
}
sinif dosyasi      alanlar                      yapici imzasi
Dis$1.class        val$y [gizli], this$0 [gizli] (Dis, int)
Dis$1SYerel.class  val$y [gizli]                (int)
Dis$1Yerel.class   val$y [gizli], this$0 [gizli] (Dis, int)
Dis$Ic.class       this$0 [gizli]               (Dis)
Dis$Yuva.class     -                            ()
Dis.class          disAlan                      ()
uretilen dosya sayisi: 6

Tek bir kaynak dosyasından altı sınıf dosyası çıktı. İç içelik sınıf dosyası düzeyinde yoktur; her biçim kendi dosyasını alır ve adları dış sınıfın adından türetilir. Adsız sınıfın kaynakta hiç adı yoktu, dosyası yine de vardır.

İkinci sütun asıl ölçümü taşıyor. Kaynakta yazılan tek alan disAlandır ve yalnız Dis dosyasında görünür. Öteki dosyalarda görünen dört alan kaynakta yazılmamıştır: this$0 ve val$y. Beş biçimin dördü en az bir gizli alan taşıyor; taşımayan tek biçim statik iç içe sınıftır.

Üçüncü sütun gizli alanların nasıl dolduğunu gösterir. Dis$Ic sınıfının yapıcısı bir Dis parametresi alır; kaynakta bu sınıfın yapıcısı hiç yazılmamıştır ve new yazan taraf da parametre vermez. Dis$1Yerel iki parametre alır: dış örnek ve yakalanan sayı. Dis$1SYerel yalnız sayıyı alır, çünkü statik bir yöntemde dış örnek yoktur. Derleyici hem alanı hem de onu dolduran parametreyi eklemiştir; iç içe yazma bir görünüm değil, bir bağlama düzeneğidir.

Tablo beş biçimi bu düzeneğe göre üç kümeye ayırıyor. Dış örneği de yakalanan değeri de taşıyanlar: örnek yönteminde tanımlanan yerel sınıf ile adsız sınıf. Yalnız birini taşıyanlar: dış örneği taşıyan iç sınıf ve yalnız yakalanan değeri taşıyan statik yöntemdeki yerel sınıf. Hiçbirini taşımayan: statik iç içe sınıf. Kaynakta bu beş biçim birbirine çok benzer biçimde yazılır — hepsi bir sınıf bildirimidir ve aralarındaki fark bir sözcük ya da bir konumdur — ama sınıf dosyasında üç ayrı yapıya dönüşürler. Bir biçimi seçmek, bir yazım kolaylığı seçmek değildir.

Paylaşılan Alan, Kopyalanan Yerel

Gizli alanlar iki ayrı şeyi taşıyor ve ikisi aynı biçimde davranmıyor. this$0 bir referanstır; val$y ise bir değerin kopyasıdır. Aradaki farkı görmek için ikisi de aynı nesne üzerinden okunur.

// Yakala.java — dış alan paylaşılır, yakalanan yerel kopyalanır
public class Yakala {
    int alan = 1;

    Runnable uret() {
        int yerel = 1;
        class Iz implements Runnable {
            @Override public void run() { System.out.println("  alan=" + alan + " yerel=" + yerel); }
        }
        Runnable r = new Iz();
        alan = 2;
        return r;
    }

    static final String GOVDE = """
        class Deneme {
            int alan = 1;
            Runnable f() {
                int yerel = 1;
                class Iz implements Runnable { public void run() { say(alan + yerel); } }
                Runnable r = new Iz();
                %s
                return r;
            }
            static void say(int n) { }
        }
        """;

    public static void main(String[] args) throws Exception {
        Yakala y = new Yakala();
        Runnable r = y.uret();
        System.out.println("nesne uretildikten sonra alan 2 yapildi, yerel yok oldu");
        r.run();
        y.alan = 3;
        System.out.println("disaridan alan 3 yazildi");
        r.run();
        System.out.println();
        for (String[] s : new String[][] {{"alan = 2;", "dis alana yeniden atama"},
                                          {"yerel = 2;", "yakalanan yerele yeniden atama"}})
            System.out.printf("%-32s -> derleniyor mu: %s%n", s[1],
                    Olcek.derle(Olcek.yeniDizin(), "Deneme", GOVDE.formatted(s[0])));
    }
}
nesne uretildikten sonra alan 2 yapildi, yerel yok oldu
  alan=2 yerel=1
disaridan alan 3 yazildi
  alan=3 yerel=1

dis alana yeniden atama          -> derleniyor mu: true
yakalanan yerele yeniden atama   -> derleniyor mu: false

Iz nesnesi üretildiği anda alan 1, yerel 1 idi. Nesne üretildikten sonra alan iki kez değişti ve iki değişiklik de okunan değere yansıdı: önce 2, sonra 3. yerel ise her iki okumada 1 kaldı. Kaynakta iki ad da aynı biçimde, hiçbir niteleyici olmadan yazılmıştır; buna karşılık biri dışarıdaki değeri izliyor, öteki üretim anında donmuş bir kopyayı okuyor.

Fark gizli alanların taşıdığı şeyden çıkıyor. this$0 dış nesneyi gösterir ve alan her okunduğunda o nesneden alınır; dolayısıyla dışarıdaki değişiklik görünür. val$y ise sayının kendisini tutar ve yapıcıda bir kez doldurulmuştur; dışarıdaki değişkenle arasında hiçbir bağ kalmamıştır. Yöntemin yerel değişkeni zaten yöntem bittiğinde ortadan kalkar — nesne ise yaşamayı sürdürür, bu yüzden izleyecek bir şey de yoktur.

Son iki satır derleyicinin bu kopyayı nasıl koruduğunu gösteriyor. Dış alana yeniden atama yapan sürüm derleniyor; yakalanan yerele yeniden atama yapan sürüm derlenmiyor. Kaynakta final sözcüğü hiçbir yerde yazılmamıştır; kısıtı derleyici kendiliğinden koyar, çünkü kopyanın aslıyla anlaşmazlığa düşebileceği tek durum aslın değişmesidir. Önceki dersin “yerel değişkende final sınıf dosyasına hiç yazılmaz” ölçümü burada karşılığını buluyor: o kısıt bir bayrak bırakmaz, ama bir kopyayı korur.

Hangi Dış Örnek — Üçüncü Bir Kaynak

Kararın tarafı bu bölümde ikiye ayrılır. Yakalanan değerin ne olacağı yapıcının çağrıldığı anda kesinleşir ve bir daha sorulmaz. Hangi dış nesnenin okunacağı ise her okumada yeniden sorulur — ve yanıtı ne bildirilen tip ne de çalışma zamanı tipi verir.

// Baglama.java — hangi dış örneğe bağlandığı çalışma zamanında belirlenir
public class Baglama {
    static class Dis {
        String ad;
        Dis(String ad) { this.ad = ad; }
        class Ic { String oku() { return ad; } }
    }

    public static void main(String[] args) throws Exception {
        Dis a = new Dis("birinci");
        Dis b = new Dis("ikinci");
        Dis.Ic x = a.new Ic();
        Dis.Ic y = b.new Ic();
        System.out.println("x ile y ayni sinif mi : " + (x.getClass() == y.getClass()));
        System.out.println("x.oku()               : " + x.oku());
        System.out.println("y.oku()               : " + y.oku());
        System.out.println();
        for (String[] s : new String[][] {
                {"I yap() { return new D().new I(); }", "dis ornek verilerek"},
                {"static I yap() { return new I(); }",  "dis ornek olmadan"}})
            System.out.printf("%-20s -> derleniyor mu: %s%n", s[1],
                    Olcek.derle(Olcek.yeniDizin(), "D",
                            "class D { class I { } " + s[0] + " }"));
    }
}
x ile y ayni sinif mi : true
x.oku()               : birinci
y.oku()               : ikinci

dis ornek verilerek  -> derleniyor mu: true
dis ornek olmadan    -> derleniyor mu: false

x ile y aynı sınıftır — çıktının ilk satırı bunu doğruluyor — ve bildirilen tipleri de aynıdır. Buna karşılık aynı yöntem çağrısı iki ayrı sonuç veriyor. Bu kursta şimdiye kadar ölçülen iki kaynak da burada yanıt veremez: iki nesnenin çalışma zamanı tipi bir, bildirilen tipi bir. Sonucu belirleyen şey gizli alanın değeridir — hangi dış örneğin this$0 alanına yazıldığıdır.

Bu, kursun sorusuna bir üçüncü yanıt eklemez; onu daha kesin kılar. Yeniden tanımlanmış bir yöntemde nesnenin sınıfı yanıtı verirken burada nesnenin bir alanı verir; ikisi de çalışma zamanına aittir, ama biri tipe biri veriye bağlıdır. Aradaki farkın pratik sonucu şudur: alt sınıf eklenmeyen bir programda ilk kaynak hiç değişmez, ikinci kaynak ise her new ifadesinde yeniden belirlenir.

Alttaki iki satır bu alanın zorunluluğunu gösteriyor. Bir iç sınıf ancak bir dış örnek üzerinden üretilebilir; dış örneği olmayan bir bağlamdan yazılan new derlemeyi durdurur. Gizli alanın doldurulamayacağı bir yerde nesne hiç kurulamaz.

Sınırlayıcı Ölçüm: Gizli Alanın Bulunmadığı Yer

Tezin sınırı ilk tablonun iki satırında duruyor. Dis$Yuva hiçbir alan taşımıyor ve yapıcısı parametresizdir; Dis$1SYerel ise this$0 taşımıyor, yalnız val$y taşıyor.

Statik iç içe sınıf, dış sınıfın gövdesinde yazılmış olmasına karşın dış örnekle hiçbir bağ kurmaz. Kaynaktaki iç içelik yalnız ad alanı ve görünürlük içindir; üretilen sınıf, dış sınıfla aynı düzeyde duran bağımsız bir sınıftır. Bu yüzden “hangi dış nesne” sorusunun çalışma zamanında bir yanıtı yoktur, çünkü soru hiç sorulmaz. Aynı gerekçe statik bir yöntemde tanımlanan yerel sınıf için de geçerlidir: o yöntem çalışırken ortada bir dış örnek bulunmadığından taşınacak bir referans da yoktur, ama yakalanan yerel değişken orada da kopyalanır.

Buradan iki sonuç çıkar. Birincisi, gizli alanın varlığını belirleyen şey sınıfın nereye yazıldığı değil, dışarıdan neye eriştiğidir: dış örneğe erişmeyen bir biçim referans taşımaz. İkincisi, ölçüm gözlenemez hâle gelebilir. Dış alanı hiç okumayan bir iç sınıf yazıldığında this$0 yine eklenir ama davranışta hiçbir izi görünmez; iki biçim arasındaki fark, ancak dış nesne yaşamaya devam ettiği için bir daha serbest bırakılamadığında ortaya çıkar. Bir bağ kurulduğunu bilmeyen bir programcı, kaldırıldığını sandığı nesnenin hâlâ tutulduğunu ancak dolaylı olarak fark eder.

Özet

  • Tek bir kaynak dosyası altı sınıf dosyası üretir; iç içelik sınıf dosyası düzeyinde yoktur, adsız sınıfın bile kendi dosyası vardır ve beş biçimin dördü kaynakta yazılmamış gizli alan taşır: dış örneği tutan this$0, yakalanan değeri tutan val$y.
  • İki iç sınıf örneği aynı sınıftan ve aynı bildirilen tipten olduğu hâlde aynı çağrıya iki ayrı yanıt verir; yanıtı veren şey tip değil, this$0 alanına yazılan dış örnektir.
  • Gizli alanları dolduran şey yapıcının gizli parametreleridir: iç sınıfın yapıcısı (Dis), örnek yönteminde tanımlanan yerel sınıfınki (Dis, int), statik yöntemdekinin ise (int) imzasını taşır.
  • Dış alan paylaşılır ve canlı okunur — nesne üretildikten sonra 2 ve 3 değerleri okundu; yakalanan yerel kopyalanır ve iki okumada da 1 kaldı.
  • Kaynakta final yazılmadığı hâlde yakalanan değişkene yeniden atama derlemeyi durdurur; kısıt kopyanın aslıyla anlaşmazlığa düşmesini engeller.
  • Statik iç içe sınıf gizli alan taşımaz ve statik yöntemdeki yerel sınıf this$0 taşımaz: gizli alanı belirleyen şey yazıldığı yer değil, dışarıdan neye eriştiğidir.

Sonraki Adım

Bu derste derleyici kaynakta hiç yazılmamış sınıflar üretti ve onlara kaynakta hiç yazılmamış alanlar ile yapıcı parametreleri koydu. Üretilen her şeyin ortak yanı, bir programcının elle yazması gereken bir bağlama işini üstlenmesiydi. Aynı üretim yeteneğinin çok daha görünür bir kullanımı vardır: tek satırlık bir bildirimden yola çıkıp bir veri taşıyıcının bütün yüzeyini — alan okuyucularını, eşitliği, karma değerini ve metne çevirmeyi — derleyicinin üretmesi. Sonraki ders bu üyeleri sayar ve her birinin kararı hangi tarafa bıraktığını sorar; sonra da üretilen eşitliğin nereye kadar koruduğunu, yani bir veri taşıyıcının içindeki değişebilir bir parçanın dışarıdan nasıl değiştirilebildiğini ö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