İçeriğe geç
academia.sh

Ders 08 / 13

Kayıtlar ve Numaralandırmalar

Tek satırlık bir kayıt bildirimi sınıf dosyasında on üye üretir ve üretilen sınıf genişletilemez, yani bildirilen tiple çalışma zamanı tipi hiçbir zaman ayrılamaz. Numaralandırmada ise gövde yazılan bir sabit kendi sınıfını alır; kaydın ürettiği eşitlik ise yalnız sığ korur.

İçindekiler

Önceki derste derleyici kaynakta hiç yazılmamış sınıflar üretti ve onlara gizli alanlarla gizli yapıcı parametreleri koydu. Üretilen her şeyin ortak yanı, bir programcının elle yazacağı bir bağlama işini üstlenmesiydi — ama üretilenler görünmezdi: hiçbiri kaynaktan çağrılabilecek bir yüzey değildi.

Bu derste aynı üretim yeteneği görünür bir işe koşulur. Java’da bir veri taşıyıcının bütün yüzeyi — alan okuyucuları, eşitlik, karma değeri ve metne çevirme — tek bir satırdan üretilebilir; bu satırın adı kayıt (record) bildirimidir. Sabit bir kümeyi tanımlayan numaralandırma da benzer bir üretim yapar. Sorular üç tanedir: bir satır kaç üye üretiyor, üretilen üyeler kararı hangi tarafa bırakıyor, ve üretilen eşitlik nereye kadar koruyor?

Ölçüm Çekirdeği

Ölçüm bir kaynağı derler, ürettiği sınıf dosyasındaki alan ve yöntem adlarını okur, sınıfın genişletilebilir olup olmadığını bildirir ve ayrıca bir kaynağın derlenip derlenmediğini döndürür.

  • SN36 — Kâhin kaynağın kendisidir: kayıt bildiriminin gövdesi boştur, dolayısıyla sınıf dosyasında görünen her üye derleyicinin ürettiğidir.
  • SN37 — Üye listesi sınıf dosyasından okunur; sayım kaynaktaki yazımı değil, üretilen dosyayı temel alır.
  • SN38 — Derleme denemeleri kendi geçici dizinlerinde yapılır ve sonuç tek bir çıkış koduyla okunur.
  • SN39 — Eşitlik ölçümünde iki nesne ayrı ayrı kurulur; kimlik yalnız == ile karşılaştırılır ve hiçbir kimlik sayısı basılmaz.
  • SN40 — Karma değeri yalnız iki nesnenin karması birbirine eşit mi sorusuyla anılır ve bu soru yalnız değer tabanlı karma üreten bileşenler için sorulur.
// Olcek.java — sınıf dosyasındaki üyeler ve genişletme denemesi
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.reflect.AccessFlag;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Olcek {
    record Sinif(boolean sonlanmis, List<String> alan, List<String> yontem) {}

    static Path yeniDizin() throws Exception { return Files.createTempDirectory("olcek"); }

    static boolean derle(Path d, String ad, String kaynak) throws Exception {
        Path k = d.resolve(ad + ".java");
        Files.writeString(k, kaynak);
        PrintWriter bos = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(bos, bos, "-d", d.toString(), "-cp", d.toString(), k.toString()) == 0;
    }

    static Sinif oku(String kaynak, String ad) throws Exception {
        Path d = yeniDizin();
        if (!derle(d, ad, kaynak)) throw new IllegalStateException("derlenmedi: " + ad);
        ClassModel cm = ClassFile.of().parse(d.resolve(ad + ".class"));
        List<String> alan = new ArrayList<>();
        for (FieldModel f : cm.fields())
            alan.add(f.fieldName().stringValue()
                    + (f.flags().has(AccessFlag.FINAL) ? " [final]" : ""));
        List<String> yontem = new ArrayList<>();
        for (MethodModel m : cm.methods()) yontem.add(m.methodName().stringValue());
        return new Sinif(cm.flags().has(AccessFlag.FINAL), alan, yontem);
    }
}

Tek Satır, On Üye

