İçeriğe geç
academia.sh

Ders 02 / 10

Program Yaşam Döngüsü

Sanal makine dört koşulu birden sağlayan tek bir imzayı giriş noktası sayar; sınıf başlatma bloğu programın başında değil sınıfın ilk kullanıldığı anda ve yalnız bir kez çalışır; kaynakta iki ad yazılan bir numaralandırma ise sınıf dosyasında üç alan ve beş yöntem taşır.

İçindekiler

Önceki ders sınıf dosyasının ne olduğunu ölçtü: kaynakta yazılan on iki yöntemin sekizi orada kaynakta bulunmayan bir adım taşıyordu, ve kaynakta hiç yazılmamış üç komutluk bir yapıcı da tabloda duruyordu. O yapıcının ne zaman çağrıldığı söylenmedi; sınıfın kendi başlatma işinin nerede durduğu da sorulmadı.

Bu ders o boşluğu ölçer. Bir sınıf dosyası diskte durduğu sürece hiçbir şey yapmaz; sanal makine onu yükler, hazırlar ve içindeki komutları bir sıraya göre çalıştırır. Sorular üç tanedir: sanal makine hangi imzayı giriş noktası (entry point) sayıyor, bir sınıfın başlatma işi ile bir örneğin başlatma işi hangi sırayla çalışıyor, ve kaynakta yazılmamış hangi üyeler bu sıraya katılıyor?

Ölçüm Çekirdeği

Önceki dersin Olcek çekirdeği komut sayıyordu. Bu derste sayılan şey komut değil üyedir: sınıf dosyasındaki alanlar ve yöntemler, her yöntemin imzası, genel ve statik olup olmadığı, dönüş tipi. Çekirdek aynı işi yapar — derler ve sınıf dosyasını okur — ama bu derste kullanılmayan komut çözümlemesini taşımaz.

  • CZ6 — Giriş noktası ölçümü sınıf dosyasının kendisinden okunur: yöntemin adı, belirteçleri, dönüş tipi ve parametre listesi. Ölçüm çalıştırıcıyı çağırmaz; dilin belirttiği dört koşulu gerçek sınıf dosyalarına karşı sınar.
  • CZ7 — Sıra ölçümünde kâhin nesnenin kendi kaydıdır: her başlatma adımı çalıştığında adını ortak bir listeye yazar, dolayısıyla sıra bir çıkarım değil bir dökümdür.
  • CZ8 — Kayıt listesi program sonunda tek seferde basılır; basma işi ölçülen sıraya karışmaz.
  • CZ9 — Üye dökümü sınıf dosyasındaki bütün üyeleri sayar; kaynakta yazılanlar da listededir. Eklenen üye, listeyle kaynağın karşılaştırılmasından okunur.
  • CZ10 — Ölçüm ortama bağlı hiçbir veri okumaz: süre, bellek adresi, kimlik ve yol yazılmaz; sayılan tek şey üye sayısı ile çalışma sırasıdır.
// Olcek.java — sınıf dosyasının üye dökümü: alanlar, yöntem imzaları ve belirteçleri
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.constant.MethodTypeDesc;
import java.lang.reflect.AccessFlag;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Olcek {
    record Uye(String ad, String imza, boolean genel, boolean statik, String donus) {}
    record Sinif(List<String> alan, List<Uye> yontem) {}

    static Sinif oku(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);
        ClassModel cm = ClassFile.of().parse(d.resolve(sinif + ".class"));
        List<String> alan = new ArrayList<>();
        for (FieldModel f : cm.fields()) alan.add(f.fieldName().stringValue());
        List<Uye> yontem = new ArrayList<>();
        for (MethodModel m : cm.methods()) {
            MethodTypeDesc t = m.methodTypeSymbol();
            List<String> p = new ArrayList<>();
            for (var a : t.parameterList()) p.add(a.displayName());
            yontem.add(new Uye(m.methodName().stringValue(), "(" + String.join(", ", p) + ")",
                    m.flags().has(AccessFlag.PUBLIC), m.flags().has(AccessFlag.STATIC),
                    t.returnType().displayName()));
        }
        return new Sinif(alan, yontem);
    }

    static List<String> adlar(Sinif s) {
        List<String> a = new ArrayList<>();
        for (Uye u : s.yontem()) a.add(u.ad() + u.imza());
        return a;
    }
}

