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
finalbayrağı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
Objectolduğ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ış,equalsdoğ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.