Ölçülen kaynak tek bir satırdır ve gövdesi boştur. Kursun somut ekseni burada da sürer: üç alanlı bir depo kaydı — ad, adet ve gram cinsinden ağırlık.

// Uretilen.java — tek satırlık bir kayıt bildiriminden kaç üye çıkıyor
import java.util.TreeSet;

public class Uretilen {
    static final String ELLE = """
        class Elle {
            private final String ad; private final int adet; private final long agirlik;
            Elle(String ad, int adet, long agirlik) {
                this.ad = ad; this.adet = adet; this.agirlik = agirlik;
            }
            public String ad() { return ad; }
            public int adet() { return adet; }
            public long agirlik() { return agirlik; }
            @Override public boolean equals(Object o) {
                return o instanceof Elle e
                        && ad.equals(e.ad) && adet == e.adet && agirlik == e.agirlik;
            }
            @Override public int hashCode() {
                return java.util.Objects.hash(ad, adet, agirlik);
            }
            @Override public String toString() {
                return "Elle[ad=" + ad + ", adet=" + adet + ", agirlik=" + agirlik + "]";
            }
        }
        """;

    public static void main(String[] args) throws Exception {
        String kaynak = "record Kayit(String ad, int adet, long agirlik) { }";
        Olcek.Sinif k = Olcek.oku(kaynak, "Kayit");
        System.out.println("kaynak: " + kaynak);
        System.out.println("alanlar (" + k.alan().size() + "): " + k.alan());
        System.out.println("yontemler (" + k.yontem().size() + "): " + k.yontem());
        System.out.println("sinif final mi: " + k.sonlanmis());
        System.out.println();
        Olcek.Sinif e = Olcek.oku(ELLE, "Elle");
        System.out.println("kaynak satiri: kayit 1, elle yazilan " + ELLE.strip().lines().count());
        System.out.println("uye sayisi   : kayit " + (k.alan().size() + k.yontem().size())
                + ", elle yazilan " + (e.alan().size() + e.yontem().size()));
        System.out.println("yontem adlari ayni mi: "
                + new TreeSet<>(k.yontem()).equals(new TreeSet<>(e.yontem())));
        System.out.println();
        System.out.printf("%-34s -> derleniyor mu: %s%n", "kaydi genisletme",
                Olcek.derle(Olcek.yeniDizin(), "T", kaynak + " class T extends Kayit { }"));
        System.out.printf("%-34s -> derleniyor mu: %s%n", "kaydin arayuz gerceklestirmesi",
                Olcek.derle(Olcek.yeniDizin(), "T", "interface T { }"
                        + " record Kayit(String ad) implements T { }"));
        System.out.printf("%-34s -> derleniyor mu: %s%n", "alanina yeniden atama",
                Olcek.derle(Olcek.yeniDizin(), "T",
                        "record Kayit(String ad) { } class T { void f(Kayit k) { k.ad = \"x\"; } }"));
    }
}
kaynak: record Kayit(String ad, int adet, long agirlik) { }
alanlar (3): [ad [final], adet [final], agirlik [final]]
yontemler (7): [<init>, toString, hashCode, equals, ad, adet, agirlik]
sinif final mi: true

kaynak satiri: kayit 1, elle yazilan 19
uye sayisi   : kayit 10, elle yazilan 10
yontem adlari ayni mi: true

kaydi genisletme                   -> derleniyor mu: false
kaydin arayuz gerceklestirmesi     -> derleniyor mu: true
alanina yeniden atama              -> derleniyor mu: false

Kaynakta bir bildirim satırı yazıldı; sınıf dosyasında üç alan ve yedi yöntem, toplam on üye var. Üç alanın üçü de final bayrağını taşıyor. Yedi yöntemin üçü bileşen okuyucusudur (ad, adet, agirlik), üçü Java’nın her nesnesinden beklediği yüzeydir (toString, hashCode, equals) ve biri bileşenleri alan asıl yapıcıdır. Bir önceki dersin gizli alanlarından farkı buradadır: bunlar gizli değildir, çağrılabilir bir yüzeydir.