Giriş Noktası: Dört Koşulu Birden Sağlayan İmza

Bir programın nereden başladığı Java’da dosyanın ilk satırı değildir. Çalıştırıcıya bir sınıf adı verilir; o sınıf yüklenir ve içinde belirli bir yöntem aranır. Aranan şey bir ad değil, bir imzadır: yöntemin adı main olacak, genel olacak, statik olacak, hiçbir değer döndürmeyecek ve tek parametresi bir dizgi dizisi olacaktır. Dört koşul birden sağlanmazsa o yöntem giriş noktası değildir.

Aşağıdaki ölçüm beş ayrı kaynağı derler ve her birinin sınıf dosyasındaki main yöntemini bu koşullara karşı sınar. Beşinin de kaynağı kusursuzdur ve beşi de sorunsuz derlenir — ayrım derleme aşamasında değil, yükleme sonrasında ortaya çıkar.

// Ana.java — sanal makinenin giriş noktası saydığı imza
import java.util.*;

public class Ana {
    public static void main(String[] args) throws Exception {
        Map<String, String> aday = new LinkedHashMap<>();
        aday.put("Dogru",     "class Dogru { public static void main(String[] a) { } }");
        aday.put("Statiksiz", "class Statiksiz { public void main(String[] a) { } }");
        aday.put("Genelsiz",  "class Genelsiz { static void main(String[] a) { } }");
        aday.put("Dizisiz",   "class Dizisiz { public static void main(String a) { } }");
        aday.put("Donduren",  "class Donduren { public static int main(String[] a) { return 0; } }");
        System.out.printf("%-10s %-11s %-6s %-6s %-6s %s%n",
                "sinif", "imza", "genel", "statik", "donus", "giris noktasi");
        for (var g : aday.entrySet())
            for (Olcek.Uye u : Olcek.oku(g.getValue(), g.getKey()).yontem())
                if (u.ad().equals("main")) {
                    boolean giris = u.genel() && u.statik()
                            && u.donus().equals("void") && u.imza().equals("(String[])");
                    System.out.printf("%-10s %-11s %-6s %-6s %-6s %s%n", g.getKey(), u.imza(),
                            u.genel(), u.statik(), u.donus(), giris);
                }
    }
}
sinif      imza        genel  statik donus  giris noktasi
Dogru      (String[])  true   true   void   true
Statiksiz  (String[])  true   false  void   false
Genelsiz   (String[])  false  true   void   false
Dizisiz    (String)    true   true   void   false
Donduren   (String[])  true   true   int    false

Beş sınıfın biri giriş noktası taşıyor. Kalan dördü tek bir koşulda düşüyor ve her düşüş ayrı bir şey söylüyor. Statiksiz statik değildir: statik olmayan bir yöntemi çağırmak için önce bir örnek gerekir, oysa programın başında henüz hiçbir örnek yoktur — sanal makine yaratmadan çağıramaz. Genelsiz sınıfın dışından erişilebilir değildir; çağıran taraf sınıfın kendisi olmadığına göre erişim kapalıdır. Dizisiz bir dizgi alır, dizgi dizisi değil: komut satırı argümanları bir dizidir ve sayısı önceden bilinmez. Donduren bir değer döndürür; programın sonlanma bilgisi dönüş değeriyle değil, ayrı bir çağrıyla verilir.

Parametrenin dizgi dizisi olması bir biçim ayrıntısı değildir: programa dışarıdan verilen argümanlar oraya, sırasıyla ve sayısı önceden bilinmeden gelir. Giriş noktası, dış dünyadan gelen tek veriyi bu tek parametrede karşılar.

