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.