Ortadaki üç satır bu yüzeyin elle yazıldığında neye mal olduğunu ölçüyor. Aynı on üyeyi elle kuran sınıf 19 satırdır ve sınıf dosyasında yine 10 üye bırakır; iki sınıfın yöntem adları birebir aynıdır. Kayıt yeni bir düzenek getirmez — sınıf dosyası düzeyinde ortaya çıkan şey sıradan bir sınıftır. Getirdiği şey, bu on üyenin elle yazılmasında yapılabilecek hataların ortadan kalkmasıdır: bir bileşen eklendiğinde elle yazılan eşitlikte onu unutmak mümkündür, üretilen eşitlikte değildir.

Java Temelleri kursunda numaralandırma için de aynı ölçüm yapılmış, kaynakta yalnız iki ad yazılan bir bildirimin sınıf dosyasında kaynakta bulunmayan alanlar ve yöntemler taşıdığı gösterilmişti. İki biçim aynı düzeneği kullanır; farkları ne ürettikleridir, üretip üretmedikleri değil.

Alt bölümdeki üç deneme üretilenin bir yanını daha gösteriyor. Kayıt genişletilemez: sınıf final bayrağını taşır ve onu genişleten bir sınıf derlenmez. Bir arayüz gerçekleştirmesi ise serbesttir. Alanına yeniden atama da derlenmez, çünkü alanların hepsi finaldır — önceki dersin ölçtüğü kısıt burada kaynakta hiç yazılmadan uygulanır.

Üretilen Üyeler Kararı Kime Bırakıyor

Kaydın genişletilememesi kursun sorusunu tuhaf bir yere getirir. Bir kayıt tipinin alt tipi olamayacağına göre, o tiple bildirilmiş bir adın çalışma zamanı tipi her zaman aynı olacaktır. Öyleyse üretilen üyeler için soru anlamını mı yitirir?

// Taraf.java — üretilen üyeler kararı hangi tarafa bırakıyor
import java.util.*;

public class Taraf {
    interface Tasiyici { }
    record Kayit(String ad, int adet) implements Tasiyici { }

    enum Islem {
        EKLE { @Override int uygula(int a) { return a + 1; } },
        TUT;
        int uygula(int a) { return a; }
    }

    public static void main(String[] args) {
        Tasiyici t = new Kayit("civata", 12);
        Object n = t;
        System.out.println("bildirilen tip Object, metne cevirme : " + n);
        Kayit a = new Kayit("civata", 12), b = new Kayit("civata", 12);
        System.out.println("a == b                               : " + (a == b));
        System.out.println("a.equals(b)                          : " + a.equals(b));
        System.out.println("karma degerleri esit mi              : " + (a.hashCode() == b.hashCode()));
        Map<Kayit, String> m = new HashMap<>();
        m.put(a, "birinci");
        System.out.println("b anahtariyla okunan                 : " + m.get(b));
        System.out.println();
        for (Islem i : Islem.values())
            System.out.printf("%-5s sinifi Islem'in kendisi mi: %-5s | bildiren tip: %s | uygula(1)=%d%n",
                    i, i.getClass() == Islem.class, i.getDeclaringClass().getSimpleName(), i.uygula(1));
        System.out.println("Islem.EKLE ile valueOf(\"EKLE\") ayni nesne mi: "
                + (Islem.EKLE == Islem.valueOf("EKLE")));
    }
}
bildirilen tip Object, metne cevirme : Kayit[ad=civata, adet=12]
a == b                               : false
a.equals(b)                          : true
karma degerleri esit mi              : true
b anahtariyla okunan                 : birinci

EKLE  sinifi Islem'in kendisi mi: false | bildiren tip: Islem | uygula(1)=2
TUT   sinifi Islem'in kendisi mi: true  | bildiren tip: Islem | uygula(1)=1
Islem.EKLE ile valueOf("EKLE") ayni nesne mi: true