Buradaki asıl gözlem, kusurun nerede yakalandığıdır. Beş kaynağın hepsi derlenmiştir; sınıf dosyaları oluşmuştur. Bir yöntemin giriş noktası olup olmadığı derleyicinin sorusu değildir, sanal makinenin sorusudur. Bu, önceki dersin kurduğu ayrımın öbür yüzüdür: derleyici kaynağa adım ekler, sanal makine ise sınıf dosyasında ne bulacağına dair kendi koşullarını taşır.

Sınıf Bir Kez, Örnek Her Seferinde

Giriş noktası bulunduktan sonra sıra çalışmaya gelir. Bir sınıfın hazırlanmasında iki ayrı düzey vardır: sınıf düzeyi iş bir kez yapılır, örnek düzeyi iş her yeni nesnede yeniden yapılır. Her düzeyin de iki adımı vardır — alan başlatıcılar ve başlatma blokları (static initializer). Bunların üstüne bir de yapıcı (constructor) gelir.

Ölçüm için kursun somut ekseni kullanılır: bir depo kaydını tutan Kayit. Üç alanı vardır — ad, adet ve gram cinsinden agirlik. Her başlatma adımı çalıştığında adını ortak bir listeye yazar; liste sonda basılır.

// Sira.java — sınıf başlatma bloğu, alan başlatıcı ve yapıcı hangi sırayla çalışır
import java.util.*;

public class Sira {
    static final List<String> IZ = new ArrayList<>();
    static void iz(String s) { IZ.add(s); }

    static class Kayit {
        static { iz("sinif: baslatma blogu"); }
        static final long BIRIM_AGIRLIK = agirlikBirimi();
        String ad;
        int adet = adetBasla();
        long agirlik;
        { iz("ornek: baslatma blogu"); }

        Kayit(String ad, long agirlik) {
            iz("yapici: " + ad);
            this.ad = ad;
            this.agirlik = agirlik;
        }

        static long agirlikBirimi() { iz("sinif: alan baslatici"); return 1L; }
        static int adetBasla() { iz("ornek: alan baslatici"); return 0; }
    }

    public static void main(String[] args) {
        iz("-- main basladi");
        new Kayit("civata", 12L);
        iz("-- ilk ornek bitti");
        new Kayit("somun", 7L);
        IZ.forEach(System.out::println);
    }
}
-- main basladi
sinif: baslatma blogu
sinif: alan baslatici
ornek: alan baslatici
ornek: baslatma blogu
yapici: civata
-- ilk ornek bitti
ornek: alan baslatici
ornek: baslatma blogu
yapici: somun

Dökümün ilk satırı beklenmedik olanıdır: main sınıf başlatma bloğundan önce başlıyor. Bir sınıfın hazırlanması program açılışında değil, o sınıfa ilk kez gerçekten ihtiyaç duyulduğunda yapılır — burada ilk new çağrısında. Bir sınıf hiç kullanılmazsa başlatma bloğu hiç çalışmaz.

İkinci gözlem tekrarın yokluğudur. İkinci new çağrısında sinif: ile başlayan iki satır yeniden görünmüyor; yalnız örnek düzeyi adımlar tekrarlanıyor. Sınıf düzeyi iş, kaç örnek üretilirse üretilsin bir kez yapılır. Sayı olarak: iki örnek için sınıf düzeyi 2 adım, örnek düzeyi 6 adım çalıştı.

Dökümdeki sinif: alan baslatici satırı ayrıca bir ayrımı görünür kılıyor. BIRIM_AGIRLIK alanı static final bildirilmiştir, yani bir daha değişmeyecektir — buna karşın değeri sınıf başlatma adımında çalışarak üretiliyor, çünkü bir yöntem çağrısından geliyor. Değeri derleme zamanında hesaplanabilen bir sabit olsaydı önceki dersin sabit katlama ölçümündeki yola girer ve burada hiçbir adım görünmezdi. Bir alanın değişmez olması, değerinin çalışmadan elde edildiği anlamına gelmez.

