Ders 03 / 13
Kalıtım ve Yeniden Tanımlama
Tek bir referans üzerinde sekiz karar okunuyor ve yanıt yarı yarıya bölünüyor: dördünü çalışma zamanı tipi, dördünü bildirilen tip veriyor. Alanların aynı adla yeniden bildirilmesi yeniden tanımlama değil gizlemedir; imza değiştiğinde ise yeniden tanımlama hiç doğmaz ve karar taraf değiştirir.
İçindekiler
İki derstir kararın bir tarafı ayrı ayrı ölçüldü. Yapıcıdan çağrılan yeniden tanımlı yöntemi çalışma zamanı tipi seçti; bir üyenin görülüp görülmediğine bildirilen tip karar verdi. İkisi hiç yan yana konmadı, dolayısıyla aradaki kuralın ne olduğu da sorulmadı.
Bu ders kursun çekirdek ölçümüdür. Tek bir referans kurulur — bildirilen tipi Ust, çalışma
zamanı tipi Alt — ve o referans üzerinden sekiz ayrı dilsel karar okunur. Her karar için tek
soru sorulur: sonucu hangi tip verdi? Ölçümün kâhini kurgunun kendisidir; iki tipi bilerek
ayırdığımız için her yanıtın kaynağı baştan bellidir.
Programlama Paradigmaları kursu kalıtımın iki vaadini ve alt tip sözleşmesinin nasıl ihlal edildiğini ölçmüştü; orada sayılan şey sözleşme ihlaliydi. Burada sayılan şey kararın tarafıdır ve ölçüm çalıştırılmış bir koşumdan gelir.
Ölçüm Çekirdeği
Çekirdek üç tipten oluşur: bir arayüz (Tasiyici), bir üst sınıf (Ust) ve onu genişleten
bir alt sınıf (Alt). Alt, Ust’un yöntemini yeniden tanımlar, alanını ise aynı adla
yeniden bildirir — ikisinin ayrı şeyler olduğu bu dersin ikinci ölçümüdür.
- SN11 — Her üye kendi kaynağını metin olarak döndürür (
ust alan,alt yontem); dönen değer hangi bildirimden geldiğini kendisi söyler, çıkarım gerekmez. - SN12 — Sekiz kararın hepsi aynı referans üzerinden okunur; değişen tek şey okunan üyenin cinsidir.
- SN13 — Ölçüm ortama bağlı hiçbir veri okumaz: kimlik karması, süre ve yol yazılmaz;
kimlik gerekirse
==ile karşılaştırılır.
// Ust.java — kursun çekirdeği: bir arayüz, bir üst sınıf, onu genişleten bir alt sınıf
interface Tasiyici {
default String tasi() { return "arayuz varsayilani"; }
}
class Ust implements Tasiyici {
String etiket = "ust alan";
String ad() { return "ust yontem"; }
String karsila(Ust u) { return "Ust.karsila(Ust)"; }
static String durum() { return "ust statik"; }
@Override public String toString() { return "Ust nesnesi"; }
}
class Alt extends Ust {
String etiket = "alt alan";
@Override String ad() { return "alt yontem"; }
String karsila(Alt a) { return "Alt.karsila(Alt)"; }
static String durum() { return "alt statik"; }
@Override public String tasi() { return "alt yeniden tanim"; }
@Override public String toString() { return "Alt nesnesi"; }
}
Derste iki ölçüm koşum değil derleme okur: birinde bir kaynağın kabul edilip edilmediği, ötekinde üretilen sınıf dosyasındaki alanlar sayılır. İkisi de aynı yardımcıdan gelir.
// Derle.java — kaynağı derler; ya derleme sonucunu ya da bir sınıfın alanlarını verir
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Derle {
static Path derle(String kaynak) throws Exception {
Path d = Files.createTempDirectory("kalitim");
Path k = d.resolve("Deneme.java");
Files.writeString(k, kaynak);
PrintWriter bos = new PrintWriter(Writer.nullWriter());
return ToolProvider.findFirst("javac").orElseThrow()
.run(bos, bos, "-d", d.toString(), k.toString()) == 0 ? d : null;
}
static String olur(String kaynak) throws Exception {
return derle(kaynak) == null ? "derlenmedi" : "derlendi";
}
static List<String> alanlar(String kaynak, String sinif) throws Exception {
Path d = derle(kaynak);
List<String> ad = new ArrayList<>();
for (FieldModel f : ClassFile.of().parse(d.resolve(sinif + ".class")).fields())
ad.add(f.fieldName().stringValue());
return ad;
}
}
Sekiz Karar, Tek Referans
// Karar.java — tek referans üzerinde sekiz karar
public class Karar {
static String secim(Ust u) { return "Ust imzasi"; }
static String secim(Alt a) { return "Alt imzasi"; }
static void bildir(String karar, String sonuc, String veren) {
System.out.printf("%-24s %-22s %s%n", karar, sonuc, veren);
}
public static void main(String[] args) {
Ust d = new Alt();
System.out.println("bildirilen tip Ust, calisma zamani tipi "
+ d.getClass().getSimpleName());
System.out.println();
System.out.printf("%-24s %-22s %s%n", "karar", "sonuc", "karari veren");
bildir("yeniden tanimli yontem", d.ad(), "calisma zamani tipi");
bildir("arayuz varsayilani", d.tasi(), "calisma zamani tipi");
bildir("metne cevirme", d.toString(), "calisma zamani tipi");
bildir("alan okumasi", d.etiket, "bildirilen tip");
bildir("asiri yukleme secimi", secim(d), "bildirilen tip");
bildir("statik yontem", Ust.durum(), "bildirilen tip");
bildir("tip sinamasi", String.valueOf(d instanceof Alt), "calisma zamani tipi");
bildir("asagi donusum", ((Alt) d).etiket, "bildirilen tip (donusum sonrasi)");
}
}
bildirilen tip Ust, calisma zamani tipi Alt karar sonuc karari veren yeniden tanimli yontem alt yontem calisma zamani tipi arayuz varsayilani alt yeniden tanim calisma zamani tipi metne cevirme Alt nesnesi calisma zamani tipi alan okumasi ust alan bildirilen tip asiri yukleme secimi Ust imzasi bildirilen tip statik yontem ust statik bildirilen tip tip sinamasi true calisma zamani tipi asagi donusum alt alan bildirilen tip (donusum sonrasi)
Sekiz kararın dördü çalışma zamanı tipinden, dördü bildirilen tipten geliyor. “Java nesneye dayalıdır, çağrı gerçek nesneye gider” cümlesi kararların yalnız yarısını açıklar.
Ayrım rastgele değildir. Üstteki üç satır ile yedinci satır çalışmayı ilgilendirir: hangi gövdenin yürütüleceği, nesnenin gerçekten ne olduğu. Bunlar çalışma zamanı tipine bakar, çünkü yanıt ancak nesne ortadayken bilinebilir. Kalan dördü adı çözme işidir: hangi alan okunacak, hangi imza seçilecek, hangi sınıfın statik yöntemi çağrılacak. Bunlar derleyicinin masasında biter ve derleyicinin elinde yalnız bildirilen tip vardır.
Kural tek cümlede toplanabilir: çağrılan davranış çalışma zamanı tipine, seçilen ad bildirilen tipe bakar. İkinci satır bunun en görünmez örneğidir — arayüzden gelen bir varsayılan yöntem de yeniden tanımlanabilir ve kararı yine çalışma zamanı tipi verir.
Son satır dördüncü bir okuma ekliyor. ((Alt) d).etiket ifadesi alt alan veriyor, oysa
nesne baştan beri aynı nesnedir. Aşağı dönüşüm nesneyi değiştirmez, bildirilen tipi
değiştirir; derleyiciye verilen bir sözdür ve sözün doğruluğu çalışma zamanında sınanır.
Yeniden Tanımlama ile Gizlemenin Ayrımı
Dördüncü satır dersin en sessiz sonucudur. d.ad() alt yontem verirken d.etiket ust
alan veriyor; ikisi de aynı d üzerinden, iki karakterlik bir fark ile yazılıyor. Alanların
aynı adla yeniden bildirilmesi yeniden tanımlama değildir; gizlemedir. Gizlenen alanın ne
olduğunu görmek için tek nesneye iki ayrı referans bağlanır.
// Gizleme.java — gizlenen alan yok olmuyor
public class Gizleme {
public static void main(String[] args) {
Ust u = new Alt();
Alt a = (Alt) u;
System.out.println("iki referans ayni nesne mi : " + (u == a));
System.out.println("Ust referansindan etiket : " + u.etiket);
System.out.println("Alt referansindan etiket : " + a.etiket);
System.out.println("Ust referansindan ad() : " + u.ad());
System.out.println("Alt referansindan ad() : " + a.ad());
a.etiket = "yazildi";
System.out.println("a.etiket yazildiktan sonra u.etiket: " + u.etiket);
}
}
iki referans ayni nesne mi : true Ust referansindan etiket : ust alan Alt referansindan etiket : alt alan Ust referansindan ad() : alt yontem Alt referansindan ad() : alt yontem a.etiket yazildiktan sonra u.etiket: ust alan
İlk satır ölçümün dayanağıdır: ortada tek bir nesne vardır. Buna karşın o tek nesne etiket
sorusuna iki ayrı yanıt veriyor, ad() sorusuna tek yanıt. Yeniden tanımlanan yöntem
üsttekinin yerini alır; yeniden bildirilen alan üsttekinin yerini almaz, üstüne konur.
Son satır bunun bellek karşılığıdır. a.etiket alanına yazıldıktan sonra u.etiket hâlâ
ust alan diyor. Aynı nesnenin içinde iki ayrı yer vardır ve biri yazıldığında öteki
değişmez. Gizlenen alan yok olmamıştır; yalnız bildirilen tipi Alt olan bir referanstan
görünmez olmuştur.
İki yerin gerçekten iki yer olduğu sınıf dosyasından da okunabilir. Aynı ikili derlenir ve her iki sınıfın bildirdiği alanlar listelenir.
// Yer.java — gizlenen alan sınıf dosyasında nerede duruyor
public class Yer {
static final String KAYNAK = """
class Ust { String etiket = "ust alan"; String ad() { return "ust yontem"; } }
class Alt extends Ust { String etiket = "alt alan";
@Override String ad() { return "alt yontem"; } }
""";
public static void main(String[] args) throws Exception {
System.out.println("Ust sinif dosyasindaki alanlar: " + Derle.alanlar(KAYNAK, "Ust"));
System.out.println("Alt sinif dosyasindaki alanlar: " + Derle.alanlar(KAYNAK, "Alt"));
}
}
Ust sinif dosyasindaki alanlar: [etiket] Alt sinif dosyasindaki alanlar: [etiket]
İki ayrı sınıf dosyası, her birinde bir etiket bildirimi. Yeniden tanımlanan ad() yöntemi
için de iki gövde vardır, ama çağrı yalnız birine gider; alanda böyle bir seçim yoktur, ikisi
de okunabilir durumdadır. Kalıtım yöntemde birini ötekinin yerine koyar, alanda ikisini
yan yana bırakır.
Bedeli buradan okunur. Bir alt sınıfa üstündekiyle aynı adı taşıyan bir alan yazan programcı, üstteki alanı değiştirdiğini düşünür. Değiştirmemiştir — iki alan da yaşamaya devam eder ve hangisinin okunacağı, o satırdaki referansın bildirilen tipine bakılarak belirlenir. Kusur yazan yerde görünmez; okuyan yerde, hem de yanlış değeri sessizce döndürerek görünür.
Derleyicinin Yakaladığı ve Kaçırdığı
Yeniden tanımlama bir bildirim değildir; iki imza denk düştüğünde kendiliğinden doğar. Bu
yüzden doğmaması da kendiliğinden olur ve sessizdir. @Override işareti tam olarak bu
sessizliği bozmak içindir: alt sınıfa “bu yöntem gerçekten bir yeniden tanımlama mı” sorusunu
derleyiciye sordurur.
- SN14 — Beş denemede de üst sınıf birebir aynıdır; değişen tek şey alt sınıfın tek satırlık bildirimidir. Ölçülen yalnız derleme sonucudur.
// Isaret.java — @Override ne yakalıyor, imza ne kadar oynayabiliyor
public class Isaret {
static final String TABAN = """
class A { String ad() { return "ust"; } A uret() { return this; }
String karsila(A a) { return "A"; } }
""";
static final String[][] DENEME = {
{ "isaretsiz, ad yanlis yazilmis", "class B extends A { String adi() { return \"alt\"; } }" },
{ "isaretli, ad yanlis yazilmis", "class B extends A { @Override String adi() { return \"alt\"; } }" },
{ "isaretli, donus tipi daraltilmis", "class B extends A { @Override B uret() { return this; } }" },
{ "isaretli, donus tipi genisletilmis", "class B extends A { @Override Object uret() { return this; } }" },
{ "isaretli, parametre tipi daraltilmis", "class B extends A { @Override String karsila(B b) { return \"B\"; } }" },
};
public static void main(String[] args) throws Exception {
System.out.printf("%-38s %s%n", "alt sinif", "sonuc");
for (String[] d : DENEME)
System.out.printf("%-38s %s%n", d[0], Derle.olur(TABAN + d[1] + "\n"));
}
}
alt sinif sonuc isaretsiz, ad yanlis yazilmis derlendi isaretli, ad yanlis yazilmis derlenmedi isaretli, donus tipi daraltilmis derlendi isaretli, donus tipi genisletilmis derlenmedi isaretli, parametre tipi daraltilmis derlenmedi
İlk iki satır işaretin bütün değerini taşıyor. Adı yanlış yazılmış bir yöntem, işaret
konmadığında derleniyor: derleyici için ortada bir kusur yoktur, alt sınıfa yeni bir
yöntem eklenmiştir. Program çalışır, hiçbir uyarı çıkmaz ve üst sınıfın sürümü çağrılmaya
devam eder. Aynı kaynak @Override ile derlenmiyor. İşaret bir davranış eklemez; yalnız
bir varsayımı derleyiciye bildirir ve varsayım tutmadığında kaynağı reddettirir.
Üçüncü ile dördüncü satır dönüş tipinin ne kadar oynayabildiğini ölçüyor. Yeniden tanımlayan
yöntem dönüş tipini daraltabilir — üstteki A döndürüyorsa alttaki B döndürebilir — ama
genişletemez. Nedeni alt tip sözleşmesidir: A bekleyen çağıran taraf bir B aldığında
sözleşme bozulmaz, ama Object aldığında bozulur. Daraltma çağırana verilen sözü güçlendirir,
genişletme zayıflatır.
Beşinci satır sıradaki bölümün kapısını açıyor. Parametre tipi daraltıldığında işaretli kaynak derlenmiyor: ortada bir yeniden tanımlama yoktur. Dönüş tipinde serbest olan daraltma, parametrede yasaktır — ve bunun sonucu yalnız bir derleme hatası değildir.
Sınırlayıcı Ölçüm: İmza Değişince Karar Taraf Değiştirir
@Override yazılmadığında aynı bildirim derlenir. O zaman ne olur? Çekirdeğe eklenen
karsila çifti tam olarak bu durumdadır: Ust bir Ust alır, Alt bir Alt alır, imzalar
denk düşmez, dolayısıyla ortada yeniden tanımlama değil aşırı yükleme vardır.
- SN15 — İki çağrıda da nesne aynı nesnedir ve argüman aynı argümandır; değişen tek şey çağrının yapıldığı referansın bildirilen tipidir.
// Imza.java — imza değişince yeniden tanımlama yerine aşırı yükleme oluyor
public class Imza {
public static void main(String[] args) {
Ust d = new Alt();
Alt gercek = new Alt();
System.out.println("Ust referansindan karsila : " + d.karsila(gercek));
System.out.println("Alt referansindan karsila : " + ((Alt) d).karsila(gercek));
System.out.println("Ust referansindan ad() : " + d.ad());
System.out.println("Alt referansindan ad() : " + ((Alt) d).ad());
}
}
Ust referansindan karsila : Ust.karsila(Ust) Alt referansindan karsila : Alt.karsila(Alt) Ust referansindan ad() : alt yontem Alt referansindan ad() : alt yontem
Dört satır ikişer ikişer okunur. Alt iki satır yeniden tanımlamanın imzasıdır: iki ayrı referans, tek yanıt. Referansın bildirilen tipini değiştirmek sonucu değiştirmiyor, çünkü kararı veren taraf o değil.
Üst iki satır ise aşırı yüklemenin imzasıdır: aynı nesne, aynı argüman, iki ayrı yanıt. Bir tek karakter bile değişmeden yazılabilecek iki çağrı, yalnız referansın bildirilen tipi yüzünden ayrı gövdelere gidiyor. Karar tarafı yer değiştirmiştir.
Sınırın anlamı budur. Bir alt sınıfa yöntem yazan programcı çoğu zaman yeniden tanımladığını
sanır; imzada bir harf ya da bir parametre tipi kayarsa yeniden tanımlama hiç doğmaz ve
yerine, çalışma zamanı tipini hiç dinlemeyen bir seçim geçer. Program derlenir, çalışır ve
üst sınıfın sürümünü çağırır. Bu yüzden @Override, kaynağa eklenen bir açıklama değil,
kararın hangi taraftan geleceğini derleme zamanında dondurmanın tek yoludur.
Özet
- Tek referans üzerinde sekiz karar okunur ve yanıt yarı yarıya bölünür: dördü çalışma zamanı tipinden, dördü bildirilen tipten gelir.
- Ayrım kuralı tektir: çağrılan davranış çalışma zamanı tipine, seçilen ad bildirilen tipe bakar. Arayüzden gelen varsayılan yöntem de yeniden tanımlanabilir ve ilk gruba girer.
- Aşağı dönüşüm nesneyi değiştirmez, bildirilen tipi değiştirir:
((Alt) d).etiketalt alan verirken nesne baştan beri aynı nesnedir. - Alanların aynı adla yeniden bildirilmesi gizlemedir: tek nesne
etiketsorusuna iki yanıt verir ve birine yazmak ötekini değiştirmez — gizlenen alan yok olmaz. @Overridebir davranış eklemez, bir varsayımı bildirir: adı yanlış yazılmış yöntem işaretsiz derlenir, işaretli derlenmez. Dönüş tipi daraltılabilir, genişletilemez.- İmza denk düşmediğinde yeniden tanımlama doğmaz, aşırı yükleme olur ve karar taraf
değiştirir:
karsilaiki referanstan iki ayrı yanıt verirkenad()tek yanıt verir.
Sonraki Adım
Bu derste Ust bir gövde taşıyordu: her yöntemin bir gerçekleştirimi vardı ve alt sınıf
onları isterse yeniden tanımlıyordu. Arayüzden gelen varsayılan yöntem de tabloya bir satır
olarak girdi, ama arayüzün ne işe yaradığı sorulmadı. Sıradaki ders soyutlamanın iki aracını
yan yana koyar: bir yetenek soyut sınıfla da arayüzle de kurulabilir, biri tek üstle
sınırlıdır, öteki çoklu. Ölçüm iki arayüzün aynı varsayılan yöntemi verdiği duruma bakar —
derleyici kaç durumda karar vermeyi reddediyor, kaç durumda sessizce bir taraf kazanıyor — ve
durum taşıyan bir yeteneğin arayüzle neden kurulamadığını gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.