İçeriğe geç
academia.sh

Ders 05 / 13

Statik Üyeler ve Başlatma Blokları

Statik yöntem yeniden tanımlanmaz, gizlenir: aynı çağrı yazımı bildirilen tipten seçilir ve boş referans üzerinden yapılan statik çağrı bile çalışır. Sınıf düzeyi durumun tek kopyası, sınıf başlatmasını hangi erişimin tetiklediği ve aynı satırın iki ayrı tarafa gittiği durum ölçülür.

İçindekiler

Önceki ders soyutlamanın iki aracını yan yana koydu: soyut sınıf tek üstle, arayüz çokluyla çalışıyordu ve iki bağımsız arayüz aynı varsayılanı verdiğinde derleyici karar vermeyi reddediyordu. İki aracın da ortak yanı vardı — ikisi de örnek düzeyinde iş görüyordu. Çağrılan her yöntem bir nesneye aitti, dolayısıyla kararı çoğunlukla çalışma zamanı tipi veriyordu.

Bu derste konu sınıfın kendisine ait üyelerdir: statik yöntem, sınıf değişkeni ve başlatma bloğu. Nesne olmadan çağrılan bir yöntemin arkasında çalışma zamanı tipi yoktur; geriye yalnız derleyicinin gördüğü ad kalır. Sorular üç tanedir: aynı satır bir örnek yöntemi ile bir statik yöntem için farklı taraflara mı gidiyor, sınıf düzeyi durum kaç kopya taşıyor, ve bir sınıfın başlatma bloğunu hangi erişim tetikliyor?

Ölçüm Çekirdeği

Kursun çekirdeği aynı ikilidir: bildirilen tip Ust, çalışma zamanı tipi Alt. Bu derste ikiliye iki üye eklenir — bir sınıf değişkeni (uretilen) ve bir örnek alanı (sira) — ama var olan üyelerin davranışı değişmez.

  • SN21 — İki tip bilerek ayrılmıştır; kâhin kurgunun kendisidir. Ust d = new Alt() yazıldığında bir yanıtın hangi tipten geldiğini yanıta bakmadan biliriz.
  • SN22Ust ve Alt ikilisine yalnız üye eklenir; ortak tanımdaki üyelerin ürettiği değerler değişmez.
  • SN23 — Başlatma ölçümünde her senaryo ayrı bir yükleyicide çalışır; bir senaryonun başlattığı sınıf ötekini etkilemez.
  • SN24 — Başlatma adımları çalıştıklarında adlarını basar; sıra bir çıkarım değil dökümdür.
  • SN25 — Sayılan tek şey kararın tarafıdır. Komut sayısı ve eklenen adım bu kursta sayılmaz; sınıf dosyası yalnız kararın nerede donduğunu göstermek için okunur ve okuma derleyiciyi program içinden çağırıp ürettiği dosyayı standart kitaplıkla ayrıştırır.
// Ust.java — kursun çekirdeği: örnek yöntemi ile statik yöntem yan yana
class Ust {
    static int uretilen = 0;
    final int sira;
    Ust() { sira = ++uretilen; }
    String ad() { return "ust yontem"; }
    static String durum() { return "ust statik"; }
}
// Alt.java — aynı iki adı yeniden bildiren alt sınıf
class Alt extends Ust {
    @Override String ad() { return "alt yontem"; }
    static String durum() { return "alt statik"; }
}

ad yönteminde @Override işareti vardır ve derleyici onu kabul eder: bu bir yeniden tanımlamadır. durum yönteminde o işaret yoktur — konsaydı derleme dururdu. Statik bir yöntem yeniden tanımlanamaz; aynı adla yeniden bildirildiğinde olan şeyin adı gizlemedir, ve gizlemenin alanlarda gördüğümüz davranışı burada yöntemlerde tekrarlanır.

Aynı Yazım, İki Taraf

İlk ölçüm beş çağrıyı yan yana koyar. Üçü Ust bildirilmiş bir referans üzerinden, biri Alt bildirilmiş bir referans üzerinden, sonuncusu ise hiçbir nesneye bağlanmamış bir referans üzerinden yapılır.

// Taraf.java — aynı çağrı yazımı hangi tipten seçiliyor
public class Taraf {
    @SuppressWarnings("static-access")
    public static void main(String[] args) {
        Ust d = new Alt();
        Alt a = new Alt();
        Ust bos = null;
        System.out.printf("%-30s %-12s %s%n", "yazilan cagri", "sonuc", "karari veren");
        System.out.printf("%-30s %-12s %s%n", "d.ad()      [bildirilen Ust]", d.ad(), "calisma zamani tipi");
        System.out.printf("%-30s %-12s %s%n", "d.durum()   [bildirilen Ust]", d.durum(), "bildirilen tip");
        System.out.printf("%-30s %-12s %s%n", "a.ad()      [bildirilen Alt]", a.ad(), "calisma zamani tipi");
        System.out.printf("%-30s %-12s %s%n", "a.durum()   [bildirilen Alt]", a.durum(), "bildirilen tip");
        System.out.printf("%-30s %-12s %s%n", "bos.durum() [bos referans]", bos.durum(), "bildirilen tip");
        System.out.println();
        System.out.println("ornek alani -> d.sira " + d.sira + " | a.sira " + a.sira);
        System.out.println("sinif alani -> Ust.uretilen " + Ust.uretilen + " | Alt.uretilen " + Alt.uretilen);
        Alt.uretilen = 99;
        System.out.println("Alt.uretilen = 99 sonrasi Ust.uretilen: " + Ust.uretilen);
    }
}
yazilan cagri                  sonuc        karari veren
d.ad()      [bildirilen Ust]   alt yontem   calisma zamani tipi
d.durum()   [bildirilen Ust]   ust statik   bildirilen tip
a.ad()      [bildirilen Alt]   alt yontem   calisma zamani tipi
a.durum()   [bildirilen Alt]   alt statik   bildirilen tip
bos.durum() [bos referans]     ust statik   bildirilen tip

ornek alani -> d.sira 1 | a.sira 2
sinif alani -> Ust.uretilen 2 | Alt.uretilen 2
Alt.uretilen = 99 sonrasi Ust.uretilen: 99

İlk iki satır aynı d referansı üzerinden yazılmıştır ve karşıt yanıt verir. d.ad() alt yontem döner: nesne bir Alttır ve yöntem oradan seçilmiştir. d.durum() ust statik döner: nesne yine aynı nesnedir, ama seçim dnin bildirilen tipinden yapılmıştır. Aradaki tek fark, Alt içindeki iki bildirimden birinin başında static sözcüğünün bulunmasıdır.

Beşinci satır bu okumanın kanıtıdır. bos hiçbir nesneye bağlı değildir; buna rağmen bos.durum() çağrısı bir sonuç verir ve hiçbir hata düşmez. Referans okunmamıştır bile. Statik bir çağrının yazımında bir referans bulunması yalnızca bir sözdizim kolaylığıdır; derleyici o referansın tipini alır, değerini atar.

Alt bölümdeki üç satır ikinci soruyu yanıtlar. İki nesne üretildi ve her biri kendi sira değerini taşıyor: 1 ve 2. Buna karşılık uretilen iki ad üzerinden okunduğunda da aynı sayıyı, 2yi veriyor. Alt.uretilen diye yazılan ad ayrı bir sayaç değildir; Alt.uretilen = 99 yazıldığında Ust.uretilen de 99 olur. Örnek alanı nesne başına bir kopya taşır, sınıf değişkeni ise kalıtım zinciri boyunca tek kopyadır.

Sınıf Dosyasındaki Kanıt

Davranış ölçüldü; şimdi kararın nerede donduğuna bakılabilir. Ölçüm çekirdeği bir kaynak metnini derler, ürettiği sınıf dosyasını okur ve bir yöntemin gövdesindeki her çağrının iki şeyini döndürür: çağrının sınıf dosyasına yazılan sahibi ve çağrının türü.

