İçeriğe geç
academia.sh

Ders 06 / 13

`final` Anahtar Sözcüğü

Tek bir anahtar sözcük üç ayrı kısıt koyar ve üçünü de derleme zamanında uygular: yeniden atama, yeniden tanımlama ve genişletme. Sabit bir final alanın değeri okuyan sınıfa yazılır ve kaynak değiştiğinde eski değer yerinde kalır; final bir referans ise nesneyi dondurmaz.

İçindekiler

Önceki dersin son ölçümünde bir değer, ait olduğu sınıfı hiç başlatmadan okundu: Ust.TAVAN bir static final alandı ve sınıf başlatma bloğu çalışmadan doğru sayı geldi. O sonucun nedeni tek bir sözcükte, final sözcüğünde duruyordu ve orada yalnız bir yan etki olarak göründü.

Bu ders o sözcüğü doğrudan ölçer. final üç ayrı yerde yazılabilir — bir değişkenin, bir yöntemin ve bir sınıfın başında — ve üç ayrı kısıt koyar. Üç kısıtın ortak yanı, hepsinin kararının derleme zamanında verilmesidir: hiçbiri çalışma zamanında sınanan bir koşul değildir. Sorular üç tanedir: bu kısıtların kaçı derleyicide durur, sabit bir alanın değeri nereye yazılır, ve final bir referans neyi korur?

Ölçüm Çekirdeği

Ölçüm üç şey yapar: bir kaynağı derler ve derlenip derlenmediğini bildirir, ürettiği sınıf dosyasındaki final bayraklarını okur, ve derlenmiş sınıfları ayrı bir yükleyicide çalıştırır.

  • SN26 — Derleme denemelerinde kâhin kurgunun kendisidir: her denemede tek bir satır değişir, dolayısıyla derlemenin durması yalnız o satıra bağlanabilir. Sonuç tek bir çıkış koduyla okunur; hata metni okunmaz, çünkü metin yerel ayara bağlıdır.
  • SN27 — Her deneme kendi geçici dizininde derlenir; denemeler birbirinin ürettiği sınıf dosyasını görmez.
  • SN28 — Gömme ölçümünde Sabit iki kez derlenir, Kullanan bir kez; ikinci koşumda okunan sınıf dosyası birincisiyle aynı dosyadır.
  • SN29 — Koşum ayrı bir yükleyicide yapılır, böylece ilk koşumda yüklenen sınıf ikincisine taşınmaz.
  • SN30 — Ortama bağlı hiçbir veri okunmaz; sayılan şey derlenen deneme sayısı, sınıf dosyasındaki bayraklar ve okunan değerdir.
// Olcek.java — derleme sonucu, sınıf dosyasındaki final bayrağı ve okuyan tarafın izi
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.lang.reflect.AccessFlag;
import java.net.URL;
import java.net.URLClassLoader;
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 String finalBayraklari(Path d, String sinif) throws Exception {
        ClassModel cm = ClassFile.of().parse(d.resolve(sinif + ".class"));
        List<String> f = new ArrayList<>();
        if (cm.flags().has(AccessFlag.FINAL)) f.add("sinif " + sinif);
        for (FieldModel x : cm.fields())
            if (x.flags().has(AccessFlag.FINAL)) f.add("alan " + x.fieldName().stringValue());
        for (MethodModel x : cm.methods())
            if (x.flags().has(AccessFlag.FINAL)) f.add("yontem " + x.methodName().stringValue());
        return String.join(", ", f);
    }

    static List<String> izler(Path d, String sinif, String yontem) throws Exception {
        ClassModel cm = ClassFile.of().parse(d.resolve(sinif + ".class"));
        List<String> iz = new ArrayList<>();
        for (MethodModel m : cm.methods()) {
            if (!m.methodName().stringValue().equals(yontem) || m.code().isEmpty()) continue;
            for (CodeElement e : m.code().get()) {
                if (e instanceof FieldInstruction f)
                    iz.add("alan okumasi " + kisa(f.owner().asInternalName())
                            + "." + f.name().stringValue());
                else if (e instanceof ConstantInstruction c)
                    iz.add("sabit yukleme " + c.constantValue());
                else if (e instanceof InvokeInstruction iv)
                    iz.add(switch (iv.opcode()) {
                        case INVOKEVIRTUAL -> "sanal cagri ";
                        case INVOKESTATIC -> "statik cagri ";
                        default -> "dogrudan cagri ";
                    } + kisa(iv.owner().asInternalName()) + "." + iv.name().stringValue());
            }
        }
        return iz;
    }

    static Object calistir(Path d, String sinif, String yontem) throws Exception {
        try (URLClassLoader cl = new URLClassLoader(new URL[] {d.toUri().toURL()}, null)) {
            var m = cl.loadClass(sinif).getDeclaredMethod(yontem);
            m.setAccessible(true);
            return m.invoke(null);
        }
    }

    static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}

