Ders 01 / 13
Sınıf, Nesne ve Yapıcı
Yapıcı zinciri üstten alta çalışır ve zincirin üst halkasından çağrılan yeniden tanımlı bir yöntem çalışma zamanı tipinden gelir: alt sınıfın alanları henüz atanmadığı için sonuç `alt: null x0` çıkar, kırılgan bir sürümde nesne hiç teslim edilmez — özel ve `final` yöntemde ise aynı tuzak doğmaz.
İçindekiler
Java Temelleri kursu sınıfı yalnız bir kap olarak kullandı: ölçülen şey kaynağın sınıf dosyasına neyi bıraktığıydı, sınıfın kendisi ölçüme girmedi. Kurs kapanışında aktarımın tek modeli okunurken bile sınıf bir çerçeveydi — bir alan taşıyordu, bir yöntem barındırıyordu, o kadar.
Bu kursta kap konu oluyor. Ve sınıf konu olur olmaz kaynakta yeni bir şey belirir: bir ad iki tip taşımaya başlar. Bir değişken bildirildiğinde derleyicinin gördüğü tip yazılır; o değişkene atanan nesnenin gerçek sınıfı bambaşka bir şey olabilir. Bu kursun her dersi tek bir soruyu sorar: bu kararı hangisi verdi? İlk ders soruyu nesnenin kurulduğu ana sorar, çünkü kurulum sırasında iki tip bir süre boyunca birbirini tutmaz.
Bir Ad, İki Tip
Bir bildirimde iki ayrı bilgi vardır. Ust d = new Alt(); satırında Ust bildirilen
tiptir (declared type): derleyicinin adı çözerken baktığı tip budur, ve hangi üyenin
yazılabileceğini o belirler. Alt ise çalışma zamanı tipidir (runtime type): nesnenin
gerçek sınıfı, çöp toplanana kadar değişmez. Çoğu programda ikisi aynı yazılır ve fark
görünmez.
Kurs bu ikisini bilerek ayırır. Çekirdek bir üst sınıf (Ust) ile onu genişleten bir alt
sınıftan (Alt) oluşur. Alt, Ust’un bir yöntemini yeniden tanımlar (overriding) —
yani aynı imzayla kendi sürümünü koyar — ve aynı adlı bir alan da bildirir. Tek bir referans
kurulur: bildirilen tipi Ust, çalışma zamanı tipi Alt. Ölçümün kâhini kurgunun
kendisidir; iki tipi biz ayırdığımız için gelen yanıtın hangisinden geldiği baştan bellidir.
Bu dersin sorusu şu: nesne henüz kuruluyorken iki tip ne durumda? Yapıcı bir yöntem değildir, ama gövdesinde yöntem çağrısı bulunabilir. O çağrı çalıştığında ortada hangi nesne vardır?
Nesneye dayalı programlamanın kavramları — nesne, sınıf, kalıtım, arayüz — Programlama Temelleri kursunda kuruldu ve burada tekrarlanmaz. Ölçülen şey kavram değil, kararın tarafıdır.
Ölçüm Çekirdeği
Ölçüm bir kurulum izidir: her başlatma adımı çalıştığında adını ve o anda gördüğü sonucu ortak bir listeye yazar, liste sonda tek seferde basılır.
- SN1 — Bildirilen tip
Ust, çalışma zamanı tipiAltolarak kurgu tarafından ayrılmıştır; her satırın hangi tipten geldiği kurgudan okunur, çıkarımdan değil. - SN2 — İz ortak bir listeye yazılır ve program sonunda basılır; basma işi ölçülen sıraya karışmaz.
- SN3 —
Altsınıfınınadetalanının başlangıç değeri yoktur ve yalnız yapıcı gövdesinde atanır; atanmamış hâli 0 göründüğü için alan başlatma ile yapıcı gövdesi ayırt edilebilir. - SN4 — Ölçüm ortama bağlı hiçbir veri okumaz: süre, bellek adresi, kimlik karması ve yol yazılmaz. Basılan tek şey sıra ve değerdir.
// Ust.java — kursun çekirdek ikilisi ve kurulum izi
import java.util.*;
class Ust {
static final List<String> IZ = new ArrayList<>();
static void iz(String s) { IZ.add(s); }
static void bas() { IZ.forEach(System.out::println); IZ.clear(); }
String etiket = "ust alan";
Ust() {
iz("2. Ust yapicisi | ad() -> " + ad());
iz(" | etiketle() -> " + etiketle());
}
String ad() { return "ust yontem"; }
String etiketle() { return "ust: " + etiket; }
}
class Alt extends Ust {
String etiket = "alt alan";
int adet;
{ iz("3. Alt alan baslatici | etiketle() -> " + etiketle()); }
Alt(int adet) {
iz("4. Alt yapici govdesi | etiketle() -> " + etiketle());
this.adet = adet;
iz("5. atamadan sonra | etiketle() -> " + etiketle());
}
@Override String ad() { return "alt yontem"; }
@Override String etiketle() { return "alt: " + etiket + " x" + adet; }
}
class Kirilgan extends Ust {
String etiket = "kirilgan alan";
@Override String etiketle() { return "kirilgan: " + etiket.length(); }
}
Alt sınıfının yapıcısında super() çağrısı yazılı değildir. Yazılmadığında derleyici
onu zincirin başına kendisi koyar; bir yapıcının ilk işi her zaman üst sınıfın yapıcısını
çalıştırmaktır. Zincir Ust’ta da bitmez, taban sınıfa kadar sürer.
Yapıcı Zinciri Hangi Sırayla Çalışıyor
// Zincir.java — nesne kurulurken kararı hangi tip veriyor
public class Zincir {
public static void main(String[] args) {
Ust.iz("1. new Alt(3) cagrildi");
Ust d = new Alt(3);
Ust.iz("6. kurulum bitti | etiketle() -> " + d.etiketle());
System.out.println("bildirilen tip Ust, calisma zamani tipi "
+ d.getClass().getSimpleName());
System.out.println();
Ust.bas();
System.out.println();
Kirilgan k = null;
try {
k = new Kirilgan();
} catch (NullPointerException e) {
System.out.println("kirilgan kurulum : " + e.getClass().getSimpleName());
}
System.out.println("teslim edilen nesne: " + k);
}
}
bildirilen tip Ust, calisma zamani tipi Alt
1. new Alt(3) cagrildi
2. Ust yapicisi | ad() -> alt yontem
| etiketle() -> alt: null x0
3. Alt alan baslatici | etiketle() -> alt: alt alan x0
4. Alt yapici govdesi | etiketle() -> alt: alt alan x0
5. atamadan sonra | etiketle() -> alt: alt alan x3
6. kurulum bitti | etiketle() -> alt: alt alan x3
kirilgan kurulum : NullPointerException
teslim edilen nesne: null
Sıra dört adımdır ve yönü tektir: üst yapıcı, alt alan başlatıcıları, alt yapıcı gövdesi,
teslim. new Alt(3) yazılmış olsa da çalışan ilk gövde Ust’un yapıcısıdır. Bu, kalıtımın
yapıcı tarafındaki karşılığıdır — bir nesne, kendi sınıfının katmanından önce üstündeki
katmanların hepsi kurulmuş olarak var olur.
İkinci satır bu dersin ölçümüdür. Ust’un yapıcısı ad() yazıyor ve karşılığında alt yontem alıyor. Çağrıyı yazan Ust’tur, çalışan Alt’ın sürümüdür: yeniden tanımlanmış
bir yöntemin kararını çalışma zamanı tipi verir, ve bu kural yapıcı zincirinin ortasında
da askıya alınmaz. Nesne henüz yarı kurulmuşken bile gerçek sınıfı bellidir.
Üçüncü satır bunun ödettiğidir. Aynı noktada etiketle() çağrısı alt: null x0
veriyor. Alt.etiketle() kendi etiket alanını okuyor, ama o alan henüz atanmamıştır:
alan başlatıcıları üst yapıcı bittikten sonra çalışır. Yani çalışma zamanı tipi
doğru yöntemi seçmiştir, seçilen yöntem ise henüz var olmayan bir duruma bakmaktadır. İki
karar ayrı taraflardan gelir ve arada bir aralık kalır.
Aralığın genişliği dört ile beşinci satır arasında okunuyor. Üçüncü satırda etiket
atanmış, adet hâlâ 0; beşinci satırda ikisi de yerindedir. adet alanı yapıcı
gövdesinde atandığı için (SN3) iki aşama birbirinden ayrılabiliyor: bildirimde verilen
değerler bir yerde, yapıcı gövdesinde verilenler daha sonra oturur. Kaynağa bakan biri
etiket ile adet’i aynı sınıfın iki alanı olarak görür; kurulum sırası onları üç ayrı
ana dağıtır.
Yarım Kurulmuş Nesnenin Bedeli
Çıktının son iki satırı aralığın gerçek maliyetidir. Kirilgan sınıfı Alt ile aynı işi
yapar, tek farkı yeniden tanımladığı yöntemin alanı yalnız okumakla kalmayıp üzerinde bir
çağrı yapmasıdır. Sonuç bir NullPointerException’dır ve önemli olan nerede doğduğudur:
istisna Kirilgan.etiketle() içinde düşer, ama o yöntemi çağıran satır Ust’un yapıcısında
durur. Kusuru yazan sınıf ile kusurun göründüğü sınıf ayrıdır.
Son satır bunun sonucunu ölçüyor: k referansı null. Yapıcı tamamlanmadığı için
new ifadesi hiçbir değer üretmemiştir. Yarı kurulmuş nesne teslim edilmez, ama teslim
edilmeden önce onun üzerinde bir yöntem çalışmıştır — hem de alt sınıfın yöntemi.
Buradan çıkan kural, bu kursun ilk somut sözleşmesidir: bir yapıcı, yeniden tanımlanabilecek bir yöntemi çağırmamalıdır. Çağırdığında yazdığı kod alt sınıfın sürümünü çalıştırır ve alt sınıfın yazarı, alanlarının atanmış olduğunu varsayarak yazmıştır. İki taraf da kendi içinde tutarlıdır; kusur ikisinin birleştiği yerde doğar ve ne üst sınıfın ne de alt sınıfın kaynağına bakarak görünür.
Sınırlayıcı Ölçüm: Özel ve Kapalı Yöntemde Tuzak Yok
Kural her yeriyle geçerli olsaydı bir şey açıklamazdı. Tuzağın doğmadığı iki durum vardır ve ikisi de aynı nedenden gelir: o yöntemler için ortada bir yeniden tanımlama yoktur.
- SN5 — Sınır ölçümündeki üç yöntem aynı gövde biçimini taşır ve yalnız
belirteçleriyle ayrılır: biri özel (
private), birifinal, biri belirteçsiz. Alt sınıfta üçünün de aynı adla bir karşılığı yazılmıştır, yalnızfinalolan hariç — o yazılamaz.
// Sinir.java — özel ve final yöntemde tuzak doğmuyor
public class Sinir {
public static void main(String[] args) {
SinirUst s = new SinirAlt();
System.out.println("kurulan nesnenin sinifi : " + s.getClass().getSimpleName());
}
}
class SinirUst {
SinirUst() {
System.out.println("yapicidan ozel() : " + ozel());
System.out.println("yapicidan sabit() : " + sabit());
System.out.println("yapicidan acik() : " + acik());
}
private String ozel() { return "ust ozel"; }
final String sabit() { return "ust final"; }
String acik() { return "ust acik"; }
}
class SinirAlt extends SinirUst {
String etiket = "alt alan";
private String ozel() { return "alt ozel: " + etiket; }
@Override String acik() { return "alt acik: " + etiket; }
}
yapicidan ozel() : ust ozel yapicidan sabit() : ust final yapicidan acik() : alt acik: null kurulan nesnenin sinifi : SinirAlt
Üç satır, üç ayrı taraf. Kurulan nesnenin sınıfı üçünde de aynıdır — dördüncü satır bunu söylüyor — ama yanıtı yalnız sonuncusu çalışma zamanı tipinden alıyor.
ozel() çağrısı ust ozel veriyor. Özel bir yöntem alt sınıfa hiç miras kalmaz; alt
sınıftaki aynı adlı yöntem onun yeniden tanımlaması değil, ayrı bir yöntemdir. Ortada
yeniden tanımlanmış bir sürüm olmadığı için çağrının gidebileceği tek hedef SinirUst’un
kendi yöntemidir; çalışma zamanı tipinin katacağı bir şey yoktur.
sabit() çağrısı ust final veriyor. final bir yöntem yeniden tanımlanamaz;
SinirAlt içine aynı imzayla bir sürüm yazılsaydı derleyici kaynağı reddederdi. Alt sınıfta
başka bir sürüm bulunamayacağına göre çağrının gidebileceği tek yer üst sınıftır. Kısıtın
kendisi bu kursun ileriki bir dersinde ayrıca ölçülür; burada görünen, kısıtın kurulum
sırasındaki sonucudur.
acik() çağrısı ise alt acik: null veriyor. Belirteçsiz yöntem yeniden
tanımlanabilir, tanımlanmıştır ve kararı çalışma zamanı tipi verir — arkasından da tanıdık
null gelir.
Sınırın anlamı şudur: yapıcıdan yapılan bir çağrının tehlikeli olup olmadığı çağrının
yazımından okunmaz. Üç satır da aynı biçimde yazılmıştır. Ayrımı yapan şey yöntemin
belirtecidir, yani üst sınıfın alt sınıfa ne kadar açık bıraktığıdır. Yapıcıdan çağrılacak
bir yardımcı yöntemi özel ya da final bildirmek, tuzağı bir uyarıya değil, dilin
kuralına dönüştürür.
Çağrı Sınıf Dosyasında Nereye Bağlanıyor
Üç çağrının kaynakta ayırt edilemediği görüldü. Sınıf dosyasında ayırt edilebiliyor mu?
Ölçüm için SinirUst derlenir ve yapıcısının gövdesindeki çağrı komutları ile o çağrıların
hedeflerinin belirteçleri okunur.
- SN6 — Ölçüm yalnız
SinirUstsınıfını derler; alt sınıf ortada yoktur. Okunan şey bir koşum değil, derleyicinin tek bir sınıf için ürettiği dosyadır.
// Olcek.java — sınıf dosyasını okur: yapıcıdaki çağrılar ve hedeflerinin belirteçleri
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.lang.reflect.AccessFlag;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Olcek {
record Cagri(String ad, String komut, String hedef, String belirtec, boolean acik) {}
static List<Cagri> yapiciCagrilari(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"));
Map<String, String> belirtec = new HashMap<>();
for (MethodModel m : cm.methods()) {
var f = m.flags();
belirtec.put(m.methodName().stringValue(),
f.has(AccessFlag.PRIVATE) ? "private"
: f.has(AccessFlag.FINAL) ? "final" : "-");
}
List<Cagri> cagri = new ArrayList<>();
for (MethodModel m : cm.methods()) {
if (!m.methodName().stringValue().equals("<init>") || m.code().isEmpty()) continue;
for (CodeElement e : m.code().get())
if (e instanceof InvokeInstruction iv) {
String ad = iv.name().stringValue();
String hedef = kisa(iv.owner().asInternalName());
String b = hedef.equals(sinif) ? belirtec.getOrDefault(ad, "-") : "-";
cagri.add(new Cagri(ad, iv.opcode().name().toLowerCase(Locale.ROOT),
hedef, b, hedef.equals(sinif) && b.equals("-")));
}
}
return cagri;
}
static String kisa(String ic) { return ic.substring(ic.lastIndexOf('/') + 1); }
}
// Komut.java — yapıcıdaki çağrılar sınıf dosyasında hangi komuta bağlanıyor
public class Komut {
static final String KAYNAK = """
class SinirUst {
SinirUst() { ozel(); sabit(); acik(); }
private String ozel() { return "ust ozel"; }
final String sabit() { return "ust final"; }
String acik() { return "ust acik"; }
}
""";
public static void main(String[] args) throws Exception {
System.out.printf("%-8s %-14s %-9s %-10s %s%n",
"cagri", "komut", "hedef", "belirtec", "yeniden tanimlanabilir");
for (Olcek.Cagri c : Olcek.yapiciCagrilari(KAYNAK, "SinirUst"))
System.out.printf("%-8s %-14s %-9s %-10s %s%n",
c.ad(), c.komut(), c.hedef(), c.belirtec(), c.acik() ? "evet" : "hayir");
}
}
cagri komut hedef belirtec yeniden tanimlanabilir <init> invokespecial Object - hayir ozel invokevirtual SinirUst private hayir sabit invokevirtual SinirUst final hayir acik invokevirtual SinirUst - evet
İlk satır kaynakta yazılmayan bir çağrıdır. SinirUst’un yapıcısında super() yazılmamıştır,
buna karşın sınıf dosyasında bir Object yapıcısı çağrısı durmaktadır ve gövdenin ilk
komutudur. Yapıcı zincirinin üstten alta çalıştığı iddiası burada okunuyor: zincirin halkası
derleyici tarafından takılmıştır.
Asıl gözlem kalan üç satırdadır. Üç çağrı da aynı komuta derlenmiştir. Kaynakta ayırt
edilemeyen fark, sınıf dosyasının çağrı yerinde de ayırt edilemiyor: ozel, sabit ve
acik için üretilen komut birebir aynıdır ve üçünün de hedefi SinirUst olarak yazılıdır.
Çağrı yerine bakan biri hangisinin alt sınıfa gidebileceğini söyleyemez.
Fark hedefin belirtecindedir. Özel bir yöntem miras kalmaz, final bir yöntem yeniden
tanımlanamaz; ikisinde de sanal makinenin arayabileceği ikinci bir gerçekleştirim yoktur, bu
yüzden dinamik seçim çalışsa bile tek bir aday bulur. Yalnız belirteçsiz yöntemde ikinci bir
aday olabilir, ve Sinir koşumunda olan da budur.
Bu, kursun ikinci iddiasının ilk örneğidir: fark sözdiziminde görünmez. Şimdi bir adım daha ileri gidildi — fark çağrının yazıldığı yerde de görünmüyor. Kararın hangi taraftan geleceği çağrıda değil, çağrılanın bildiriminde yazılıdır.
Özet
- Bir ad iki tip taşır: derleyicinin gördüğü bildirilen tip ve nesnenin gerçek sınıfı olan çalışma zamanı tipi. Kursun her ölçümü kararın hangisinden geldiğini sorar.
- Yapıcı zinciri üstten alta çalışır: üst yapıcı, alt sınıfın alan başlatıcıları, alt
yapıcının gövdesi, sonra teslim.
super()yazılmasa da sınıf dosyasında durur ve yapıcının ilk komutudur. - Yapıcıdan çağrılan yeniden tanımlı bir yöntemin kararını çalışma zamanı tipi verir:
Ust’un yapıcısıad()yazar,alt yontemalır. - Seçilen yöntem henüz atanmamış alanlara bakar: aynı noktada
etiketle()alt: null x0döner, çünkü alan başlatıcıları üst yapıcı bittikten sonra çalışır. - Alan yalnız okunmayıp üzerinde çağrı yapıldığında kurulum bir
NullPointerExceptionile düşer venewhiçbir değer üretmez: referansnullkalır, yarı kurulmuş nesne teslim edilmez. - Tuzak özel ve
finalyöntemde doğmaz: üç çağrı sınıf dosyasında aynı komuta derlenir ve aynı hedefi yazar; farkı çağrı yeri değil, çağrılanın belirteci taşır.
Sonraki Adım
Bu derste Ust ile Alt arasındaki bütün üyeler birbirine açıktı; hangi yöntemin
görülebildiği hiç sorulmadı. Oysa sınır ölçümünde ilk kez bir belirteç işe karıştı: private
yazılan yöntem alt sınıfa hiç miras kalmadı ve kararı bildirilen tipe bıraktı. O tek sözcüğün
neyi kapattığı ölçülmedi. Sıradaki ders adın kimden görüldüğünü sorar: dört görünürlük
düzeyi ile dört ayrı çağrı konumu çaprazlanır ve on altı çiftin kaçının derlendiği sayılır.
Ölçümün ikinci yarısı görünürlüğün nerede yaşadığını gösterir — sınıf dosyasında bir bayrak
olarak durur, çalışma zamanında ise üye yerinde durmaya devam eder.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.