// Olcek.java — sınıf dosyasındaki çağrının sahibi ve türü
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Olcek {
    record Cagri(String sahip, String ad, String tur) {}

    static Path 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 d;
    }

    static List<Cagri> cagrilar(String kaynak, String sinif, String yontem) throws Exception {
        ClassModel cm = ClassFile.of().parse(derle(kaynak, sinif).resolve(sinif + ".class"));
        List<Cagri> c = 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 InvokeInstruction iv)
                    c.add(new Cagri(kisa(iv.owner().asInternalName()),
                            iv.name().stringValue(), tur(iv.opcode())));
        }
        return c;
    }

    static String tur(Opcode o) {
        return switch (o) {
            case INVOKESTATIC -> "statik cagri";
            case INVOKEVIRTUAL -> "sanal cagri";
            default -> "dogrudan cagri";
        };
    }

    static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}
// Sahip.java — çağrının sahibi sınıf dosyasına ne olarak yazıldı
public class Sahip {
    static final String KAYNAK = """
        class Ust {
            String ad() { return "ust yontem"; }
            static String durum() { return "ust statik"; }
        }
        class Alt extends Ust {
            @Override String ad() { return "alt yontem"; }
            static String durum() { return "alt statik"; }
        }
        class Cagiran {
            static String ustOrnek(Ust d) { return d.ad(); }
            static String altOrnek(Alt a) { return a.ad(); }
            @SuppressWarnings("static-access")
            static String ustStatik(Ust d) { return d.durum(); }
            @SuppressWarnings("static-access")
            static String altStatik(Alt a) { return a.durum(); }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.printf("%-11s %-24s %s%n", "yontem", "sinif dosyasindaki cagri", "cagri turu");
        for (String y : new String[] {"ustOrnek", "altOrnek", "ustStatik", "altStatik"})
            for (Olcek.Cagri c : Olcek.cagrilar(KAYNAK, "Cagiran", y))
                System.out.printf("%-11s %-24s %s%n", y, c.sahip() + "." + c.ad(), c.tur());
    }
}
yontem      sinif dosyasindaki cagri cagri turu
ustOrnek    Ust.ad                   sanal cagri
altOrnek    Alt.ad                   sanal cagri
ustStatik   Ust.durum                statik cagri
altStatik   Alt.durum                statik cagri

Dört satırın hepsinde sahip olarak bildirilen tip yazılıdır; bu, tablonun kolay yanlış okunan yeridir. ustOrnek yönteminde de sahip Usttur, oysa çalışan yöntem Altınkidir. Ayrım sahipte değil türdedir. Sanal çağrıda sahip yalnızca bir başlangıç noktasıdır: sanal makine çağrıyı nesnenin gerçek sınıfından başlayarak yeniden çözer, dolayısıyla sınıf dosyasındaki Ust.ad yazısı sonucu belirlemez. Statik çağrıda ise çözme yoktur; sahip sonucun kendisidir ve Alt.durum ile Ust.durum iki ayrı hedeftir.

Bu, dersin ana ölçümünü tek cümleye indirir: statik çağrının hedefi derleme sırasında yazılmıştır ve çalışma zamanında değiştirilecek bir yeri yoktur.

Başlatmayı Hangi Erişim Tetikler

Sınıf düzeyi durum bir yerde kurulmalıdır ve o yer başlatma bloğudur. Bu bloğun ne zaman çalıştığı — programın açılışında değil, sınıfa ilk kez ihtiyaç duyulduğunda ve bir kez — Java Temelleri kursunda ölçüldü ve burada tekrarlanmaz. Buradaki soru ötekidir: ihtiyaç duyulmasını hangi yazım sayıyor?

Aşağıdaki ölçüm dört ayrı erişimi dört ayrı yükleyicide çalıştırır, böylece her senaryo temiz bir başlangıçtan başlar. Başlatma blokları çalıştıklarında adlarını basar.

// Tetik.java — sınıf başlatmasını hangi erişim tetikler
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;

public class Tetik {
    static final String KAYNAK = """
        class Ust {
            static { System.out.println("  Ust baslatildi"); }
            static int sayac = 0;
            static final int TAVAN = 100;
        }
        class Alt extends Ust {
            static { System.out.println("  Alt baslatildi"); }
            static int altSayac = 0;
        }
        class S1 { static void calis() { System.out.println("  okunan deger: " + Alt.sayac); } }
        class S2 { static void calis() { System.out.println("  okunan deger: " + Ust.TAVAN); } }
        class S3 { static void calis() { System.out.println("  okunan deger: " + Alt.altSayac); } }
        class S4 { static void calis() { new Alt(); System.out.println("  bir Alt uretildi"); } }
        """;

    static void senaryo(Path d, String surucu, String yazim) throws Exception {
        System.out.println("yazilan erisim: " + yazim);
        try (URLClassLoader cl = new URLClassLoader(new URL[] {d.toUri().toURL()}, null)) {
            var m = cl.loadClass(surucu).getDeclaredMethod("calis");
            m.setAccessible(true);
            m.invoke(null);
        }
        System.out.println();
    }

    public static void main(String[] args) throws Exception {
        Path d = Olcek.derle(KAYNAK, "Ust");
        senaryo(d, "S1", "Alt.sayac     [alan Ust icinde bildirildi]");
        senaryo(d, "S2", "Ust.TAVAN     [derleme zamani sabiti]");
        senaryo(d, "S3", "Alt.altSayac  [alan Alt icinde bildirildi]");
        senaryo(d, "S4", "new Alt()");
    }
}
yazilan erisim: Alt.sayac     [alan Ust icinde bildirildi]
  Ust baslatildi
  okunan deger: 0

yazilan erisim: Ust.TAVAN     [derleme zamani sabiti]
  okunan deger: 100

yazilan erisim: Alt.altSayac  [alan Alt icinde bildirildi]
  Ust baslatildi
  Alt baslatildi
  okunan deger: 0

yazilan erisim: new Alt()
  Ust baslatildi
  Alt baslatildi
  bir Alt uretildi

Birinci senaryoda kaynakta yazılan ad Alttır, ama başlatılan sınıf **Ust**tur ve Alt hiç başlatılmaz. Yazılan ad tetikleyici değildir; alanın bildirildiği sınıf tetikleyicidir ve bu bildirim derleme sırasında bulunur. Üçüncü senaryo aynı yazımla ama Alt içinde bildirilen bir alanla yapılır ve bu kez iki sınıf da başlatılır, üstelik önce üst sınıf. Aynı yazım, iki ayrı sonuç; ayrımı yapan şey adın kendisi değil, adın hangi sınıfa çözüldüğüdür.

İkinci senaryo daha da ileri gider: hiçbir sınıf başlatılmaz, ama değer okunur ve doğru gelir. TAVAN bir static final alandır ve değeri derleme sırasında hesaplanabilen bir sabittir; derleyici o değeri okuyan tarafa yazmıştır, dolayısıyla Ust sınıfına çalışma zamanında hiç dokunulmamıştır. Sınıf değişkeni okumak sınıfı başlatır, sabit okumak başlatmaz — ve iki satır kaynakta birbirinden ayırt edilemez.

Sınırlayıcı Ölçüm: Aynı Satır İki Tarafa Gider

Şimdiye kadarki ölçümler bir örnek yöntemiyle bir statik yöntemi ayrı satırlarda çağırdı. Asıl kusur ikisi yan yana konduğunda doğar: tek bir satır, tek bir referans, iki ayrı karar.

// Yanyana.java — aynı satır iki ayrı tarafa gider
public class Yanyana {
    @SuppressWarnings("static-access")
    static String satir(Ust d) { return d.ad() + " / " + d.durum(); }

    @SuppressWarnings("static-access")
    public static void main(String[] args) {
        for (Ust d : new Ust[] {new Ust(), new Alt()})
            System.out.printf("Ust d = new %-6s -> %s%n", d.getClass().getSimpleName() + "()", satir(d));
        Alt a = new Alt();
        System.out.printf("Alt a = new %-6s -> %s%n", "Alt()", a.ad() + " / " + a.durum());
    }
}
Ust d = new Ust()  -> ust yontem / ust statik
Ust d = new Alt()  -> alt yontem / ust statik
Alt a = new Alt()  -> alt yontem / alt statik

satir yöntemi hiç değişmedi; üç satırın üçünde de aynı gövde çalıştı. Buna karşılık üç sonuç üç ayrı bileşim veriyor. Birinci satırda iki yanıt uyuşuyor, çünkü bildirilen tip ile çalışma zamanı tipi aynı. Üçüncü satırda da uyuşuyor, aynı nedenle. Ayrım yalnız ikinci satırda görünüyor — ve tam da orada bir programcının beklediği okuma bozuluyor: nesne bir Alt olduğu hâlde durum bilgisi Usttan geliyor.

Buradan iki sınır birden çıkar. Birincisi, kuralın tersine döndüğü durum yoktur; statik üye her zaman bildirilen tipten seçilir. İkincisi ve daha önemlisi, ayrım çoğu programda gözlenemez: bildirilen tip ile çalışma zamanı tipi ayrılmadıkça iki taraf aynı yanıtı verir ve fark hiç ortaya çıkmaz. Bir sınıfın bir alt sınıfı yazıldığı gün, o güne kadar doğru çalışan bir satır sessizce yön değiştirebilir. Kusurun bedeli yazan yerde değil, alt sınıfın kullanıldığı yerde ödenir; ve orada görünen şey bir hata değil, yanlış bir değerdir.

Özet

  • Statik yöntem yeniden tanımlanmaz, gizlenir: d.ad() çalışma zamanı tipinden alt yontem, d.durum() bildirilen tipten ust statik verir; iki satır arasındaki tek fark bildirimdeki static sözcüğüdür.
  • Statik çağrı referansın değerini hiç okumaz; boş referans üzerinden yapılan bos.durum() çağrısı sonuç verir ve hata düşmez.
  • Örnek alanı nesne başına bir kopya taşır (sira 1 ve 2), sınıf değişkeni kalıtım zinciri boyunca tektir: Alt.uretilen = 99 yazıldığında Ust.uretilen de 99 olur.
  • Sınıf dosyasında dört çağrının da sahibi bildirilen tiptir; ayrım türdedir. Sanal çağrıda sahip bir başlangıç noktasıdır, statik çağrıda sonucun kendisidir.
  • Sınıf başlatmasını yazılan ad değil, alanın bildirildiği sınıf tetikler: Alt.sayac yalnız Ustu başlatır, Ust.TAVAN ise hiçbir sınıfı başlatmaz.
  • Ayrım yalnız bildirilen tip ile çalışma zamanı tipi ayrıldığında görünür; üç satırlık ölçümün ikisinde iki taraf aynı yanıtı verir ve fark gözlenemez.

Sonraki Adım

Bu dersin son ölçümünde bir değer sınıf başlatmasını hiç tetiklemeden okundu: TAVAN bir static final alandı ve değeri okuyan tarafa geçmişti. O tek sözcük, final, burada yalnız bir yan etki olarak göründü; oysa üç ayrı işi birden yapar ve üçünde de kararı derleme zamanına taşır. Bir yerel değişkene yeniden atama yapılmasını, bir yöntemin yeniden tanımlanmasını ve bir sınıfın genişletilmesini aynı sözcük engeller. Sonraki ders bu üç kısıtı ayrı ayrı ölçer, sabit değerin okuyan sınıfa gömülmesinin sonucunu gösterir ve final bir referansın nesneyi dondurmadığını — yani değiştirilemezlik olmadığını — aynı ölçümle sınırlar.

İ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