Üç Kısıt, Üçü de Derleme Zamanında

İlk ölçüm dokuz kaynak derler. Denemeler çiftler hâlinde kurulmuştur: her final denemesinin yanında, tek farkı o sözcüğün yokluğu olan bir eşi vardır.

// Kisit.java — final üç ayrı kısıt koyar, üçü de derleme zamanında
import java.nio.file.Path;

public class Kisit {
    static final String[][] DENEME = {
        {"yerel degiskene yeniden atama",  "class T { void f() { int y = 1; y = 2; } }"},
        {"final yerele yeniden atama",     "class T { void f() { final int y = 1; y = 2; } }"},
        {"alana yeniden atama",            "class T { int a; T() { a = 1; } void f() { a = 2; } }"},
        {"final alana yeniden atama",      "class T { final int a; T() { a = 1; } void f() { a = 2; } }"},
        {"final alana yapicida atama",     "class T { final int a; T() { a = 1; } }"},
        {"yontemi yeniden tanimlama",      "class A { int f() { return 1; } } class T extends A { @Override int f() { return 2; } }"},
        {"final yontemi yeniden tanimlama","class A { final int f() { return 1; } } class T extends A { int f() { return 2; } }"},
        {"sinifi genisletme",              "class A { } class T extends A { }"},
        {"final sinifi genisletme",        "final class A { } class T extends A { }"},
    };

    public static void main(String[] args) throws Exception {
        System.out.printf("%-33s %s%n", "denenen yazim", "derleniyor mu");
        for (String[] d : DENEME)
            System.out.printf("%-33s %s%n", d[0], Olcek.derle(Olcek.yeniDizin(), "T", d[1]));

        String kaynak = """
            final class Bayrak {
                final int sabitAlan = 1;
                int digerAlan = 2;
                final int kilitli() { return 1; }
                int serbest() { final int y = 1; return y; }
            }
            """;
        Path d = Olcek.yeniDizin();
        Olcek.derle(d, "Bayrak", kaynak);
        System.out.println();
        System.out.println("sinif dosyasinda final bayragi tasiyanlar:");
        System.out.println("  " + Olcek.finalBayraklari(d, "Bayrak"));

        Path e = Olcek.yeniDizin();
        Olcek.derle(e, "A", """
            class A { final int kilitli() { return 1; } int serbest() { return 2; } }
            class Cagiran {
                static int a(A x) { return x.kilitli(); }
                static int b(A x) { return x.serbest(); }
            }
            """);
        System.out.println();
        System.out.println("final yontem cagrisi        : " + Olcek.izler(e, "Cagiran", "a"));
        System.out.println("final olmayan yontem cagrisi: " + Olcek.izler(e, "Cagiran", "b"));
    }
}
denenen yazim                     derleniyor mu
yerel degiskene yeniden atama     true
final yerele yeniden atama        false
alana yeniden atama               true
final alana yeniden atama         false
final alana yapicida atama        true
yontemi yeniden tanimlama         true
final yontemi yeniden tanimlama   false
sinifi genisletme                 true
final sinifi genisletme           false

