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. - SN22 —
UstveAltikilisine 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 bildirimdekistaticsö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 (
sira1 ve 2), sınıf değişkeni kalıtım zinciri boyunca tektir:Alt.uretilen = 99yazıldığındaUst.uretilende 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.sayacyalnızUstu başlatır,Ust.TAVANise 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.