İlk satır soruyu yanıtlıyor. Bildirilen tip Objecttir ve Object sınıfının metne çevirme yöntemi kaydın verdiğini vermez; buna karşılık basılan metin kaydın ürettiği biçimdir. Üretilen toString bir yeniden tanımlamadır ve her yeniden tanımlama gibi kararı çalışma zamanı tipine bırakır. Kayıt genişletilemez olduğu için o tip bilinen tek tiptir, ama karar yine de o taraftan gelir. Genişletilememe kararın tarafını değiştirmez; yanıtın önceden bilinebilir olmasını sağlar.

Sonraki dört satır üretilen eşitliğin ne olduğunu gösteriyor. a ile b ayrı ayrı kurulmuş iki nesnedir ve == yanlış verir: kimlik ayrıdır. equals ise doğru verir, çünkü üretilen gerçekleştirim bileşenleri karşılaştırır. Karma değerleri de eşittir ve bunun gözlenebilir sonucu son satırdadır: a ile eklenen bir eşleme girişi b ile okunabiliyor. Kimlik ile eşitliğin ayrıldığı bu üç satır, bir veri taşıyıcının nasıl davranması gerektiğinin tanımıdır ve kaynakta tek bir karakteri yazılmamıştır.

Numaralandırmada aynı soru ters uçtan sorulur. EKLE ile TUT aynı bildirilen tiptedir, ama EKLEnin çalışma zamanı sınıfı numaralandırmanın kendisi değildir: gövde yazılan bir sabit kendi sınıfını alır. TUT içinse ikisi aynıdır. İki sabit aynı yöntem çağrısına ayrı yanıtlar veriyor — 2 ve 1 — ve yanıtı veren şey yine çalışma zamanı tipidir. Bildiren tip her ikisinde de Islem olarak kalır; bu, bildirilen tiple çalışma zamanı tipinin ayrıldığı yerde sabitin hangi kümeye ait olduğunun kaybolmadığını gösterir. Son satır ise kümenin sabit oluşunu ölçer: adla bulunan sabit == ile aynı nesnedir, çünkü her sabitten bir tane vardır.

İki biçimin bu derste yan yana durmasının nedeni buradan okunur. Kayıt kaç tane olduğu bilinmeyen taşıyıcılar için yazılır ve her birinin kimliğini değerine bırakır; numaralandırma kaç tane olduğu bilinen bir küme tanımlar ve her birinin kimliğini kendinde tutar. Birinde equals doğruyken == yanlış olabilir, ötekinde ikisi hep birlikte yanıt verir. Ortak yanları üretilen üyelerin kaynağıdır: iki bildirim de derleyiciye bir yüzey ürettirir ve üretilen yüzeyin kararı çalışma zamanı tipinden gelir.

Sınırlayıcı Ölçüm: Kayıt Sığ Değiştirilemezdir

Üretilen alanların hepsi finaldı ve üretilen eşitlik bileşenleri karşılaştırıyordu. Bu iki gerçek birlikte okunduğunda bir kayıt “değiştirilemez” görünür. Sınır tam buradadır.

// Sig.java — kayıt sığ değiştirilemezdir
import java.util.Arrays;

public class Sig {
    record Parti(String ad, int[] adetler) { }

    record Guvenli(String ad, int[] adetler) {
        Guvenli { adetler = adetler.clone(); }
    }

    public static void main(String[] args) {
        int[] disari = {1, 2, 3};
        Parti p = new Parti("civata", disari);
        Guvenli g = new Guvenli("civata", disari);
        System.out.println("kurulustan sonra    : " + Arrays.toString(p.adetler())
                + " | kopyalayan: " + Arrays.toString(g.adetler()));
        disari[0] = 99;
        System.out.println("disaridan yazildi   : " + Arrays.toString(p.adetler())
                + " | kopyalayan: " + Arrays.toString(g.adetler()));
        System.out.println();
        Parti q = new Parti("civata", new int[] {99, 2, 3});
        System.out.println("ad alanlari esit mi : " + p.ad().equals(q.ad()));
        System.out.println("dizi icerigi esit mi: " + Arrays.equals(p.adetler(), q.adetler()));
        System.out.println("p.equals(q)         : " + p.equals(q));
    }
}
kurulustan sonra    : [1, 2, 3] | kopyalayan: [1, 2, 3]
disaridan yazildi   : [99, 2, 3] | kopyalayan: [1, 2, 3]