sinif dosyasinda final bayragi tasiyanlar:
  sinif Bayrak, alan sabitAlan, yontem kilitli

final yontem cagrisi        : [sanal cagri A.kilitli]
final olmayan yontem cagrisi: [sanal cagri A.serbest]

Dokuz denemenin dördü durdu ve dördü de bir final denemesiydi. Eşleri sorunsuz derlendi; tek fark o sözcüktür. Beşinci satır kısıtın tam yerini gösterir: final bir alana atama yasak değildir, yeniden atama yasaktır. Alan yapıcıda bir kez atanabilir ve orada atanmazsa derleme yine durur; kısıt “hiç yazılmasın” değil, “tam bir kez yazılsın” biçimindedir.

Yedinci satır kursun sorusuna bağlanır. Bir yöntem final bildirildiğinde ona alt sınıftan başka bir yanıt yazmak olanaksızdır. Önceki derste bir çağrının iki olası yanıtı vardı ve hangisinin geleceğini çalışma zamanı tipi belirliyordu. final bu sayıyı bire indirir: çalışma zamanı tipi ne olursa olsun tek bir gövde kalır, dolayısıyla soru anlamını yitirir. final çalışma zamanı tipinin kararını bildirilen tipe taşımaz; kararı ortadan kaldırır.

Çıktının son iki satırı bu okumayı bir adım keskinleştiriyor. final bir yöntemin çağrısı da, final olmayanınki de sınıf dosyasına sanal çağrı olarak yazılıyor; iki satır arasında hiçbir fark yok. Yani derleyici çağrının düzeneğini değiştirmiyor, çağrının seçebileceği gövde kümesini bire indiriyor. Önceki dersteki statik çağrı ile arasındaki fark budur: orada yazım değişmeden hedef değişiyordu, burada hedef seçimi yerinde duruyor ama seçilecek tek bir aday kalıyor.

Ortadaki blok kısıtların nerede durduğunu ayırır. Sınıf dosyasında final bayrağını üç şey taşıyor: sınıfın kendisi, sabitAlan ve kilitli yöntemi. serbest yönteminin gövdesindeki final int y bildirimi listede yok. Yerel değişkenin kısıtı sınıf dosyasına hiç yazılmaz; o kısıt yalnız derleyicinin belleğinde yaşar ve derleme bittiğinde ortadan kalkar. Öteki ikisi bayrak olarak kalır, çünkü onları başka bir derleme okuyacaktır: bir alt sınıf yazan derleyici kilitli yönteminin bayrağını görmek zorundadır.

Sabit Değer Nereye Yazılıyor

Önceki dersin TAVAN ölçümü bir sorun bırakmıştı: sınıf başlatılmadığı hâlde değer nereden geldi? Aşağıdaki ölçüm iki alanı yan yana koyar. TAVAN değerini doğrudan bir sayıdan alır; TABAN aynı sayıyı bir yöntem çağrısından alır. İkisi de static finaldır.

// Gomme.java — sabit final alanın değeri okuyan sınıfa yazılır
import java.nio.file.Path;

public class Gomme {
    static String sabit(int n) {
        return """
            class Sabit {
                static final int TAVAN = %d;
                static final int TABAN = hesapla();
                static int hesapla() { return %d; }
            }
            """.formatted(n, n);
    }

    static final String KULLANAN = """
        class Kullanan {
            static int tavan() { return Sabit.TAVAN; }
            static int taban() { return Sabit.TABAN; }
            static String deger() { return "TAVAN=" + tavan() + " TABAN=" + taban(); }
        }
        """;