Üçüncüsü sıranın kendisidir. Her iki düzeyde de önce alan başlatıcı, sonra başlatma bloğu çalışıyor; ikisi de kaynaktaki yazılma sırasını izliyor. Yapıcının gövdesi bunların sonuncusudur — yani bir yapıcı, gövdesi çalışmaya başladığında alanların çoktan başlatılmış olduğunu varsayabilir. Bir alanı yapıcıda atamak ile bildiriminde atamak aynı işi yapmaz: ikincisi önce çalışır, birincisi onun üzerine yazar.

Kaynakta Yazılmayan Üyeler

Önceki dersin tablosunda kaynakta hiç yazılmamış bir <init> vardı. Şimdi ona doğrudan bakılabilir — ve yanına, çok daha fazlasını ekleyen bir biçim konabilir. Kaynakta iki ad yazılan bir numaralandırma (enum), sınıf dosyasında kaç üye taşır?

// Uye.java — kaynakta yazılmayan üyeler ve yapıcı yazıldığında olan
public class Uye {
    public static void main(String[] args) throws Exception {
        Olcek.Sinif bos = Olcek.oku("class Depo { }", "Depo");
        System.out.println("class Depo { }            -> alan " + bos.alan()
                + ", yontem " + Olcek.adlar(bos));
        Olcek.Sinif kendi = Olcek.oku("class Depo { Depo(int adet) { } }", "Depo");
        System.out.println("class Depo { Depo(int) }  -> alan " + kendi.alan()
                + ", yontem " + Olcek.adlar(kendi));

        Olcek.Sinif y = Olcek.oku("enum Yon { KUZEY, GUNEY }", "Yon");
        System.out.println();
        System.out.println("kaynakta yazilan ad sayisi: 2");
        System.out.println("sinif dosyasindaki alanlar (" + y.alan().size() + "): " + y.alan());
        System.out.println("sinif dosyasindaki yontemler (" + y.yontem().size() + "): "
                + Olcek.adlar(y));
    }
}
class Depo { }            -> alan [], yontem [<init>()]
class Depo { Depo(int) }  -> alan [], yontem [<init>(int)]

kaynakta yazilan ad sayisi: 2
sinif dosyasindaki alanlar (3): [KUZEY, GUNEY, $VALUES]
sinif dosyasindaki yontemler (5): [values(), valueOf(String), <init>(String, int), $values(), <clinit>()]

Gövdesi tümüyle boş bir sınıf, sınıf dosyasında bir yöntem taşıyor: parametresiz bir yapıcı. Kaynakta bir tek karakter yazılmadan bir üye eklenmiştir; bunun adı varsayılan yapıcıdır ve tek işi taban sınıfın yapıcısını çağırmaktır. Önceki dersin tablosundaki üç komutluk <init> satırı buydu.

Numaralandırma çok daha ileri gider. Kaynakta yazılan iki ada karşılık sınıf dosyasında üç alan ve beş yöntem var. KUZEY ile GUNEY yazılmıştır; $VALUES yazılmamıştır. Yöntemlerin hiçbiri yazılmamıştır: values ile valueOf numaralandırmanın belirtimde tanımlanan yüzeyidir, $values onlara hizmet eder, <init> iki parametre alır — oysa kaynakta hiçbir yapıcı çağrısı yazılmamıştır — ve <clinit> bir önceki bölümde ölçtüğümüz sınıf başlatma bloğunun sınıf dosyasındaki adıdır. Numaralandırmanın sabitleri işte orada kurulur.

Böylece kursun üçüncü iddiası doğuyor: eklenen şey yalnız adım değil, üyedir. Bir sınıfın yüzeyi, kaynağında yazılan üyelerden ibaret değildir.

Sınırlayıcı Ölçüm: Yazınca Kaybolan Üye

Çıktının ikinci satırı tezin sınırını çiziyor. Depo sınıfına tek parametreli bir yapıcı yazıldığında sınıf dosyasında yine bir yöntem var, ama bu sefer o yöntem <init>(int) — yani yazdığımızın kendisi. Parametresiz yapıcı kaybolmuştur.

Bunun görünen sonucu vardır ve sessiz değildir. Aynı çağrıyı iki Depo biçiminin yanına koyup derlemeyi deneyelim.

// Etki.java — eklenen yapıcı kaybolunca çağıran tarafta ne oluyor
public class Etki {
    static String dene(String kaynak) {
        try { Olcek.oku(kaynak, "Depo"); return "derlendi"; }
        catch (Exception e) { return "derlenmedi"; }
    }

    public static void main(String[] args) {
        String kul = " class Kul { static Depo yap() { return new Depo(); } }";
        System.out.println("yapici yazilmamis + new Depo() -> " + dene("class Depo { }" + kul));
        System.out.println("yapici yazilmis   + new Depo() -> "
                + dene("class Depo { Depo(int adet) { } }" + kul));
    }
}
yapici yazilmamis + new Depo() -> derlendi
yapici yazilmis   + new Depo() -> derlenmedi

Çağıran taraf iki durumda da aynı satırı yazıyor, ve satırın kendisi değişmediği hâlde biri derleniyor öbürü derlenmiyor. Depo sınıfına eklenen bir yetenek kaldırılmadı; kaynağa bir yapıcı eklendiği için eklenen yapıcı ortadan kalktı. Derleyici bir üyeyi yalnızca hiçbiri yazılmamışken koyar; yazılan tek bir yapıcı, eklenen yapıcıyı yerinden eder.

Eklenen adımın koşulu buradan da okunur. Derleyici bir boşluğu doldurur; boşluk doldurulduğunda ekleme yapılmaz. Bir sınıfın dosyasında ne bulacağımızı bilmek için kaynakta ne yazdığımıza değil, ne yazmadığımıza bakmak gerekir.

Özet

  • Bir sınıf dosyası ancak yüklendiğinde iş yapar; giriş noktası dört koşulu birden sağlayan tek bir imzadır — genel, statik, değer döndürmeyen, tek parametresi dizgi dizisi olan main.
  • Beş aday sınıfın hepsi sorunsuz derlenir, yalnız biri giriş noktası taşır: bu koşullar derleyicinin değil, sanal makinenin sorusudur.
  • Sınıf düzeyi başlatma programın açılışında değil, sınıfa ilk kez ihtiyaç duyulduğunda ve yalnız bir kez çalışır; iki örnek için sınıf düzeyi 2, örnek düzeyi 6 adım sayıldı.
  • Her iki düzeyde sıra aynıdır: önce alan başlatıcı, sonra başlatma bloğu, en sonda yapıcının gövdesi.
  • Gövdesi boş bir sınıf sınıf dosyasında bir varsayılan yapıcı taşır; kaynakta iki ad yazılan bir numaralandırma üç alan ve beş yöntem taşır — eklenen şey yalnız adım değil, üyedir.
  • Tek parametreli bir yapıcı yazıldığında eklenen parametresiz yapıcı kaybolur: derleyici yalnız hiçbiri yazılmamış olan üyeyi ekler.

Sonraki Adım

Bu derste bir sınıfın ne zaman ve hangi sırayla çalıştığı ölçüldü, ama sanal makinenin o sınıfı nasıl bulduğu hiç sorulmadı. Ölçümlerde geçen Depo ve Kayit adları tek başlarına yazıldı; oysa bir sınıf dosyasındaki her tip adı, önceki derste gördüğümüz gibi, tam biçimiyle taşınıyordu. Adın kısası ile tamı arasındaki fark nerede kapanıyor — derleme zamanında mı, yükleme sırasında mı? Sonraki ders bunu ölçer: aynı program içe aktarımla ve tam nitelikli adla yazıldığında üretilen sınıf dosyaları birbirinden ayırt edilebiliyor mu, ve paket adı bu dosyada ne değiştiriyor?

İ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