ad alanlari esit mi : true
dizi icerigi esit mi: true
p.equals(q)         : false

p kurulduktan sonra hiçbir üyesine yazılmadı; yine de adetler bileşeni değişti. Değişikliği yapan taraf kaydı hiç görmedi bile — elindeki dizi üzerine yazdı ve kayıt aynı diziyi tutuyordu. final alan, önceki derste ölçüldüğü gibi yalnız bağlamayı korur; bağın ucundaki nesne bu kısıtın dışındadır. Kayıt bu kısıtı bileşen başına uygular, dolayısıyla koruması bileşenlerin tipi kadardır — sığdır.

Alt üç satır aynı sınırı eşitlik tarafında gösteriyor. p ile q artık aynı adı ve aynı içerikte bir diziyi taşıyor; iki karşılaştırma da doğru veriyor. Buna karşılık p.equals(q) yanlıştır. Üretilen eşitlik her bileşeni o bileşenin kendi eşitliğiyle karşılaştırır; bir dizinin kendi eşitliği ise kimliktir. Aynı kayıt tipi içinde bir bileşen değere, öteki kimliğe bakıyor ve bu ayrım kaynakta hiçbir yerde yazılı değil.

İki kusurun da çaresi aynı yerdedir. Guvenli kaydı asıl yapıcısında diziyi kopyalar ve dışarıdaki değişiklikten etkilenmez; çıktının ikinci satırı bunu gösteriyor. Derleyicinin ürettiği şey bir başlangıç noktasıdır, bir güvence değil: üretilen üyeler bileşenlerin davranışını devralır, düzeltmek yazan tarafa kalır.

Özet

  • Gövdesi boş, tek satırlık bir kayıt bildirimi sınıf dosyasında üç alan ve yedi yöntem üretir; üç alanın üçü de final bayrağını taşır.
  • Üretilen yedi yöntem üç bileşen okuyucusu, üç ortak yüzey yöntemi (toString, hashCode, equals) ve bileşenleri alan asıl yapıcıdır.
  • Kayıt genişletilemez, arayüz gerçekleştirebilir ve alanına yeniden atama derlemeyi durdurur.
  • Üretilen üyeler kararı çalışma zamanı tipine bırakır: bildirilen tip Object olduğunda bile basılan metin kaydın ürettiği biçimdir. Genişletilememe kararın tarafını değiştirmez, yanıtı önceden bilinebilir kılar.
  • Ayrı ayrı kurulan iki kayıt için == yanlış, equals doğrudur ve karma değerleri eşittir; numaralandırmada ise gövde yazılan sabit kendi sınıfını alır ve aynı çağrıya ayrı yanıt verir.
  • Kaydın koruması sığdır: dizi bileşeni dışarıdan değişir ve üretilen eşitlik o bileşende değere değil kimliğe bakar; asıl yapıcıda kopyalamak ikisini de düzeltir.

Sonraki Adım

Bu konu boyunca ölçülen şey hep aynı soruydu: kararı bildirilen tip mi verdi, çalışma zamanı tipi mi. Son iki derste soruya bir taraf daha katıldı — derleyicinin kendisi. Gizli alanları, yapıcı parametrelerini ve bir veri taşıyıcının bütün yüzeyini o üretti; ürettiklerinin hepsi sınıf dosyasında durdu ve çalışma zamanında okunabildi. Öyleyse derleyicinin elindeki bir başka bilgiye, tip bilgisinin kendisine ne oluyor? Bir kabın hangi tipte öğe tuttuğu kaynakta yazılıdır ve derleyici o yazıya bakarak denetim yapar. Sıradaki konu bu bilginin çalışma zamanında ne kadarının kaldığını ölçer: iki ayrı tip parametresiyle yazılmış iki kap çalışma zamanında ayırt edilebiliyor mu, ve denetimi yapan taraf ortadan kalktığında yanlış yazmanın bedelini kim ödüyor?

İ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