    public static void main(String[] args) throws Exception {
        Path d = Olcek.yeniDizin();
        Olcek.derle(d, "Sabit", sabit(100));
        Olcek.derle(d, "Kullanan", KULLANAN);
        for (String y : new String[] {"tavan", "taban"})
            System.out.println("Kullanan." + y + "() icindeki iz: " + Olcek.izler(d, "Kullanan", y));
        System.out.println();
        System.out.println("ilk kosum    : " + Olcek.calistir(d, "Kullanan", "deger"));
        Olcek.derle(d, "Sabit", sabit(200));
        System.out.println("Sabit 200 ile yeniden derlendi, Kullanan derlenmedi");
        System.out.println("ikinci kosum : " + Olcek.calistir(d, "Kullanan", "deger"));
    }
}
Kullanan.tavan() icindeki iz: [sabit yukleme 100]
Kullanan.taban() icindeki iz: [alan okumasi Sabit.TABAN]

ilk kosum    : TAVAN=100 TABAN=100
Sabit 200 ile yeniden derlendi, Kullanan derlenmedi
ikinci kosum : TAVAN=100 TABAN=200

İlk iki satır soruyu yanıtlıyor. tavan yönteminin sınıf dosyasında Sabit sınıfına hiçbir referans yok; yalnız 100 sayısının kendisi var. taban yönteminde ise bir alan okuması duruyor ve o okumanın sahibi Sabittir. Kaynakta iki satır birbirinin aynıdır — ikisi de bir noktadan sonra bir alan adı yazar — ama biri değeri okuyan sınıfa kopyalamış, öteki bir bağ bırakmıştır.

Kopyalamanın bedeli ikinci koşumda görünüyor. Sabit 200 değeriyle yeniden derlendi; Kullanan derlenmedi ve diskte eski hâliyle duruyor. Çalıştırıldığında TABAN yeni değeri, 200’ü veriyor: bağ duruyordu, bağ üzerinden okundu. TAVAN ise hâlâ 100 diyor. Kaynağa bakan biri Sabit içinde 200 görecek, çalışan programda 100 okuyacaktır.

Bu, kursun sorduğu sorunun en sert biçimidir. Karar burada bildirilen tiple çalışma zamanı tipi arasında değil, derleme zamanıyla çalışma zamanı arasında bölünmüştür — ama sonucu aynıdır: iki taraftan hangisinin yanıt verdiğini bilmeyen bir programcı, kaynakta gördüğü değerin çalıştığını sanır. Kusur Sabiti değiştiren kişide de değildir; Kullananın yeniden derlenmemesindedir ve hiçbir yerde bir hata iletisi üretmez.

Sınırlayıcı Ölçüm: final Referans Nesneyi Dondurmaz

Üç kısıt da ölçüldü ve hepsi derleme zamanında duruyor. Şimdi bu tezin sınırı: final bir ada bağlı nesnenin içi ne oluyor?

// Dondur.java — final referans nesneyi dondurmaz
import java.util.Arrays;

public class Dondur {
    static class Kayit {
        String ad;
        int adet;
        Kayit(String ad, int adet) { this.ad = ad; this.adet = adet; }
        @Override public String toString() { return ad + ":" + adet; }
    }

    static final String GOVDE = """
        class Deneme {
            static class Kayit { int adet; }
            void f() { final Kayit k = new Kayit(); %s }
        }
        """;

    public static void main(String[] args) throws Exception {
        final Kayit k = new Kayit("civata", 12);
        final int[] agirlik = {10, 20, 30};
        Kayit ilk = k;
        System.out.println("baslangic        : " + k + " " + Arrays.toString(agirlik));
        k.ad = "somun";
        k.adet = 99;
        agirlik[0] = 99;
        System.out.println("degisiklikten son: " + k + " " + Arrays.toString(agirlik));
        System.out.println("referans ayni mi : " + (k == ilk));
        System.out.println();
        for (String[] y : new String[][] {{"k.adet = 1;", "alani degistirme"},
                                          {"k = new Kayit();", "referansi yeniden baglama"}})
            System.out.printf("%-26s -> derleniyor mu: %s%n", y[1],
                    Olcek.derle(Olcek.yeniDizin(), "Deneme", GOVDE.formatted(y[0])));
    }
}
baslangic        : civata:12 [10, 20, 30]
degisiklikten son: somun:99 [99, 20, 30]
referans ayni mi : true

alani degistirme           -> derleniyor mu: true
referansi yeniden baglama  -> derleniyor mu: false

k bir final yerel değişkendir ve iki alanının ikisi de değişti; agirlik de finaldır ve ilk öğesi değişti. Buna karşılık k == ilk hâlâ doğru: referans hiç değişmedi. Korunan şey nesne değil, ad ile nesne arasındaki bağdır.

Son iki satır ayrımı tek ölçüye indiriyor. Aynı final bildirimi üzerinde alanı değiştiren satır derleniyor, referansı yeniden bağlayan satır derlenmiyor. final, Java Temelleri kursunda ölçülen aktarım modelinin üstüne bir kısıt koyar ve o kısıt yalnız bağlamaya değer: değerin kendisi bir referans olduğunda, referansın gösterdiği nesne bu kısıtın dışında kalır.

Buradan iki sonuç çıkar. Birincisi, final bir alan değiştirilemezlik değildir; bir tipin değiştirilemez olması için alanlarının final olması yetmez, o alanların gösterdiği nesnelerin de değişmemesi gerekir. İkincisi, ölçümün gözlenemediği durum vardır: agirlik yerine bir sayı ya da bir dizgi konsaydı ayrım hiç görünmezdi, çünkü o değerlerin değiştirilebilecek bir içi yoktur. Kısıtın yetip yetmediği, final sözcüğüne değil tipe bağlıdır.

Özet

  • final üç ayrı kısıt koyar ve dokuz denemenin dördü tam bu sözcük yüzünden durur: yeniden atama, yeniden tanımlama ve genişletme.
  • Kısıt “hiç atanmasın” değil, “tam bir kez atansın” biçimindedir; final bir alan yapıcıda atanabilir.
  • final bir yöntem çalışma zamanı tipinin verebileceği yanıt sayısını ikiden bire indirir; kararı öteki tarafa taşımaz, ortadan kaldırır.
  • Sınıf dosyasında final bayrağını sınıf, alan ve yöntem taşır; yerel değişkenin kısıtı hiç yazılmaz, çünkü onu başka bir derlemenin okumasına gerek yoktur.
  • Sabit bir static final alanın değeri okuyan sınıfa yazılır: kaynak 200 olduğu hâlde yeniden derlenmeyen okuyucu 100 okumayı sürdürür, hesaplanan alan ise 200 verir.
  • final bir referans nesneyi dondurmaz; alanları ve dizinin öğeleri değişir, yalnız ad ile nesne arasındaki bağ korunur.

Sonraki Adım

Bu dersin ölçümünde bir kısıt sınıf dosyasına hiç yazılmadı: final bir yerel değişkenin kısıtı yalnız derleyicide durdu ve derleme bittiğinde ortadan kalktı. Öyleyse o kısıt neye yarıyor? Java’da derleyicinin bu sözü yazılmadığı hâlde kendiliğinden istediği bir yer vardır: bir yöntemin gövdesi içinde tanımlanan bir sınıf, dışarıdaki yerel değişkeni kullandığında o değişkenin bir daha atanmamasını şart koşar. Şartın nedeni, o değişkenin oraya kopyalanmasıdır — ve kopyayı taşımak için ortaya kaynakta hiç yazılmamış yeni sınıf dosyaları çıkar. Sonraki ders bu dosyaları sayar: iç içe yazılan bir sınıf kaç ayrı dosya üretiyor, hangileri dış örneğe gizli bir alan taşıyor, ve o gizli alanla kopyalanan değer arasındaki fark ne zaman görünür oluyor?

İ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