İçeriğe geç
academia.sh

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ı tipi Alt olarak 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.
  • SN3Alt sınıfının adet alanı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), biri final, biri belirteçsiz. Alt sınıfta üçünün de aynı adla bir karşılığı yazılmıştır, yalnız final olan 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 SinirUst sı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 yontem alır.
  • Seçilen yöntem henüz atanmamış alanlara bakar: aynı noktada etiketle() alt: null x0 dö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 NullPointerException ile düşer ve new hiçbir değer üretmez: referans null kalır, yarı kurulmuş nesne teslim edilmez.
  • Tuzak özel ve final yö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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat