Java ile Nesne Yönelimli Programlama · Ders Notu
Bölüm 4.3: Kalıtım
Bir yazılımda birbirine çok benzeyen sınıflar sık görülür. Öğrenci ile öğretmenin adı ve yaşı vardır; kedi, köpek ve kuş yer, uyur ve ses çıkarır. Bu ortak kısmı her sınıfta yeniden yazmak hem emek ister hem de hataya kapı açar: bir yerde yapılan düzeltme öbür yerde unutulur. Kalıtım bu sorunu çözer. Ortak alan ve metotlar bir kez, bir üst sınıfta yazılır; alt sınıflar onları kalıtımla alır ve yalnız kendi farklarını ekler. Not önce kalıtımın ne olduğunu, extends söz dizimini ve alt sınıfa nelerin geçtiğini anlatır. Ardından super anahtar kelimesini, yapıcı zincirini ve ilklendirme sırasını adım adım izler. Metot geçersiz kılma ve kuralları, protected erişim ve final ile kalıtımı sınırlama bunu izler. Son kesimler sınıflar arasındaki is-a ve has-a ilişkilerini, kalıtımın tasarım tuzaklarını ve her sınıfın atası olan Object sınıfının toString, equals ve hashCode metotlarını işler.
Öğrenme çıktıları
- Kavram
- Kalıtımı, üst sınıf ve alt sınıf kavramlarını tanımlamak; is-a ilişkisini tanımak.
- Söz dizimi
extendsile alt sınıf yazmak; neyin kalıtımla alındığını, neyin alınmadığını açıklamak.- Yapıcılar
super(...)vesuper.metot()kullanmak; yapıcı zincirinin ve ilklendirmenin çalışma sırasını izlemek.- Geçersiz kılma
- Metodu geçersiz kılmak, aşırı yüklemeden ayırmak,
@Overridekullanmak ve kuralların gerekçesini açıklamak. - Erişim ve sınırlama
protectederişimini vefinalile kalıtımın nasıl sınırlandığını açıklamak.- Tasarım
- Kalıtım ile oluşum arasında seçim yapmak; kırılgan üst sınıf ve yerine koyma ilkesi sorunlarını tanımak.
- Object sınıfı
toString,equalsvehashCodemetotlarını doğru yazmak;==ileequalsfarkını bilmek.
4.3.1Kalıtım kavramı
Sınıf, aynı yapıdaki nesnelerin kalıbıdır: hangi verileri tuttuklarını ve hangi işleri yaptıklarını söyler. Gerçek bir yazılımda sınıflar birbirinden bağımsız durmaz. Bir kısmı ötekilerin daha özel bir biçimidir. Her dikdörtgen bir şekildir, her öğrenci bir kişidir, her kedi bir hayvandır. Kalıtım, sınıflar arasındaki bu “daha özel biçimi” ilişkisini koda döker. Bu kesim kalıtımı tanımlar, Java'daki söz dizimini verir, kalıtımla neyin alındığını ve bir alt sınıf nesnesinin bellekte nasıl durduğunu anlatır.
4.3.1.1Kalıtım nedir?
Kalıtım (inheritance), var olan bir sınıfın alan ve metotlarını alıp onları yeni ya da değiştirilmiş yeteneklerle zenginleştirerek yeni bir sınıf oluşturmaktır. Var olan sınıfa üst sınıf (superclass), yeni sınıfa alt sınıf (subclass) denir.
Tanımdaki iki fiil önemlidir: almak ve zenginleştirmek. Alt sınıf, üst sınıfın alan ve metotlarını yeniden yazmadan alır. Bu alınan üyeler alt sınıfta tekrar tanımlanmaz; alt sınıfın kendi üyeleri gibi kullanılır. Alt sınıf bunların üstüne yeni alanlar ve yeni metotlar ekleyebilir. Üst sınıftan aldığı bir metodun davranışını da değiştirebilir.
Gündelik dildeki bir cümle ilişkiyi iyi özetler: “Her dikdörtgen bir şekildir.” Bir çizim programında her şeklin bir rengi vardır ve her şekil çizilebilir. Dikdörtgen bu ortak özelliklerin hepsini taşır. Üstüne kendi eni ve boyu vardır; alanı da bu ikisinden hesaplanır. Bu ilişki bir diyagramla da gösterilebilir. Şekil 4.3.1 onu UML (Unified Modeling Language, birleşik modelleme dili) sınıf diyagramıyla gösterir.
Dikdortgen sınıfı Sekil sınıfından türer. UML'de kalıtım, ucu üst sınıfa dönük, içi boş üçgen okla çizilir. Alt sınıfın kutusunda yalnız kendi eklediği üyeler yazılır; renk ve ciz() kalıtımla gelir.Diyagramda Dikdortgen'in kutusunda renk alanı ve ciz() metodu yoktur. Bunlar yazılmamıştır, ama her Dikdortgen nesnesinde bulunurlar. Kalıtım tam da budur: ortak kısım bir kez, üst sınıfta yazılır. Her alt sınıf yalnız kendi farkını taşır. Oku izleyen bir okuyucu “Dikdortgen bir Sekil'dir” cümlesini kurar. Okun yönü bu cümlenin yönüdür.
4.3.1.2extends ile alt sınıf
Java'da bir sınıfın başka bir sınıftan türediği extends anahtar kelimesiyle bildirilir. Söz dizimi class AltSinif extends UstSinif biçimindedir. extends İngilizcede “genişletir” demektir: alt sınıf, üst sınıfı genişletir.
// Temel sınıf (üst sınıf)
public class Sekil {
// Temel sınıfa ait nitelik ve metotlar
// ...
}
// Yeni sınıf (alt sınıf), Sekil'den türer
public class Dikdortgen extends Sekil {
// Yeni sınıfa ait nitelik ve metotlar
// ...
}Bu iki sınıfın gövdesi henüz boştur, ama ilişki kurulmuştur. Dikdortgen türünden her nesne artık bir Sekil'dir. Sekil'e yazılacak her public ve protected üye Dikdortgen'de de kullanılabilir. Alt sınıfın kodu bu üyeleri kendi üyeleriymiş gibi, nesne adı ya da ön ek olmadan çağırır.
extends sözcüğünden sonra yalnız bir sınıf yazılabilir. Java sınıflarda çoklu kalıtıma izin vermez. Bir sınıfta extends hiç yazılmamışsa sınıfın yine de bir üst sınıfı vardır. Derleyici o sınıfı gizlice java.lang.Object'ten türetir. Şekil 4.3.2 bu iki adımlı zinciri gösterir.
Dikdortgen, Sekil'i açıkça genişletir. Sekil'de extends yazılmadığı için derleyici onu Object'ten türetir. Zincir her zaman Object'te biter.Alt sınıfta yapılmaması gereken bir şey de vardır. Üst sınıftaki bir alanla aynı adda bir alan alt sınıfta yeniden tanımlanırsa Java hata vermez. Ama bu iki alan aynı değişken değildir. Nesnede iki ayrı değişken oluşur; alt sınıfın kodu kendi alanını, üst sınıfın kodu kendininkini görür. Bu duruma alan gizleme (field hiding) denir. Hangi kodun hangi değeri okuduğunu izlemek zorlaşır ve hatalar sessizce ortaya çıkar. Kalıtımla gelen alan alt sınıfta yeniden tanımlanmaz.
Üst sınıfta String renk varken alt sınıfa da String renk eklemek. Alt sınıfın yapıcısında renk = "kırmızı" yazılır; üst sınıfın ciz() metodu ise kendi renk alanını okur ve null görür. Doğrusu, alanı yalnız üst sınıfta tutmak ve değerini üst sınıfın yapıcısıyla vermektir.
4.3.1.3Alt ve üst sınıf: özelleştirme
Alt sınıf, üst sınıfın bütün davranışlarını sergiler. Buna ek olarak kendine özgü davranışlar da sergileyebilir. Bir kedi bir hayvanın yaptığı her şeyi yapar: yer, uyur, nefes alır. Ayrıca miyavlar ve tırmanır. Bu yüzden kalıtıma özelleştirme (specialization) de denir. Alt sınıf, üst sınıfın temsil ettiği grubun içindeki daha özel bir grubu temsil eder.
İlişki tek yönlüdür. Her kedi bir hayvandır, ama her hayvan kedi değildir. Kod düzeyinde bunun karşılığı açıktır. Hayvan tipinde bir değişkene bir Kedi nesnesi atanabilir, çünkü bir kedi hayvandan beklenen her şeyi yapar. Tersi ise olmaz. Bir Kedi değişkenine sıradan bir Hayvan nesnesi atanamaz; o nesnenin miyavlayabileceği bilinmez.
Object vardır. Yukarı çıkıldıkça sınıflar genelleşir, aşağı inildikçe özelleşir.Java'nın sınıf hiyerarşisi java.lang paketindeki Object sınıfıyla başlar. Şekil 4.3.3 bu hiyerarşiden bir kesit verir. Hayvan, String ve Sekil doğrudan Object'ten türer; Kedi ve Kopek ise Hayvan üzerinden dolaylı olarak türer. Her sınıf, doğrudan ya da dolaylı olarak Object'ten kalıtım alır. Bu yüzden her nesnede Object'in metotları hazır bulunur.
4.3.1.4Kalıtım türleri
Sınıflar arasındaki kalıtım ilişkileri dört biçimde düzenlenebilir. Şekil 4.3.4 dördünü yan yana gösterir.
- Tek düzeyli (single) kalıtımda bir alt sınıf ve onun bir üst sınıfı vardır:
Kedi extends Hayvan. - Çok düzeyli (multilevel) kalıtımda sınıflar bir zincir oluşturur:
Hayvan extends CanliveKedi extends Hayvan.Kedi,Canli'nin üyeleriniHayvanüzerinden alır. Her düzey bir öncekinin her şeyini taşır. - Hiyerarşik (hierarchical) kalıtımda bir üst sınıftan birçok alt sınıf türer:
Kedi,KopekveKusüçü deHayvan'dan türer. Ortak kod bir kez yazılır; her alt sınıf yalnız kendi farkını ekler. - Çoklu (multiple) kalıtımda bir sınıfın birden çok üst sınıfı olur. Java sınıflarda buna izin vermez. Aynı ihtiyaç arayüzlerle karşılanır: bir sınıf tek bir sınıfı genişletirken birçok arayüzü uygulayabilir.
Bu kuralların sonucu olarak Java'daki sınıf hiyerarşisi bir ağaçtır. Ağacın kökü Object'tir. Object dışındaki her sınıfın tam olarak bir doğrudan üst sınıfı vardır. Zincirin derinliği için bir sınır yoktur. Ama derin zincirler okumayı ve değiştirmeyi zorlaştırır: bir sınıfın davranışını anlamak için yukarıdaki bütün sınıflara bakmak gerekir. Uygulamada iki üç düzeyden derin hiyerarşiler ancak iyi bir gerekçeyle kurulur.
4.3.1.5Neler kalıtımla alınır?
“Alt sınıf, üst sınıfın her şeyini alır” cümlesi kısmen doğrudur. Doğru olan kısmı şudur: alt sınıf nesnesinin içinde üst sınıfın bütün alanları vardır. Ama bu alanlara ve metotlara alt sınıfın kodundan doğrudan erişilip erişilemeyeceğini erişim belirleyicisi belirler. Yapıcılar ise hiç kalıtımla alınmaz. Tablo 4.3.1 durumu üye türüne göre özetler.
| Üye | Alt sınıfa geçer mi? | Not |
|---|---|---|
public / protected alan ve metotlar | Evet | Alt sınıfta doğrudan kullanılır. |
| Belirleyicisiz (paket erişimli) üyeler | Yalnız aynı paketteyse | Başka paketteki alt sınıf erişemez. |
private alanlar | Nesnede yer alır, ama doğrudan erişilemez | Üst sınıfın public / protected metotlarıyla (getter, setter) kullanılır. |
private metotlar | Hayır | Görünmez; geçersiz kılınamaz. |
| Yapıcılar (constructors) | Hayır | Alt sınıf yapıcısı üst sınıfınkini super(...) ile çağırır. |
static üyeler | Erişilebilir | Sınıfa aittir; geçersiz kılınmaz, yalnız gizlenebilir. |
Tablonun en çok karıştırılan satırı private alanlarınkidir. Bir Hayvan'ın private double saglik alanı olsun. Kedi extends Hayvan yazıldığında her Kedi nesnesinde bir saglik alanı bulunur. Kedi uyuduğunda sağlığı artar, değer bu alanda saklanır. Ama Kedi sınıfının kodunda saglik += 5 yazılırsa derleyici saglik has private access in Hayvan iletisiyle hata verir. Alan oradadır, ama adıyla erişim yalnız Hayvan'ın kendi koduna açıktır. Kedi o alana Hayvan'ın sunduğu metotlar üzerinden ulaşır.
Yapıcıların kalıtımla alınmamasının da mantıklı bir nedeni vardır. Yapıcının adı sınıfın adıdır. Hayvan(String isim) yapıcısı Kedi'ye geçseydi new Kedi("Mavi") çağrısı yalnız Hayvan kısmını kurar, Kedi'nin kendi alanları kurulmadan kalırdı. Bu yüzden her sınıf kendi yapıcısını yazar. Alt sınıfın yapıcısı, üst sınıf kısmının kurulması için üst sınıfın yapıcısını super(...) ile çağırır.
Hayvan sınıfında şu üyeler olsun: private String isim, protected int yas, paket erişimli void yemekYe(), public String getIsim() ve private void sindir(). Kedi extends Hayvan başka bir pakette yazılıyor.
Kedi'nin kodunda yas (protected) ve getIsim() (public) doğrudan kullanılır. isim nesnede vardır, ama ona yalnız getIsim() ile ulaşılır. yemekYe() paket erişimlidir ve Kedi başka pakette olduğu için görünmez. sindir() ise private'tır, hiç görünmez. Kedi aynı paketteki bir sınıf olsaydı yemekYe() de kullanılabilirdi.
4.3.1.6Alt sınıf nesnesinin yapısı
Kalıtımı ilk öğrenenler çoğu zaman şöyle bir resim kurar: new Kedi(...) yazılınca bir Kedi nesnesi ve onun içinde ya da yanında bir Hayvan nesnesi oluşur. Bu resim yanlıştır. new Kedi(...) tek bir nesne oluşturur. Üst sınıf için ayrı bir nesne oluşmaz.
Kedi nesnesi. Object, Hayvan ve Kedi kısımları aynı nesnenin içindedir. Yığındaki k değişkeni bu tek nesneyi gösterir. this ve super aynı nesneye bakar.Şekil 4.3.5 nesnenin içini gösterir. Nesne katmanlardan oluşur. En içte Object'ten gelen kısım durur. Onun üstünde Hayvan'ın alanları (isim, saglik), en üstte de Kedi'nin kendi alanı (evcil) yer alır. Hayvan'ın alanları private olsa bile bu nesnenin içindedir. Yalnız Kedi'nin kodu onlara adıyla erişemez.
super anahtar kelimesi bu resimde ayrı bir nesneyi göstermez. super, aynı nesnenin üst sınıf kısmına üst sınıfın kodu üzerinden ulaşmanın yoludur. this ile super aynı adresi taşır; fark, hangi sınıfın metodunun çağrılacağındadır. Bu ayrım yapıcı zincirini ve geçersiz kılmayı anlamanın anahtarıdır.
Bunun bir kanıtı vardır. Object'in getClass() metodu, nesnenin çalışma zamanındaki gerçek sınıfını döndürür. Aşağıdaki programda getClass() çağrısı Hayvan'ın yapıcısının içindedir:
class Hayvan {
Hayvan() {
System.out.println("Hayvan() içinde: "
+ getClass().getSimpleName());
}
}
class Kedi extends Hayvan { }
// main içinde:
new Kedi();
new Hayvan();Hayvan() içinde: Kedi
Hayvan() içinde: Hayvanİlk satır şaşırtıcı görünebilir. Çalışan kod Hayvan'ın yapıcısıdır, ama nesnenin sınıfı Kedi çıkar. Nedeni şudur: new Kedi() önce bir Kedi nesnesi için yer ayırır, sonra bu nesnenin parçalarını sırayla kurar. Hayvan() çalışırken kurduğu şey bir Kedi'nin Hayvan kısmıdır. Ayrı bir Hayvan nesnesi yoktur. İkinci satırda ise new Hayvan() gerçekten bir Hayvan nesnesi oluşturur. Kedi sınıfında yapıcı yazılmamıştır; derleyici ona parametresiz bir varsayılan yapıcı ekler ve bu yapıcı Hayvan()'ı çağırır.
4.3.1.7Ortak kodu üst sınıfa taşımak
Kalıtım çoğu zaman tasarımın başında kurulmaz. Önce birkaç sınıf yazılır, sonra aralarında tekrar eden kod fark edilir. Bu tekrarı bulup ortak bir üst sınıfa taşımaya genelleştirme (generalization) denir. Genelleştirme, özelleştirmenin ters yönüdür. Özelleştirmede genel bir sınıftan özel bir sınıfa inilir; genelleştirmede özel sınıflardan ortak bir genel sınıfa çıkılır.
Bir okul yönetim programında başlangıçta şu iki sınıf olsun:
class Ogrenci {
private String ad;
private int yas;
private double ortalama;
String kimlik() { return ad + " (" + yas + ")"; }
}
class Ogretmen {
private String ad;
private int yas;
private String brans;
String kimlik() { return ad + " (" + yas + ")"; }
}İki sınıfta ad ve yas alanları da kimlik() metodu da birebir aynıdır. Bu tekrarın bedeli değişiklik anında ödenir. Yaşın gösterim biçimi değişsin; örneğin “(20)” yerine “20 yaş” yazılsın. İki sınıfın da düzeltilmesi gerekir. Biri unutulursa program aynı bilgiyi iki farklı biçimde yazar. Sınıf sayısı arttıkça unutma olasılığı da artar.
Ortak kısım bir Kisi sınıfına taşınınca kod şöyle olur:
class Kisi {
private String ad;
private int yas;
Kisi(String ad, int yas) { this.ad = ad; this.yas = yas; }
String kimlik() { return ad + " (" + yas + ")"; }
}
class Ogrenci extends Kisi {
private double ortalama;
Ogrenci(String ad, int yas, double ortalama) {
super(ad, yas);
this.ortalama = ortalama;
}
@Override String kimlik() {
return super.kimlik() + ", ort. " + ortalama;
}
} // Ogretmen de aynı biçimde: brans eklenirOgrenci artık yalnız kendi farkını taşır: ortalama alanını. Adı ve yaşı kendisi saklamaz; yapıcısında bunları super(ad, yas) ile Kisi'nin yapıcısına verir. kimlik() metodunu geçersiz kılar, ama ortak kısmı yeniden yazmaz. super.kimlik() ile Kisi'nin sürümünü çağırır ve sonuna ortalamayı ekler. Ogretmen de aynı biçimde yazılır: alanı brans'tır ve kimlik()'in sonuna branşı ekler. Program çalışınca iki satır yazılır:
Ece (20), ort. 3.4
Murat (41), FizikArtık yaşın gösterim biçimi tek bir yerde, Kisi.kimlik()'te değişir; iki alt sınıf da yeni biçimi kendiliğinden kullanır.
Genelleştirmeden önce sorulacak bir soru vardır: “Her öğrenci bir kişi midir?” Cevap evetse genelleştirme doğrudur. İki sınıfın yalnız birkaç alanı benziyorsa ve aralarında “bir türüdür” ilişkisi yoksa kalıtım yanlış araçtır. Örneğin bir Ogrenci ile bir Ders sınıfının ikisinde de ad alanı olabilir. Ama bir ders bir öğrenci değildir. Böyle durumlarda ortak kod ayrı bir yardımcı sınıfa konur ve oluşumla kullanılır.
Bir elektrik devresi benzetim programında Direnc, Kondansator ve Bobin sınıfları olsun. Üçünde de iki düğüm numarası (dugumA, dugumB), bir ad ve bagla(int a, int b) metodu tekrar ediyor. Her birinin kendine özgü değeri ise farklı: direnç, sığa ve endüktans.
“Her direnç bir devre elemanı mıdır?” sorusunun cevabı evettir; diğer ikisi için de öyledir. Ortak alanlar ve bagla metodu bir DevreElemani üst sınıfına taşınır. Her alt sınıfta yalnız kendi değeri ve o değere bağlı hesap kalır; örneğin bir empedans(double frekans) metodu. Düğüm numaralarının denetimi değişirse yalnız DevreElemani düzeltilir.
4.3.2Hayvan örneği
Kalıtımın parçaları tek bir örnek üzerinde birlikte görüldüğünde daha kolay anlaşılır. Bu kesim bir hayvan hiyerarşisi kurar. Ortak üyeler bir üst sınıfta toplanır, alt sınıflar bir davranışı değiştirir ve kendilerine özgü davranış ekler.
4.3.2.1Hayvan sınıf hiyerarşisi
Kedi, Kopek, Kus ve At sınıfları Hayvan'dan türer. Her biri konus() metodunu kendine göre geçersiz kılar ve kendine özgü bir metot ekler. - işareti private, + işareti public üyeyi gösterir.Şekil 4.3.6 hiyerarşik kalıtımın tipik bir örneğidir. Hayvan sınıfı bütün hayvanlarda ortak olan alanları taşır: isim, yas, saglik. Ortak davranışlar da onda yazılıdır: konus, yemekYe, uyu, toString. Bu üyeler bir kez, Hayvan'da yazılır. Dört alt sınıfın kutusunda ise yalnız iki metot vardır. Biri konus()'tur; her hayvan farklı ses çıkardığı için her alt sınıf bu metodu kendine göre yeniden yazar. Öteki o hayvana özgü bir davranıştır: kedi avlanır, köpek top oynar, kuş uçar, at koşar.
Diyagramdan bir sonuç daha çıkar. Yeni bir hayvan türü eklemek için Hayvan'a dokunmak gerekmez. Örneğin bir Inek sınıfı Hayvan'dan türetilir, konus()'u geçersiz kılar ve bir sagil() metodu ekler. Uyuma, yemek yeme ve sağlık takibi kendiliğinden gelir.
4.3.2.2Hayvan ve Kopek sınıfları
Hiyerarşinin kodu iki sınıfla başlar. Üst sınıf Hayvan, bütün hayvanlarda ortak olan alanları ve davranışları içerir:
public class Hayvan {
private String isim; // ortak nitelikler
private int yas;
private double saglik;
public Hayvan(String isim, int yas) {
this.isim = isim;
this.yas = yas;
this.saglik = 100.0;
}
public String getIsim() { return isim; } // private isim'e erişim yolu
public void konus() {
System.out.println("Bu hayvan konuşur.");
}
public void uyu() {
saglik += 5;
System.out.println(isim + " uyudu. Sağlık: " + saglik);
}
}Üç alan da private'tır. Dışarıdan ve alt sınıflardan doğrudan erişilemezler. isim'e erişim yolu getIsim() metodudur. Yapıcı iki değer alır ve sağlığı 100 ile başlatır. konus() genel bir ileti yazar; hangi hayvanın nasıl konuştuğu henüz bilinmez. uyu() sağlığı 5 artırır ve durumu yazar.
Alt sınıf Kopek yalnız farkı yazar:
public class Kopek extends Hayvan {
public Kopek(String isim, int yas) {
super(isim, yas); // üst sınıf yapıcısı alanları doldurur
}
@Override
public void konus() { // miras alınan davranışı değiştirir
System.out.println(getIsim() + " havlar.");
}
public void topOyna() { // yalnızca Kopek'te olan davranış
System.out.println(getIsim() + " top oynuyor!");
}
}Sınıf üç şey yapar. Yapıcısı, aldığı adı ve yaşı super(isim, yas) ile Hayvan'ın yapıcısına verir; alanları Hayvan doldurur. konus() metodunu geçersiz kılar: @Override etiketi, bu metodun üst sınıftan gelen bir metodu yeniden yazdığını bildirir. Son olarak yalnız köpeklerde olan topOyna() metodunu ekler. Dikkat edilirse Kopek'in kodu isim alanına doğrudan dokunmaz; adı getIsim() ile alır. isim private olduğu için başka yol da yoktur.
Kopek kopek = new Kopek("Karabaş", 4);
kopek.konus(); // Kopek'in sürümü
kopek.topOyna(); // Kopek'e özgü
kopek.uyu(); // Hayvan'dan mirasKarabaş havlar.
Karabaş top oynuyor!
Karabaş uyudu. Sağlık: 105.0Üç çağrı üç farklı durumu gösterir. kopek.konus() Kopek'in geçersiz kıldığı sürümü çalıştırır; “Bu hayvan konuşur.” yazılmaz. kopek.topOyna() yalnız Kopek'te olan metottur. kopek.uyu() ise Kopek'te hiç yazılmamıştır. Java bu metodu önce Kopek'te arar, bulamayınca bir üst sınıfa, Hayvan'a çıkar ve orada bulur. Metot Hayvan'ın kodudur, ama bu köpek nesnesinin saglik alanını 100'den 105'e çıkarır.
Metot arama kuralı kalıtımın temel işleyişidir. Bir nesne üzerinde bir metot çağrıldığında arama nesnenin gerçek sınıfından başlar. Metot orada yoksa hiyerarşide yukarı çıkılır. İlk bulunan sürüm çalışır. Object'e kadar çıkıp bulunamayan bir metodu ise derleyici zaten kabul etmez.
4.3.2.3Kalıtımın avantajları
Kalıtımın üç yararı vardır.
- Yeniden kullanım. Üst sınıfın metot ve alanları alt sınıfta yeniden yazılmadan kullanılır. Ortak kod tek yerde durur; bir düzeltme bütün alt sınıflara yansır.
- Genişletme. Var olan bir sınıf, kodu değiştirilmeden yeni yeteneklerle genişletilir. Üst sınıf başka bir ekibin ya da bir kütüphanenin olabilir; kaynak koduna dokunmak gerekmez.
- Değiştirme. Üst sınıfın metotları alt sınıfta geçersiz kılınarak (overriding) değiştirilebilir. Alt sınıf, ortak davranışın yalnız kendisine uymayan kısmını yeniden yazar.
Genişletmeye bir hesap makinesi örneği verilebilir. Elimizde dört işlem yapan, son sonucu bir hafıza alanında tutan bir HesapMakinesi sınıfı olsun. Sunumda yalnız diyagramı verilen bu sınıf şöyle yazılabilir:
class HesapMakinesi {
private double hafiza;
public double topla(int a, int b) { hafiza = a + b; return hafiza; }
public double carp(int a, int b) { hafiza = a * b; return hafiza; }
public double hafizayiGoruntule() { return hafiza; }
}GelismisHesapMakinesi, HesapMakinesi'nden türer ve us ile faktoriyel metotlarını ekler. Üst sınıfın kodu değişmez.Bilimsel hesap gereken bir kullanıcı için üs alma ve faktöriyel eklenecek olsun. Şekil 4.3.7 çözümü gösterir. Yeni sınıf eskisini genişletir; dört işlem ve hafıza kendiliğinden gelir:
public class GelismisHesapMakinesi
extends HesapMakinesi {
// Yeni yetenek: üs alma
public double us(double taban, int us) {
double sonuc = 1.0;
for (int i = 0; i < us; i++) {
sonuc *= taban;
}
return sonuc;
}
// faktoriyel(int) benzer biçimde eklenir
}
// main içinde
GelismisHesapMakinesi h = new GelismisHesapMakinesi();
System.out.println(h.topla(5, 3)); // 8.0 (miras)
System.out.println(h.hafizayiGoruntule()); // 8.0 (miras)
System.out.println(h.us(2, 5)); // 32.0 (yeni)8.0
8.0
32.0h.topla(5, 3) ve h.hafizayiGoruntule() çağrıları GelismisHesapMakinesi'nde yazılmamıştır; ikisi de kalıtımla gelir ve 8.0 verir. us(2, 5) yeni metottur. Döngü beş tur döner ve sonuc sırasıyla 2, 4, 8, 16, 32 olur. us parametresinin metodun adıyla aynı olması Java'da sorun değildir. Metot adları ile değişken adları ayrı ad alanlarında durur. Okunurluk için yine de farklı adlar seçmek daha iyidir.
Maliyet açısından bakıldığında kalıtımla gelen bir metodu çağırmak, sınıfın kendi metodunu çağırmakla aynı maliyettedir. Java sanal makinesi her sınıf için bir metot tablosu tutar. Arama, tablodaki bir girişe bakmaktan ibarettir; hiyerarşinin derinliği her çağrıda yeniden taranmaz. Yani kalıtımın asıl bedeli çalışma süresinde değil, tasarımda ödenir. Bu bedel, sınıflar arasındaki sıkı bağlılıktır.
4.3.3Yapıcılar ve super
Bir alt sınıf nesnesi kurulurken üst sınıf kısmı da kurulmalıdır. Üst sınıfın alanlarına ilk değer verme işi ise üst sınıfa aittir. Bu kesim alt sınıf ile üst sınıf arasındaki bu işbirliğinin aracını, super anahtar kelimesini anlatır. Ardından yapıcıların hangi sırayla çalıştığını üç ayrı kod izlemesiyle adım adım gösterir.
4.3.3.1super anahtar kelimesi
super, alt sınıfın kodu içinde üst sınıfı gösteren anahtar kelimedir. İki biçimde kullanılır. super(...) alt sınıfın yapıcısı içinde üst sınıfın yapıcısını çağırır. super.metot() ise üst sınıftaki bir metodun sürümünü çağırır; genellikle geçersiz kılınmış bir metodun içinden kullanılır.
super(...) ile ilgili iki kural vardır. Birincisi, super(...) yapıcının ilk deyimi olmalıdır. Bu kural Java 21 dahil bütün yaygın sürümlerde geçerlidir. Java 25 kuralı biraz gevşetmiştir: super(...)'dan önce, kurulmakta olan nesneye dokunmayan deyimler yazılabilir. Örneğin parametreler denetlenebilir. Ama bu not Java 21'i esas alır. İkincisi, yapıcıda super(...) ya da this(...) yazılmazsa derleyici gizlice super(); ekler. Yani bir üst sınıf yapıcısı her zaman çağrılır; programcının tek seçimi hangisinin çağrılacağıdır.
Bu kuralların arkasında bir tasarım ilkesi yatar: her sınıf yalnız kendi verilerini yönetir. Kedi, Hayvan'ın isim alanına kendisi atama yapmaz; işi super(isim)'e bırakır. Hayvan kendi alanlarının nasıl ilk değer alacağını en iyi bilen sınıftır. Belki adın boş olmadığını denetler, belki sağlığı 100'den başlatır. Alt sınıf bu kuralları bilmek zorunda kalmaz.
class Kedi extends Hayvan {
private boolean evcil;
Kedi(String isim, boolean evcil) {
super(isim); // Hayvan(String)
this.evcil = evcil; // kendi verisi
}
@Override
public String toString() {
return super.toString() // Hayvan'ınki
+ ", Evcil: " + evcil;
}
}
// main içinde
System.out.println(new Kedi("Mavi", true));İsim: Mavi, Sağlık: 100.0, Evcil: trueBurada Hayvan(String isim) yapıcısı adı alanına yazar ve sağlığı 100.0 yapar. Hayvan'ın toString() metodu da ad ve sağlık bilgisini "İsim: Mavi, Sağlık: 100.0" biçiminde döndürür. Kedi'nin yapıcısı ilk satırda super(isim) ile bu yapıcıyı çağırır, sonra kendi alanı evcil'i doldurur. Kedi'nin toString()'i ise üst sınıfın ürettiği metni yeniden üretmez. super.toString() ile o metni alır ve sonuna yalnız kendi alanını ekler. println, kendisine verilen nesnenin toString() metodunu çağırır. Nesne bir Kedi olduğu için Kedi'nin sürümü çalışır.
Bu kalıp çok yaygındır: geçersiz kılınan metot önce super ile üst sınıfın işini yaptırır, sonra kendi farkını ekler. Böylece üst sınıfın davranışı değiştiğinde, örneğin Hayvan.toString() yaşı da yazmaya başladığında, Kedi'nin çıktısı da kendiliğinden güncellenir.
4.3.3.2Kod izleme: super.tanit() zinciri
super.metot() çağrıları zincir hâlinde de kullanılabilir. Üç düzeyli bir hiyerarşide her sınıf tanit() metodunu geçersiz kılsın ve her biri önce üst sınıfınkini çağırsın:
class Canli {
void tanit() { System.out.println("Canlıyım"); }
}
class Hayvan extends Canli {
void tanit() {
super.tanit();
System.out.println("Hayvanım");
}
}
class Kedi extends Hayvan {
void tanit() {
super.tanit();
System.out.println("Kediyim");
}
}
new Kedi().tanit(); // mainCanlıyım
Hayvanım
KediyimÇıktının sırası ilk bakışta tersmiş gibi görünür. Çağrı Kedi'den başlar, ama ilk yazılan satır Canli'ninkidir. Tablo 4.3.2 çağrıları adım adım izler.
| Adım | Olay | Çağrı yığını | Çıktı |
|---|---|---|---|
| 1 | Nesne bir Kedi; arama en alttan başlar, Kedi.tanit() bulunur. | main, Kedi.tanit | — |
| 2 | super.tanit(): Hayvan.tanit() çağrılır. Kedi.tanit bekler. | main, Kedi.tanit, Hayvan.tanit | — |
| 3 | Hayvan içindeki super sabit olarak Canli'yi gösterir: Canli.tanit(). | main, Kedi.tanit, Hayvan.tanit, Canli.tanit | — |
| 4 | En üstteki sürüm yazar ve döner. | main, Kedi.tanit, Hayvan.tanit | Canlıyım |
| 5 | Hayvan.tanit() kaldığı yerden sürer, yazar ve döner. | main, Kedi.tanit | Hayvanım |
| 6 | Kedi.tanit() kendi satırını yazar ve döner. | main | Kediyim |
super.tanit() zincirinden dört an. (a) Kedi.tanit() çalışır. (b) İki super çağrısıyla yığına Hayvan.tanit ve Canli.tanit eklenir. (c) Canli ilk satırı yazar. (d) Bütün çağrılar döndü; çıktılar ters sırada geldi.Üçüncü adım ince bir noktayı taşır. Nesne hâlâ bir Kedi'dir. Normal bir metot çağrısında arama nesnenin gerçek sınıfından başlar. Bu kurala uyulsaydı Hayvan'ın içindeki çağrı yeniden Kedi.tanit()'e gider ve sonsuz bir döngü oluşurdu. super bu kurala uymaz. Hayvan'ın kodunda yazılan super, nesnenin sınıfı ne olursa olsun, her zaman Hayvan'ın üst sınıfını, yani Canli'yi gösterir. Bu bağ derleme anında kurulur; çalışma zamanında nesneye bakılarak seçilmez.
Şekil 4.3.8 zincirin genel biçimini gösterir. Çağrılar Kedi → Hayvan → Canli yönünde gider, çıktılar ters sırada gelir. Her sınıf önce üst sınıfın işini yaptırır, sonra kendi farkını ekler. Yığın en çok dört çerçeveye ulaşır: main ve üç tanit. Zincirin her düzeyi bir çağrı ve bir çerçeve ekler. Yani d düzeyli bir zincirin süresi ve yığın kullanımı d ile orantılıdır, O(d)'dir. new Hayvan().tanit() çağrılsaydı zincir bir düzey kısa olur ve yalnız “Canlıyım” ile “Hayvanım” yazılırdı.
4.3.3.3Kod izleme: yapıcı zinciri
Yapıcılar da benzer bir zincir oluşturur. Aşağıdaki kodda Hayvan'ın yapıcısı super()'ı açıkça yazar. Yazılmasaydı derleyici aynı satırı kendisi ekleyecekti.
class Hayvan {
private String isim;
private double saglik;
Hayvan(String isim) {
super(); // Object() çağrılır
this.isim = isim;
saglik = 100.0;
System.out.println("Hayvan yapıcısı");
}
}
class Kedi extends Hayvan {
private boolean evcil;
Kedi(String isim, boolean evcil) {
super(isim);
this.evcil = evcil;
System.out.println("Kedi yapıcısı");
}
}
// main içinde
Kedi kedi = new Kedi("Mavi", true);
System.out.println("Nesne hazır");Hayvan yapıcısı
Kedi yapıcısı
Nesne hazırnew Kedi("Mavi", true) ifadesinin değerlendirilmesi on bir adımda incelenebilir. Tablo 4.3.3 bu adımları, nesnenin alanlarının o anki değerleriyle birlikte verir.
| Adım | Çalışan kod | isim | saglik | evcil | Çıktı |
|---|---|---|---|---|---|
| 1 | new: nesneye yer ayrılır | null | 0.0 | false | |
| 2 | Kedi(...) çağrılır | null | 0.0 | false | |
| 3 | super(isim): Hayvan(...); Kedi bekler | null | 0.0 | false | |
| 4 | super(): Object() | null | 0.0 | false | |
| 5 | Object() döndü; this.isim = isim | "Mavi" | 0.0 | false | |
| 6 | saglik = 100.0 | "Mavi" | 100.0 | false | |
| 7 | Hayvan yazar ve döner | "Mavi" | 100.0 | false | Hayvan yapıcısı |
| 8 | Kedi sürer: this.evcil = evcil | "Mavi" | 100.0 | true | |
| 9 | Kedi yazar | "Mavi" | 100.0 | true | Kedi yapıcısı |
| 10 | Nesnenin adresi kedi'ye atanır | "Mavi" | 100.0 | true | |
| 11 | main sürer | "Mavi" | 100.0 | true | Nesne hazır |
new nesneye yer ayırdı; bütün alanlar varsayılan değerde. (b) Hayvan kendi alanlarını doldurdu. (c) Kedi kendi alanını doldurdu. (d) Nesnenin adresi kedi değişkenine atandı.İzlemenin ilk adımı önemlidir. new, yapıcı çalışmadan önce nesnenin bütün alanlarına yer ayırır. Hayvan'dan gelen isim ve saglik da bunlara dahildir. Bütün alanlar varsayılan değerlerini alır: referanslar null, sayılar 0, boolean false. Şekil 4.3.9'un ilk karesi bu durumu gösterir. Yapıcılar alan oluşturmaz, yalnız var olan alanlara değer yazar.
Sonra çağrılar aşağıdan yukarı gider: Kedi → Hayvan → Object. Her yapıcı ilk iş olarak bir üst sınıfın yapıcısını çağırdığı için hiçbiri gövdesine geçemeden bekler. Zincir her zaman Object'te biter. Object()'in üst sınıfı yoktur, gövdesi de boştur; hemen döner. Bundan sonra gövdeler yukarıdan aşağı tamamlanır: önce Hayvan kendi alanlarını doldurup iletisini yazar, sonra Kedi. Sonuç tek cümleyle söylenebilir: yapıcılar yukarıdan aşağıya tamamlanır, Object → Hayvan → Kedi.
Bu sıranın bir gerekçesi vardır. Alt sınıfın yapıcısı, üst sınıftan gelen alanları ve metotları kullanabilmelidir. Örneğin Kedi'nin yapıcısı getIsim()'i çağırıp bir ileti yazmak isteyebilir. Bunun güvenli olması için Hayvan kısmı o anda kurulmuş olmalıdır. Üst sınıfın önce tamamlanması bunu garanti eder.
Son adımda nesne hazırdır ve adresi kedi değişkenine atanır. Yapıcı sürerken kedi değişkeni henüz bir değer taşımaz. Yarım kurulmuş bir nesne main'deki değişkene hiç ulaşmaz.
4.3.3.4Gizli super() ve hatalar
Derleyicinin gizlice eklediği super(); çağrısı çoğu zaman yararlıdır, ama yeni başlayanların en sık karşılaştığı derleme hatasının da kaynağıdır. Üst sınıfta parametresiz bir yapıcı yoksa gizli çağrının karşılığı bulunmaz:
class Hayvan {
private String isim;
Hayvan(String isim) { this.isim = isim; }
}
class Kedi extends Hayvan {
Kedi(String isim) {
// derleyici buraya super(); ekler
System.out.println("Kedi");
}
}error: constructor Hayvan in class Hayvan
cannot be applied to given types;
required: String
found: no argumentsKedi'nin yapıcısında super(...) yazılmadığı için derleyici super(); ekler. Hayvan'da yalnız Hayvan(String) olduğu için bu çağrının gideceği yapıcı yoktur.İleti ilk bakışta kafa karıştırır. Programcı Hayvan'ın yapıcısını çağırmamıştır, ama derleyici bir çağrının yanlış argümanlarla yapıldığını söyler. İletinin söylediği şudur: Hayvan'ın yapıcısı bir String bekler (required: String), oysa çağrıda hiç argüman yoktur (found: no arguments). Argümansız çağrı, Şekil 4.3.10'da görüldüğü gibi, derleyicinin eklediği super();'dır.
Aynı hata, alt sınıfta hiç yapıcı yazılmadığında da çıkar:
class Hayvan {
private String isim;
Hayvan(String isim) { this.isim = isim; }
}
class Kedi extends Hayvan { }Main.java:5: error: constructor Hayvan in class Hayvan cannot be applied to given types;
class Kedi extends Hayvan { }
^
required: String
found: no argumentsHiç yapıcı yazılmamış bir sınıfa derleyici parametresiz bir varsayılan yapıcı (default constructor) ekler. Bu yapıcının gövdesi yalnız super();'dır. Bu yüzden Kedi için eklenen yapıcı da parametresiz Hayvan()'ı arar ve bulamaz. Varsayılan yapıcı yalnız sınıfta hiçbir yapıcı yoksa eklenir. Hayvan'a Hayvan(String) yazıldığı anda onun parametresiz yapıcısı ortadan kalkmıştır.
İkinci tür hata super(...)'ın yerini yanlış seçmektir:
Kedi(String isim) {
System.out.println("Kedi");
super(isim);
}error: call to super must be first
statement in constructorJava 21'de super(...) yapıcının ilk deyimi olmak zorundadır. Önüne bir println bile yazılamaz. Çözüm iki hatada da aynıdır: alt sınıf yapıcısının ilk satırında uygun üst sınıf yapıcısı açıkça çağrılır, yani super(isim); yazılır. Bir başka çözüm üst sınıfa parametresiz bir yapıcı eklemektir. Ama bu yol, Hayvan'ın adsız bir hayvan oluşturmaya izin vermesi demektir; bu çoğu zaman istenmez.
constructor Hayvan in class Hayvan cannot be applied to given types iletisini gören programcı genellikle Kedi'yi oluşturan new satırına bakar ve orada hata arar. Hata ise Kedi sınıfının yapıcısındadır: üst sınıfın parametreli yapıcısı çağrılmamıştır. İletinin satır numarası bunu gösterir; satır, Kedi'nin yapıcı bildirimidir.
Java 25 ile gelen değişiklik bu ikinci hatayı ortadan kaldırır. Java 25'te super(isim)'den önce System.out.println("Kedi") yazılabilir; bu deyim kurulmakta olan nesneye dokunmaz. Ama o deyimlerde de this kullanılamaz, nesnenin metotları çağrılamaz ve alanları okunamaz. Kuralın özü değişmez: üst sınıf kısmı kurulmadan nesne kullanılmaz. İlk hatayı ise hiçbir sürüm çözmez; üst sınıfın parametreli yapıcısı yine açıkça çağrılmalıdır.
4.3.3.5Kod izleme: this(...) ve super(...)
Bir yapıcı, aynı sınıfın başka bir yapıcısını this(...) ile çağırabilir. Bu da yapıcı zincirinin (constructor chaining) bir biçimidir ve tekrar eden ilk değer verme kodunu tek yapıcıda toplar. this(...) da super(...) gibi ilk deyim olmalıdır. Bu yüzden bir yapıcı ya this(...) ile ya super(...) ile başlar, ikisiyle birden değil. Aşağıdaki kodda iki sınıfın ikisinde de iki yapıcı vardır:
class Calisan {
private String ad; private double maas;
Calisan(String ad) {
this(ad, 20000.0);
System.out.println("Calisan(String)");
}
Calisan(String ad, double maas) {
this.ad = ad; this.maas = maas;
System.out.println("Calisan(String, double)");
}
double getMaas() { return maas; }
}
class Yonetici extends Calisan {
private double prim;
Yonetici(String ad) {
this(ad, 5000.0);
System.out.println("Yonetici(String)");
}
Yonetici(String ad, double prim) {
super(ad);
this.prim = prim;
System.out.println("Yonetici(String, double)");
}
double gelir() { return getMaas() + prim; }
}
Yonetici y = new Yonetici("Ayşe"); // main
System.out.println(y.gelir());Calisan(String, double)
Calisan(String)
Yonetici(String, double)
Yonetici(String)
25000.0Tek bir new dört yapıcıyı çalıştırır. Çağrılar şu sırayla gider: Yonetici(String) → this(ad, 5000.0) ile Yonetici(String, double) → super(ad) ile Calisan(String) → this(ad, 20000.0) ile Calisan(String, double) → gizli super() ile Object(). Zincirin son yapıcısı Calisan(String, double)'dır; ilk satırı this ya da super olmadığı için derleyici oraya super() ekler.
| Adım | Olay | ad | maas | prim | Çıktı |
|---|---|---|---|---|---|
| 1 | new; Yonetici(String) çağrılır | null | 0.0 | 0.0 | |
| 2 | this(ad, 5000.0) | null | 0.0 | 0.0 | |
| 3 | super(ad): Calisan(String) | null | 0.0 | 0.0 | |
| 4 | this(ad, 20000.0), gizli super(): Object() döner | null | 0.0 | 0.0 | |
| 5 | Calisan(String, double) gövdesi | "Ayşe" | 20000.0 | 0.0 | Calisan(String, double) |
| 6 | Calisan(String) gövdesinin kalanı | "Ayşe" | 20000.0 | 0.0 | Calisan(String) |
| 7 | Yonetici(String, double): prim = 5000.0 | "Ayşe" | 20000.0 | 5000.0 | Yonetici(String, double) |
| 8 | Yonetici(String) biter | "Ayşe" | 20000.0 | 5000.0 | Yonetici(String) |
| 9 | y.gelir() = 20000.0 + 5000.0 | "Ayşe" | 20000.0 | 5000.0 | 25000.0 |
this(...) ve super(...) izlemesinden dört an. (a) super(ad): Yonetici kısmı, Calisan kısmı bitene kadar bekler. (b) İlk gövde en üstte çalışır; ad ve maas dolar. (c) super(ad) tamamlandı, şimdi prim dolar. (d) Nesnenin adresi y'ye atanır.Tablo 4.3.4 ve Şekil 4.3.11 aynı kuralı bir kez daha gösterir. Çağrılar aşağı doğru, gövdeler yukarıdan başlayarak tamamlanır. Dört yapıcı çalıştı, ama yığında hangi anda kaç yapıcı olursa olsun tek bir nesne kuruldu. prim alanı yedinci adıma kadar 0.0'da kalır. Alt sınıfın kendi alanı, üst sınıf kısmı tamamen bitmeden dolmaz.
Son satırdaki gelir() metodu kalıtımla gelen getMaas()'ı ve kendi prim alanını toplar. maas private olduğu için Yonetici onu adıyla okuyamaz; getter kullanır.
Yapıcı zincirinin bir sınırı vardır: döngü kurulamaz. Calisan(String) this(ad, 1.0)'ı, o da this(ad)'ı çağırsaydı zincir hiç Object'e ulaşmazdı. Derleyici bunu yakalar:
class Calisan {
Calisan(String ad) { this(ad, 1.0); }
Calisan(String ad, double m) { this(ad); }
}error: recursive constructor invocationAynı yapıcıda hem this(...) hem super(...) yazmak da yasaktır. İkisi de ilk deyim olmak ister:
Yonetici(String ad) {
this(ad, 5000.0);
super(ad);
}error: call to super must be first statement in constructorZincirdeki yapıcılardan yalnız sonuncusu super(...) çağırır. Öbürleri this(...) ile ona devreder.
4.3.3.6Kalıtımda ilklendirme sırası
Yapıcılar, bir nesne kurulurken çalışan tek kod değildir. Alan bildirimindeki ilk değerler (int a = deger(...)) ve static bloklar da çalışır. Kalıtım işin içine girince bu parçaların sırası önem kazanır. Aşağıdaki kod sırayı görünür kılar. Her parça çalışırken bir iz yazar:
class Ust {
static { System.out.println("Ust static"); }
int a = deger("Ust alanı");
Ust() { System.out.print("Ust(), "); }
static int deger(String s) {
System.out.print(s + ", ");
return 1;
}
}
class Alt extends Ust {
static { System.out.println("Alt static"); }
int b = deger("Alt alanı");
Alt() { System.out.println("Alt()"); }
}
// main içinde:
new Alt();
new Alt();Ust static
Alt static
Ust alanı, Ust(), Alt alanı, Alt()
Ust alanı, Ust(), Alt alanı, Alt()Çıktı iki aşamayı ayırır: sınıf başlatma ve nesne kurma.
Sınıf başlatma. JVM (Java Virtual Machine, Java sanal makinesi) bir sınıfı ilk kez kullanmadan önce onu başlatır. Bu sırada sınıfın static blokları ve static alanların ilk değerleri çalışır. Bir alt sınıfı başlatmadan önce üst sınıf başlatılır. Bu yüzden önce “Ust static”, sonra “Alt static” yazılır. Sınıf başlatma program boyunca bir kez yapılır.
Nesne kurma. Her new için şu dört adım bu sırayla çalışır:
- Üst sınıfın alan ilk değerleri (
a = deger("Ust alanı")), - üst sınıfın yapıcı gövdesi (
Ust()), - alt sınıfın alan ilk değerleri (
b = deger("Alt alanı")), - alt sınıfın yapıcı gövdesi (
Alt()).
Bunun nedeni derleyicinin alan ilk değerlerini nereye koyduğudur. Derleyici, alan bildirimlerindeki atamaları her yapıcının içine, super(...) çağrısından hemen sonraya yerleştirir. Yani Alt() aslında şöyle derlenir: önce super(), sonra b = deger("Alt alanı"), sonra gövde. Ust() da aynı biçimde: önce super(), yani Object(), sonra a = ..., sonra gövde.
new Alt() için önce Ust'un static bloğu çalışır. (b) Nesne ayrıldı; Ust'un alanı a ilk değerini aldı. (c) Ust() gövdesi çalışırken b hâlâ 0'dır. (d) İkinci new Alt(): static bloklar yeniden çalışmaz.Şekil 4.3.12'nin üçüncü karesi bu sıranın tehlikeli bir sonucunu gösterir. Ust() gövdesi çalışırken alt sınıfın alanı b hâlâ 0'dır. Alt kısım henüz hiç ilk değer almamıştır. Bu ayrıntı, üst sınıfın yapıcısı alt sınıfın geçersiz kıldığı bir metodu çağırdığında önem kazanır:
class Ust {
Ust() { yaz(); }
void yaz() { System.out.println("Ust.yaz"); }
}
class Alt extends Ust {
int b = 7;
String s = "hazır";
@Override void yaz() {
System.out.println("Alt.yaz b=" + b + " s=" + s);
}
}
// main içinde:
Alt a = new Alt();
a.yaz();Alt.yaz b=0 s=null
Alt.yaz b=7 s=hazırnew Alt() önce Ust()'u çalıştırır. Ust() içindeki yaz() çağrısı, nesne bir Alt olduğu için Alt.yaz()'a gider. Ama o anda Alt'ın alan ilk değerleri henüz atanmamıştır: b 0, s null'dır. Kaynak kodda int b = 7 yazdığı hâlde metot 0 görür. Nesne kurulduktan sonra yapılan ikinci çağrı ise doğru değerleri görür.
Üst sınıfın yapıcısında geçersiz kılınabilen bir metodu çağırmak. Alt sınıf bu metodu geçersiz kılarsa metot, alt sınıfın alanları daha ilk değer almadan çalışır. Sonuç varsayılan değerlerle yapılan yanlış bir hesap ya da bir NullPointerException'dır. Yapıcıdan çağrılan metotlar private, final ya da static yapılırsa geçersiz kılınamaz ve sorun ortadan kalkar.
Örnek ilklendirici bloklar ({ ... }) alan ilk değerleriyle aynı yerde, super(...)'dan hemen sonra çalışır. Şu sınıflar verilsin:
class A {
static { System.out.print("As "); }
{ System.out.print("Ab "); }
A() { System.out.print("A "); }
}
class B extends A {
static { System.out.print("Bs "); }
{ System.out.print("Bb "); }
B() { this(0); System.out.print("B "); }
B(int x) { System.out.print("B" + x + " "); }
}Programdaki ilk new B() ne yazar? Önce sınıflar başlatılır: As Bs. Sonra B() çağrılır. İlk satırı this(0) olduğu için gövdesine geçmeden B(int)'e gider. B(int)'in ilk satırında gizli super() vardır: A'nın örnek bloğu ve gövdesi çalışır, Ab A yazılır. super() döndükten sonra B'nin örnek bloğu çalışır: Bb. Ardından B(int)'in gövdesi B0 yazar, en son B()'nin gövdesi B yazar. Sonuç: As Bs Ab A Bb B0 B.
Örnek bloğu B()'de değil, yalnız super(...) çağıran yapıcıda, yani B(int)'te çalışır. Bu yüzden Bb bir kez yazılır. Ardından new B(7) çağrılırsa static bloklar yeniden çalışmaz: Ab A Bb B7.
4.3.4Geçersiz kılma
Alt sınıf, üst sınıftan aldığı bir davranışı her zaman olduğu gibi kullanmak istemez. Köpek “Bu hayvan konuşur.” demez, havlar. Dikdörtgen genel bir şekil gibi değil, dikdörtgen olarak çizilir. Bu kesim alt sınıfın kalıtımla aldığı bir metodu nasıl yeniden yazdığını, bunun hangi kurallara bağlı olduğunu ve bu kuralların neden konduğunu anlatır.
4.3.4.1Metodu geçersiz kılma
Geçersiz kılma (overriding), alt sınıfın üst sınıftan kalıtımla aldığı bir metodu aynı imzayla yeniden yazarak kendi davranışını vermesidir. Metodun imzası, adı ve parametre listesidir (parametrelerin sayısı, tipleri ve sırası).
Geçersiz kılmanın dört temel koşulu vardır:
- Ad ve parametre listesi aynı olmalıdır.
- Dönüş tipi aynı ya da onun bir alt tipi olmalıdır.
- Erişim daraltılamaz:
publicbir metot alt sınıfta dapublickalmalıdır. final,staticveprivatemetotlar geçersiz kılınamaz.
class Sekil {
public void ciz() {
System.out.println("Şekil çiziliyor.");
}
}
class Dikdortgen extends Sekil {
@Override
public void ciz() {
System.out.println("Dikdörtgen çiziliyor.");
}
}
// main içinde
new Sekil().ciz();
new Dikdortgen().ciz();Şekil çiziliyor.
Dikdörtgen çiziliyor.Dikdortgen, ciz()'i aynı ad, aynı (boş) parametre listesi, aynı dönüş tipi ve aynı erişimle yeniden yazar. new Sekil().ciz() üst sınıfın sürümünü, new Dikdortgen().ciz() alt sınıfınkini çalıştırır. Hangi sürümün çalışacağına nesnenin gerçek sınıfı karar verir. Değişkenin tipi Sekil olsa bile durum değişmez: Sekil s = new Dikdortgen(); s.ciz(); yine “Dikdörtgen çiziliyor.” yazar. Bu davranışa dinamik bağlama denir ve çok biçimliliğin temelidir.
Koşullardan biri bozulursa derleyici hata verir. Alt sınıfta public yazmayı unutmak erişimi paket düzeyine daraltır:
// alt sınıfta: void ciz()
error: ciz() in Dikdortgen cannot override
ciz() in Sekil
attempting to assign weaker access
privileges; was publicDönüş tipini değiştirmek de geçersiz kılmayı bozar:
// alt sınıfta: public int ciz()
error: ciz() in Dikdortgen cannot override
ciz() in Sekil
return type int is not compatible
with voidİki iletinin ilk satırı aynıdır: ciz() in Dikdortgen cannot override ciz() in Sekil. Derleyici aynı imzalı iki metot görmüş ve bunu geçersiz kılma girişimi saymıştır. İkinci satır hangi kuralın bozulduğunu söyler. İletiyi okurken asıl bilgi bu ikinci satırdadır.
4.3.4.2Geçersiz kılma ve aşırı yükleme
Geçersiz kılma, adı benzeyen başka bir kavramla sık karıştırılır: aşırı yükleme (overloading). Aşırı yüklemede aynı adlı, ama parametre listesi farklı birden çok metot yazılır. İkisinin farkı Tablo 4.3.5'te özetlenir.
| Geçersiz kılma (overriding) | Aşırı yükleme (overloading) | |
|---|---|---|
| Nerede? | Alt sınıfta, kalıtımla alınan metot için | Aynı sınıfta (ya da alt sınıfta) |
| Ad | Aynı | Aynı |
| Parametreler | Aynı | Farklı (sayı ya da tip) |
| Dönüş tipi | Aynı ya da alt tipi | Serbest |
| Amaç | Kalıtımla alınan davranışı değiştirmek | Aynı işi farklı girdilerle yapmak |
| Seçim zamanı | Çalışma zamanı (nesnenin gerçek tipi) | Derleme zamanı (argüman tipleri) |
Aynı sınıfta ikisi birlikte de bulunabilir:
class Dikdortgen extends Sekil {
@Override
public void ciz() { ... } // geçersiz kılma
public void ciz(String renk) { ... } // aşırı yükleme
}İlk ciz(), Sekil'deki metodu geçersiz kılar. İkincisi ciz(String renk)'tir; parametre listesi farklı olduğu için yeni bir metottur ve ciz()'i aşırı yükler. d.ciz("Mavi") çağrısında derleyici argüman listesine bakar: bir String verilmiştir, dolayısıyla ciz(String) seçilir. Bu metodun gövdesi rengi iletinin başına ekler:
Mavi dikdörtgen çiziliyor.Tablonun son satırı en önemli farkı söyler. Aşırı yüklemede hangi metodun çağrılacağına derleyici, argümanların derleme anındaki tiplerine bakarak karar verir. Geçersiz kılmada ise karar program çalışırken, nesnenin gerçek sınıfına bakılarak verilir. İki kavram da çok biçimliliğin (polymorphism) temelidir. Birincisine derleme zamanı, ikincisine çalışma zamanı çok biçimliliği denir.
4.3.4.3@Override etiketi
@Override bir etikettir (annotation). Metodun, üst sınıftan kalıtımla alınan bir metodu geçersiz kıldığını bildirir. Derleyici bu bildirimi denetler: üst sınıflarda ya da arayüzlerde aynı imzalı bir metot yoksa derleme hatası verir.
public class Dikdortgen extends Sekil {
// miras alınan metodu geçersiz kıl
@Override
public void ciz() {
System.out.println("Dikdörtgen çiziliyor.");
}
}Etiket zorunlu değildir; yazılmasa da geçersiz kılma çalışır. Ama her zaman yazılması önerilir. İki yararı vardır. Birincisi, kodu okuyan biri metodun üst sınıftan geldiğini hemen görür. İkincisi ve daha önemlisi, yazım hatalarını derleme anında yakalar. Metodun adı yanlış yazılmış olsun:
@Override
public void Ciz() { ... }error: method does not override or implement
a method from a supertypeCiz büyük harfle başlar. Java büyük ve küçük harfi ayırt ettiği için Ciz() ile ciz() farklı metotlardır. @Override olmasaydı ne olurdu? Derleyici hata vermezdi; Ciz() sessizce yeni bir metot olurdu. Program çalışır, ama yanlış davranırdı:
class Dikdortgen extends Sekil {
public void Ciz() { // @Override yok
System.out.println("Dikdörtgen çiziliyor.");
}
}
// main içinde:
Sekil s = new Dikdortgen();
s.ciz();Şekil çiziliyor.s.ciz() çağrısı için Dikdortgen'de geçersiz kılınmış bir ciz() yoktur; arama Sekil'e çıkar ve genel sürüm çalışır. Bu tür bir hata ancak program denenirken, belki çok sonra fark edilir. @Override yazılmış olsaydı derleme anında yakalanırdı. Aynı durum parametre tipini yanlış yazmakta, örneğin equals(Object) yerine equals(Nokta) yazmakta da görülür.
4.3.4.4Geçersiz kılma kuralları
Geçersiz kılmanın kuralları tek tek ezberlenebilir, ama hepsi tek bir gerekçeden türer. Tablo 4.3.6 kuralları, bozulduklarında javac'ın verdiği iletilerle birlikte verir. İletiler JDK 21 ile alınmıştır.
| Özellik | Alt sınıftaki metotta izin verilen | Kural bozulursa javac |
|---|---|---|
| Ad ve parametreler | Birebir aynı | Hata yok: yeni bir metot (aşırı yükleme) olur |
| Dönüş tipi | Aynı ya da bir alt tipi (kovaryant) | return type String is not compatible with Hayvan |
| Erişim | Aynı ya da daha geniş: protected → public | attempting to assign weaker access privileges |
| Kontrol edilen istisna | Aynısı, daha özeli ya da hiç | overridden method does not throw Exception |
static | Örnek metodu static olamaz; static metot örnek metodu olamaz | overriding method is static / overridden method is static |
final metot | Geçersiz kılınamaz | overridden method is final |
private metot | Görünmez; aynı adlı metot ilgisiz yeni bir metottur | Hata yok (@Override varsa hata) |
Ortak gerekçe şudur: alt sınıf nesnesi, üst sınıf nesnesinin beklendiği her yerde kullanılabilmelidir. Sekil tipinde bir değişkenle çalışan bir kod, o değişkenin gösterdiği nesnenin aslında Dikdortgen mi Daire mi olduğunu bilmez. Bu kod yalnız Sekil'in verdiği söze güvenir. Üst sınıfın sözü üç şeyden oluşur: metot ne döndürecek, kime açık olacak, hangi istisnaları fırlatabilecek. Alt sınıf bu sözü koruyabilir ya da genişletebilir, ama daraltamaz.
Kurallar bu gerekçeyle tek tek açıklanır. Üst sınıfın metodu public ise her yerden çağrılabilir. Alt sınıf onu paket erişimine indirseydi, Sekil değişkeni üzerinden başka bir paketten yapılan çağrı bir Dikdortgen nesnesine ulaştığında erişim yasak olurdu. Erişimi genişletmek ise sorun yaratmaz. protected bir metodu public yapmak, eski çağıranların hiçbirini bozmaz.
İstisnalarda da durum aynıdır. Üst sınıfın f() metodu hiç kontrol edilen istisna bildirmiyorsa, onu çağıran kod try-catch yazmamıştır. Alt sınıfın sürümü throws Exception derse bu kod hazırlıksız yakalanır. Derleyici bunu önler:
class Ust { void f() { } }
class Alt extends Ust {
@Override void f() throws Exception { }
}error: f() in Alt cannot override f() in Ust
overridden method does not throw ExceptionTersi serbesttir. Üst sınıf throws Exception bildiriyorsa alt sınıf daha özel bir istisna (throws IOException) bildirebilir ya da hiç bildirmeyebilir. Çağıran zaten Exception'ı yakalamaya hazırdır; daha özel bir istisna da bunun içindedir. RuntimeException ve alt sınıfları kontrol edilmeyen istisnalardır; bu kural onlara uygulanmaz.
class U { protected void a() { } public void b() { } } verilsin. class V extends U içinde @Override public void a() { } ve @Override void b() { } yazılıyor. Hangisi derlenir?
a() üst sınıfta protected'tır; alt sınıfta public yapmak erişimi genişletir, derlenir. b() üst sınıfta public'tir; belirleyicisiz yazmak erişimi paket düzeyine indirir. javac b() in V cannot override b() in U ve attempting to assign weaker access privileges; was public iletilerini verir. Erişim sırası dardan genişe şöyledir: private, paket, protected, public.
4.3.4.5Kovaryant dönüş tipi
Dönüş tipi kuralı ilk okumada katı görünür, ama bir esnekliği vardır. Geçersiz kılan metot, üst sınıftaki dönüş tipinin bir alt tipini döndürebilir:
class Hayvan {
Hayvan yavrula() { return new Hayvan(); }
public String toString() { return "Hayvan"; }
}
class Kedi extends Hayvan {
@Override
Kedi yavrula() { return new Kedi(); } // alt tip
void miyavla() { System.out.println("miyav"); }
public String toString() { return "Kedi"; }
}
// main içinde:
Kedi k = new Kedi();
k.yavrula().miyavla(); // dönüşüm gerekmez
Hayvan h = k;
System.out.println(h.yavrula());miyav
KediKovaryant dönüş tipi (covariant return type), geçersiz kılan metodun, üst sınıftaki dönüş tipinin bir alt tipini döndürmesidir.
Hayvan.yavrula() bir Hayvan döndürür. Kedi.yavrula() ise bir Kedi döndürür; Kedi, Hayvan'ın alt tipi olduğu için bu geçerli bir geçersiz kılmadır. Yararı ilk çağrıda görülür. Kedi referansıyla çağıran, sonucu doğrudan Kedi olarak alır. k.yavrula().miyavla() yazılabilir; (Kedi) dönüşümüne gerek kalmaz. Dönüş tipi Hayvan kalsaydı miyavla() çağrısı derlenmezdi, çünkü Hayvan'da böyle bir metot yoktur.
Hayvan referansıyla çağıran için ise söz bozulmaz. h.yavrula()'nın derleme anındaki tipi Hayvan'dır ve gelen nesne gerçekten bir hayvandır, çünkü bir Kedi de bir Hayvan'dır. Çağrı nesne bir Kedi olduğu için Kedi.yavrula()'ya gider; println yeni kedinin toString()'ini çağırır ve “Kedi” yazar.
Kovaryans yalnız referans tiplerinde geçerlidir. int döndüren bir metodu long döndürecek biçimde geçersiz kılmak olmaz; ilkel tipler arasında alt tip ilişkisi yoktur. İlgisiz bir referans tipi de olmaz:
error: yavrula() in Kedi cannot override
yavrula() in Hayvan
return type String is not compatible with HayvanString, Hayvan'ın alt tipi değildir. Hayvan değişkeniyle yavrula()'yı çağıran kod bir hayvan bekler, eline bir dizgi geçer. Derleyici buna izin vermez.
4.3.4.6private ve static metotlar
private bir metot alt sınıfa görünmez. Alt sınıfta aynı adla yazılan bir metot onu geçersiz kılmaz; onunla ilgisiz, yeni bir metottur. Bu bazen beklenmedik sonuçlar doğurur:
class Ust {
private void selam() { System.out.println("Ust.selam"); }
void cagir() { selam(); }
}
class Alt extends Ust {
void selam() { System.out.println("Alt.selam"); } // yeni
}
// main içinde:
Alt a = new Alt();
a.cagir();
a.selam();Ust.selam
Alt.selama.selam() beklendiği gibi Alt'ın metodunu çağırır. Şaşırtıcı olan a.cagir()'ın sonucudur. cagir(), Ust'ta yazılmıştır ve Alt onu kalıtımla alır. İçindeki selam() çağrısı derleme sırasında Ust'un kendi private metoduna bağlanır. private metotlar geçersiz kılınamadığı için derleyici bu çağrıyı çalışma zamanına bırakmaz. Nesne bir Alt olsa da “Ust.selam” yazılır.
Ust'taki selam() private olmasaydı durum tersine dönerdi:
class Ust {
void selam() { System.out.println("Ust.selam"); }
void cagir() { selam(); }
}
class Alt extends Ust {
@Override void selam() { System.out.println("Alt.selam"); }
}
// main içinde:
new Alt().cagir();Alt.selamBurada Alt.selam() gerçekten geçersiz kılar. cagir()'daki çağrı nesnenin gerçek sınıfına göre seçilir ve Alt'ın sürümü çalışır. İki program arasındaki tek fark private sözcüğüdür, ama davranış tamamen değişir.
Programcı Alt.selam()'ın geçersiz kıldığını sanıyorsa yanılgıyı @Override alışkanlığı ortaya çıkarır. private örnekteki Alt.selam()'a @Override eklenirse derleyici method does not override or implement a method from a supertype iletisini verir. Etiket, “geçersiz kıldığımı sanıyorum, denetle” demektir; derleyici de denetimin sonucunu bildirir.
Aynı durum static metotlarda da görülür. Alt sınıftaki aynı imzalı static metot üst sınıfınkini geçersiz kılmaz, yalnız gizler (hiding). Hangisinin çalışacağına nesne değil, değişkenin derleme anındaki tipi karar verir:
class Ust {
static void f() { System.out.println("Ust.f"); }
void g() { System.out.println("Ust.g"); }
}
class Alt extends Ust {
static void f() { System.out.println("Alt.f"); }
@Override void g() { System.out.println("Alt.g"); }
}
// main içinde:
Ust u = new Alt();
u.f();
u.g();Ust.f
Alt.gu bir Alt nesnesini gösterir, ama tipi Ust'tur. static metot f() sınıfa aittir. Derleyici u.f()'yi Ust.f() olarak okur ve nesneye hiç bakmaz. Örnek metodu g() ise geçersiz kılınmıştır; nesnenin gerçek sınıfı seçimi yapar. static metodu bir nesne değişkeni üzerinden çağırmak bu yüzden yanıltıcıdır. Doğru yazım Ust.f() ya da Alt.f()'dir. Bir örnek metodu static metotla, ya da tersine, aynı imzayla yazmak ise derleme hatasıdır:
error: f() in Alt cannot override f() in Ust
overriding method is static
error: f() in Alt cannot override f() in Ust
overridden method is static4.3.4.7Örnek: ürün hiyerarşisi
Geçersiz kılmanın gücü, üst sınıfın kendi içinden yaptığı çağrılarda ortaya çıkar. Bir mağaza programında ürünlerin satış fiyatı, KDV (katma değer vergisi) eklenerek hesaplansın. Gıda ürünlerinde KDV oranı farklıdır; bazı ürünlerde de indirim vardır.
class Urun {
private String ad;
private double fiyat;
Urun(String ad, double fiyat) {
this.ad = ad; this.fiyat = fiyat;
}
double kdvOrani() { return 0.20; }
double satisFiyati() { return fiyat * (1 + kdvOrani()); }
@Override public String toString() {
double f = Math.round(satisFiyati() * 100) / 100.0;
return ad + ": " + f + " TL";
}
}
class GidaUrunu extends Urun {
GidaUrunu(String ad, double fiyat) { super(ad, fiyat); }
@Override double kdvOrani() { return 0.01; }
}
class IndirimliUrun extends Urun {
private double indirim;
IndirimliUrun(String ad, double fiyat, double indirim) {
super(ad, fiyat);
this.indirim = indirim;
}
@Override double satisFiyati() {
return super.satisFiyati() * (1 - indirim);
}
@Override public String toString() {
return super.toString()
+ " (%" + (int) (indirim * 100) + " indirim)";
}
}Urun sınıfı fiyat hesabını iki adıma böler. kdvOrani() oranı verir, satisFiyati() bu oranı kullanarak hesap yapar. toString() ise satış fiyatını iki ondalığa yuvarlar ve adla birlikte yazar. Yuvarlamada Math.round(x * 100) / 100.0 kalıbı kullanılır. Bu kalıp, kayan nokta hesabının doğurabileceği uzun kuyrukları temizler; örneğin 0.1 + 0.2 işleminin sonucu 0.30000000000000004'tür.
İki alt sınıf iki farklı yol izler. GidaUrunu yalnız kdvOrani()'nı değiştirir. IndirimliUrun ise satisFiyati()'nı geçersiz kılar, ama üst sınıfın hesabını değiştirmez; onu genişletir. super.satisFiyati() sonucuna indirimi uygular. toString()'i de genişletir ve sonuna indirim oranını ekler.
Urun[] sepet = {new Urun("Kalem", 50),
new GidaUrunu("Ekmek", 10),
new IndirimliUrun("Kulaklık", 1000, 0.25)};
for (Urun u : sepet) System.out.println(u);Kalem: 60.0 TL
Ekmek: 10.1 TL
Kulaklık: 900.0 TL (%25 indirim)| Ürün | kdvOrani() | satisFiyati() | Hesap | Sonuç |
|---|---|---|---|---|
Kalem (Urun) | Urun: 0.20 | Urun | 50 × 1,20 | 60.0 |
Ekmek (GidaUrunu) | GidaUrunu: 0.01 | Urun | 10 × 1,01 | 10.1 |
Kulaklık (IndirimliUrun) | Urun: 0.20 | IndirimliUrun → super | 1000 × 1,20 × 0,75 | 900.0 |
Tablo 4.3.7'nin ikinci satırı örneğin en öğretici kısmıdır. GidaUrunu'nda satisFiyati() ve toString() yazılmamıştır. İkisi de Urun'dan gelir. Ama satisFiyati()'nın içindeki kdvOrani() çağrısı this.kdvOrani() demektir ve this bir GidaUrunu'dur. Çağrı nesnenin sınıfına göre seçilir; %1 oran kullanılır. Üst sınıfın kodu, alt sınıfın yazdığı bir metodu çağırmış olur. Üst sınıf yazılırken GidaUrunu belki henüz yoktu.
Üçüncü satırda zincir iki düzeylidir. toString() önce IndirimliUrun'unkidir ve super.toString() ile Urun'unkini çağırır. Urun.toString(), satisFiyati()'nı çağırır; nesne bir IndirimliUrun olduğu için IndirimliUrun.satisFiyati() çalışır. O da super.satisFiyati() ile Urun'un hesabını yaptırır: 1000 × 1,20 = 1200. Sonra indirimi uygular: 1200 × 0,75 = 900. İndirim oranı (int) (indirim * 100) ile tam sayıya çevrilir ve “%25” yazılır.
Fiyatı 200 TL olan üç ürün düşünülsün: düz bir Urun, bir GidaUrunu ve %10 indirimli bir IndirimliUrun. Satış fiyatları nedir?
Urun: 200 × 1,20 = 240.0. GidaUrunu: kdvOrani() 0.01 döner, 200 × 1,01 = 202.0. IndirimliUrun: önce super.satisFiyati() = 240, sonra 240 × 0,90 = 216.0. İndirim KDV'li fiyata uygulanır. KDV'siz fiyata uygulansaydı sonuç değişmezdi, çünkü çarpma sırası sonucu etkilemez: 200 × 0,90 × 1,20 de 216'dır.
Bu tasarım, üst sınıfın yeni ürün türlerine kapalı olmadan genişletilebildiğini gösterir. Lüks tüketim vergisi olan bir ürün eklenecekse yeni bir alt sınıf kdvOrani()'nı ya da satisFiyati()'nı geçersiz kılar. Urun'a ve sepeti dolaşan döngüye dokunmak gerekmez.
4.3.5Erişim ve sınırlama
Kalıtım, üst sınıfın içini alt sınıflara bir ölçüde açar. Bu açıklığın ne kadar olacağı tasarımın bir parçasıdır. Bu kesim önce alt sınıflara özel bir erişim düzeyi olan protected'ı anlatır. Ardından kalıtımı tamamen kapatan final'ı ve nesnenin görünümünü üst sınıf tipiyle daraltmayı işler.
4.3.5.1protected erişim seviyesi
Java'da dört erişim düzeyi vardır. public üyelere her yerden erişilir. private üyelere yalnız o sınıfın içinden erişilir. Belirleyici yazılmayan üyeler paket erişimlidir; aynı paketteki bütün sınıflar onları görür. protected ise paket erişimi ile public arasında bir düzeydir.
| Belirleyici | Aynı sınıf | Aynı paket | Alt sınıf (başka paket) | Her yer |
|---|---|---|---|---|
private | ✓ | ✗ | ✗ | ✗ |
| (belirleyici yok: paket) | ✓ | ✓ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ |
public | ✓ | ✓ | ✓ | ✓ |
protected bir üyeye sınıfın kendisi, aynı paketteki bütün sınıflar ve başka paketlerdeki alt sınıflar erişebilir.
Tablo 4.3.8 her satırda bir öncekinden daha geniş bir kapsam gösterir. protected sözcüğü “korumalı” demektir, ama tablodan görüldüğü gibi paket erişiminden daha açıktır. Bu sık karıştırılır. protected, paket erişiminin üstüne bir izin daha ekler: başka paketteki alt sınıflara açılır.
Bir sınıfın private alanlarına alt sınıf yalnız üst sınıftan kalıtımla aldığı public ya da protected metotlar aracılığıyla erişebilir. İyi uygulama şudur: alanlar yine private kalır; alt sınıfların ihtiyaç duyduğu işlemler protected metotlar olarak açılır. Alanı protected yapmak kolay görünür, ama alanın değerini denetleme sorumluluğu artık bütün alt sınıflara dağılır. Bir alt sınıf sağlığı −50 yapabilir ve Hayvan'ın bunu önleme şansı kalmaz.
4.3.5.2protected: kod örneği
Aşağıdaki iki sınıf farklı paketlerdedir. Hayvan canli paketinde, Kedi evcil paketindedir.
package canli;
public class Hayvan {
protected String isim; // alt sınıflar
private double saglik; // yalnızca Hayvan
public Hayvan(String isim) {
this.isim = isim;
saglik = 100.0;
}
public double getSaglik() { return saglik; }
protected void saglikEkle(double miktar) {
saglik += miktar;
}
}package evcil; // başka paket
import canli.Hayvan;
public class Kedi extends Hayvan {
public Kedi(String isim) { super(isim); }
public void uyu() {
// protected alan: izin var
System.out.println(isim + " uyuyor.");
// saglik += 5; // private: hata
saglikEkle(5); // protected metot
System.out.println("Sağlık: "
+ getSaglik());
}
}Hayvan'ın iki alanı iki farklı tasarım seçimini gösterir. isim protected'tır; alt sınıflar onu doğrudan okuyabilir. saglik ise private'tır. Ona yalnız iki yoldan ulaşılır: herkese açık getSaglik() okur, yalnız alt sınıflara açık saglikEkle(double) değiştirir.
Kedi'nin uyu() metodu bu iki yolu kullanır. isim'i doğrudan yazar; Kedi başka pakette olsa da Hayvan'ın alt sınıfıdır. Sağlığı saglikEkle(5) ile artırır ve getSaglik() ile okur. new Kedi("Mavi").uyu(); şunu yazar:
Mavi uyuyor.
Sağlık: 105.0Yorum satırındaki saglik += 5; açılırsa derleme durur:
error: saglik has private access in HayvanBu tasarımın yararı somuttur. Hayvan'ın sahibi ileride sağlığın 100'ü aşmamasını isteyebilir. Bu kuralı yalnız saglikEkle'ye yazar; bütün alt sınıflar kendiliğinden uyar. Alan protected olsaydı her alt sınıfın kodunu tek tek denetlemek gerekirdi.
Alt sınıf olmayan bir sınıf ise başka paketten protected üyeye erişemez:
package diger;
import canli.Hayvan;
public class Bakici { // alt sınıf değil
String ad(Hayvan h) { return h.isim; }
}diger/Bakici.java:4: error: isim has protected access in HayvanBakici ne canli paketindedir ne de Hayvan'ın alt sınıfıdır. Tablodaki dört sütunun hiçbiri ona izin vermez. Bakici canli paketine taşınsaydı derlenirdi: aynı paketteki sınıflar protected üyelere erişir.
4.3.5.3protected erişimin ince kuralı
protected'ın bir kuralı tabloya sığmaz. Başka paketteki alt sınıf, protected üyeye yalnız kendi tipinden (ya da kendi alt tiplerinden) bir referans üzerinden erişebilir:
package canli;
public class Hayvan {
protected String isim;
public Hayvan(String isim) {
this.isim = isim;
}
}package evcil;
import canli.Hayvan;
public class Kedi extends Hayvan {
public Kedi(String isim) { super(isim); }
void karsilastir(Kedi k, Hayvan h) {
System.out.println(isim); // kendi alanı
System.out.println(k.isim); // Kedi üzerinden
System.out.println(h.isim); // Hayvan üzerinden
}
}evcil/Kedi.java:8: error: isim has protected access in Hayvan
System.out.println(h.isim);karsilastir metodundaki üç satır üç farklı referans kullanır. isim tek başına yazıldığında this.isim demektir; this bir Kedi'dir ve erişim serbesttir. k.isim'de k'nin tipi Kedi'dir; bu da serbesttir. h.isim'de ise h'nin tipi Hayvan'dır ve derleme durur.
Kuralın gerekçesi h'nin neyi gösterebileceğindedir. h herhangi bir Hayvan olabilir, örneğin bir Kopek. protected'ın verdiği izin “alt sınıf, kendi kalıtımla aldığı kısma dokunabilir” iznidir. Bir kedinin bir köpeğin iç verisine dokunması bu iznin dışındadır. Derleyici bu yüzden Hayvan tipli referansa izin vermez. Aynı nedenle Kopek tipli bir parametre de geçmez: Kedi içinde k.isim, k bir Kopek ise yine isim has protected access in Hayvan iletisini doğurur.
Aynı paketteki sınıflar için bu kısıt yoktur. Orada protected, paket erişimini de kapsar. canli paketindeki bir Veteriner sınıfı, alt sınıf olmasa da herhangi bir Hayvan'ın isim'ini okuyabilir ve saglikEkle'yi çağırabilir.
4.3.5.4final ile kalıtımı sınırlamak
final anahtar kelimesi kalıtımı sınırlamak için kullanılır. Bir metot final tanımlanırsa hiçbir alt sınıfta geçersiz kılınamaz. Bir sınıf final tanımlanırsa ondan hiçbir alt sınıf türetilemez.
class Hayvan {
public final void nefesAl() { ... }
}
class Kedi extends Hayvan {
public void nefesAl() { ... }
}error: nefesAl() in Kedi cannot override
nefesAl() in Hayvan
overridden method is finalfinal class Sabit { ... }
class Deneme extends Sabit { }error: cannot inherit from final Sabitfinal sınıftan alt sınıf türetilemez; final metot alt sınıfta geçersiz kılınamaz.Java kütüphanesinde pek çok final sınıf vardır: String, Math ve Integer bunlardandır. class BenimDizgim extends String { } yazmak cannot inherit from final String hatasını verir. Nedeni güvenlik ve tutarlılıktır. String nesneleri değiştirilemez olmalıdır. Bir metot bir String aldığında o metnin sonradan değişmeyeceğine güvenir. Örneğin dosya adları, ağ adresleri ve HashMap anahtarları bu güvene dayanır. String'in değiştirilebilen bir alt sınıfı olsaydı bu söz bozulurdu. final bu kapıyı kapatır.
final metot da benzer bir gerekçeyle kullanılır. Hayvan'ın nefesAl() metodu sağlık hesabının doğruluğu için kritikse, hiçbir alt sınıfın onu değiştirmemesi istenir. Yapıcıdan çağrılan metotları final yapmak da, alt sınıfın henüz ilk değer almamış alanlarını okuyan bir geçersiz kılmayı önler.
Değişkenlerde final başka bir anlam taşır: değişkene değer bir kez atanır ve değişken sabit olur. Aynı sözcük üç yerde üç anlam taşır. Ortak noktaları “bundan sonra değişmez”dir: değişkenin değeri, metodun gövdesi, sınıfın hiyerarşideki yeri.
4.3.5.5Nesnenin görünümünü kısıtlama
Bir alt sınıf nesnesi, üst sınıf tipinde bir değişkene atanabilir. Bu atamaya yukarı dönüşüm (upcasting) denir. Atama kendiliğindendir, dönüşüm işleci gerekmez, çünkü her Lastik bir Parca'dır.
Parca p = new Lastik("Petlas", "XYZ-123", "225/45R17", true);
System.out.println(p.seriNumarasi()); // izin verilir → XYZ-123
// p.kislikMi() // izin verilmezParca tipindeki p referansı bir Lastik nesnesini gösterir. p üzerinden nesnenin yalnız Parca kısmı görünür; ebat, kislik ve kislikMi() gizli kalır.Şekil 4.3.14 önemli bir ayrımı gösterir. Nesnenin kendisi değişmemiştir; hâlâ bütün alanlarıyla bir Lastik'tir. Değişen yalnız ona bakan pencerenin genişliğidir. Derleyici hangi metotların çağrılabileceğine değişkenin tipine bakarak karar verir. p'nin tipi Parca'dır; Parca'da seriNumarasi() vardır, kislikMi() yoktur:
error: cannot find symbol
symbol: method kislikMi()
location: variable p of type ParcaBu kısıtlama bir tasarım aracıdır. Bir metot parametre olarak Parca alırsa her türlü parçayla çalışır ve yalnız parçalara özgü işlemleri kullanabilir. Böylece dışarıya nesnenin yalnız gereken yüzü açılır. Stok sayan bir metot, lastiğin kışlık olup olmadığını bilmek zorunda kalmaz.
Lastik'e özgü bir metoda ulaşmak gerekirse aşağı dönüşüm (downcasting) yapılır. Aşağı dönüşüm açıkça yazılır ve çalışma anında denetlenir. Bu yüzden önce instanceof ile tip kontrolü yapılır:
Parca p = new Lastik("Petlas", "XYZ-123", "225/45R17", true);
if (p instanceof Lastik) {
Lastik l = (Lastik) p; // güvenli
System.out.println(l.kislikMi());
}
Parca q = new Parca("Bosch", "AB-9");
Lastik l2 = (Lastik) q; // q bir Lastik değiltrue
Exception in thread "main" java.lang.ClassCastException: class Parca cannot be cast to class Lastikp instanceof Lastik doğru olduğu için (Lastik) p güvenlidir ve true yazılır. q ise düz bir Parca'dır. Derleyici (Lastik) q'yu kabul eder, çünkü bir Parca değişkeni bir Lastik'i gösteriyor olabilir. Çalışma anında JVM nesneye bakar, onun bir Lastik olmadığını görür ve ClassCastException fırlatır. Aşağı dönüşüm yeni bir nesne üretmez. Aynı nesneye daha geniş bir pencereden bakmayı sağlar; nesne o pencereye uymuyorsa istisna oluşur.
Kontrolsüz aşağı dönüşüm: (Lastik) p yazmak, p bir Lastik değilse ClassCastException doğurur. Dönüşümden önce if (p instanceof Lastik) denetimi yapılır. Java 16'dan beri bu ikisi tek adımda yazılabilir: if (p instanceof Lastik l) { … l.kislikMi() … }. Burada l, denetim doğruysa dönüştürülmüş referansı tutar.
4.3.6Sınıflar arası ilişkiler
Kalıtım, iki sınıfı ilişkilendirmenin tek yolu değildir. Bir sınıf başka bir sınıfın nesnesini alan olarak da tutabilir. Bu kesim iki ilişkiyi karşılaştırır, Java'nın çoklu kalıtıma neden izin vermediğini açıklar ve kalıtımın yanlış kullanıldığında doğurduğu iki sorunu kod izlemeleriyle gösterir.
4.3.6.1is-a ve has-a ilişkileri
is-a (“bir türüdür”) ilişkisi kalıtımdır: bir sınıf başka bir sınıftan türetilir ve onun bir türü olur. Alt sınıf nesnesi üst sınıfın nesnesi gibi de kullanılabilir.
has-a (“sahiptir”) ilişkisi ise oluşumdur (composition): bir sınıf, başka bir sınıf tipinde bir alan içerir. Nesne, içinde başka nesnelere referanslar tutar.
İki ilişkiyi ayırmanın en kolay yolu cümle kurmaktır. “Araba bir taşıttır” doğru bir cümledir; Araba extends Arac yazılır. “Araba bir motordur” yanlıştır. Doğru cümle “arabanın bir motoru vardır”dır; motor arabanın bir alanı olur. Gerçek sınıflar çoğu zaman iki ilişkiyi birlikte taşır:
// Is-a ilişkisi (kalıtım)
public class Araba extends Arac {
// Has-a ilişkisi (oluşum)
private Motor motor;
public Araba(int beygirGucu) {
motor = new Motor(beygirGucu);
}
public void yolaCik() {
motor.calistir(); // Motor'a devret
hizlan(50); // Arac'tan miras
System.out.println("Hız: " + hiz
+ " km/sa");
}
}
// Motor: beygirGucu alanı ve calistir():
// "Motor çalıştı: " + beygirGucu + " BG"Araba, Arac'tan türer (is-a). Araba bir Motor nesnesi içerir (has-a). UML'de oluşum, içeren sınıf tarafında dolu bir eşkenar dörtgenle çizilir.Arac sınıfında bir hiz alanı ve hizlan(int) metodu vardır; hiz alt sınıfların erişebilmesi için protected'tır. Motor sınıfı beygir gücünü tutar ve calistir() metodu “Motor çalıştı: … BG” yazar. Araba yapıcısında kendi motorunu oluşturur. yolaCik() metodu iki ilişkiyi birlikte kullanır: motoru çalıştırma işini Motor nesnesine devreder, hızlanmayı Arac'tan kalıtımla aldığı metotla yapar. new Araba(110).yolaCik(); şunu yazar:
Motor çalıştı: 110 BG
Hız: 50 km/sa4.3.6.2Kod izleme: araba ve motor
İki ilişkinin bellekteki farkı bir izlemeyle görülür. Sunumdaki izleme, aynı sınıfların kısaltılmış bir sürümünü kullanır:
class Motor {
private int bg;
Motor(int bg) { this.bg = bg; }
void calistir() {
System.out.println("Motor: " + bg + " BG");
}
}
class Araba extends Arac { // is-a
private Motor motor; // has-a
Araba(int bg) { motor = new Motor(bg); }
void yolaCik() {
motor.calistir(); hizlan(50);
System.out.println("Hız: " + hiz);
}
}
new Araba(90).yolaCik(); // mainMotor: 90 BG
Hız: 50new Araba(90) tek bir nesne oluşturur; Arac'tan gelen hiz bu nesnenin içindedir. (b) Yapıcı ikinci bir nesne, bir Motor oluşturur; motor alanı onu gösterir. (c) motor.calistir(): iş referans izlenerek Motor'a devredilir. (d) hizlan(50) aynı Araba nesnesinin hiz alanını 50 yapar.Şekil 4.3.16 iki ilişkinin bellekteki karşılığını açıkça ayırır. Kalıtım tek bir nesne verir. hiz alanı ayrı bir Arac nesnesinde değil, Araba nesnesinin kendisindedir. Oluşum ise iki ayrı nesne verir. Araba nesnesindeki motor alanı, heap'teki başka bir nesneyi gösteren bir referanstır. Araba bir motor değildir, motoru vardır; bağlantı bir referanstır.
Çağrıların akışı da farklıdır. motor.calistir() çağrısında Araba işi kendisi yapmaz; referansı izleyip Motor nesnesine devreder. Yığına calistir() çerçevesi eklenir ve bu çerçevedeki this, Motor nesnesini gösterir. hizlan(50) çağrısında ise this değişmez; metot Arac'ta yazılmıştır, ama aynı Araba nesnesi üzerinde çalışır.
4.3.6.3Çoklu kalıtım
Java'da bir sınıf yalnız bir sınıftan türeyebilir; doğrudan çoklu kalıtım desteklenmez:
class D extends B, C { } // geçersizerror: '{' expected
class D extends B, C { }
^İleti biraz garip görünür. Derleyici “çoklu kalıtım yasaktır” demez, sözdizimi hatası verir. extends'ten sonra tek bir sınıf adı bekler ve adın hemen ardından sınıf gövdesinin {'ini arar. Virgülü görünce durur.
D hem B'den hem C'den türeseydi, ikisi de A'nın yaz() metodunu farklı biçimde geçersiz kıldığında D'nin hangisini alacağı belirsiz olurdu.Yasağın gerekçesi elmas problemidir (diamond problem). Şekil 4.3.17 durumu gösterir: A'dan B ve C türer, ikisi de yaz()'ı farklı geçersiz kılar. D ikisinden birden türeyebilseydi d.yaz() hangi sürümü çalıştırırdı? Diyagram bir elmasa benzediği için sorun bu adı alır. Sorunun ikinci bir yüzü de vardır: A'nın alanları D nesnesinde bir kez mi, iki kez mi bulunmalı? C++ gibi çoklu kalıtıma izin veren diller bu sorulara karmaşık kurallarla cevap verir. Java'nın tasarımcıları sınıflarda çoklu kalıtımı hiç almamayı seçmiştir.
Birden fazla sınıfın yetenekleri gerekiyorsa iki yol vardır. Birincisi oluşumdur: sınıf, yeteneklerini almak istediği nesneleri alan olarak tutar. İkincisi arayüzlerdir (interface). Bir sınıf tek bir sınıfı genişletirken birçok arayüzü uygulayabilir:
interface Sarj { void sarjEt(); }
class ElektrikliAraba extends Araba
implements Sarj {
@Override
public void sarjEt() {
System.out.println("Şarj ediliyor.");
}
}ElektrikliAraba hem bir Araba'dır hem de şarj edilebilir bir şeydir. Arayüz yalnız bir söz verir: sarjEt() diye bir metot olacaktır. Gövdeyi sınıf yazar. Klasik arayüzlerde alan olmadığı için elmas probleminin alan yüzü ortaya çıkmaz.
4.3.6.4Tasarım ilkeleri: kalıtım mı, oluşum mu?
Kalıtımla tasarımda dört ilke yol gösterir.
- is-a testi. “Her X bir Y midir?” cümlesi doğru değilse kalıtım kullanılmaz. Araba bir motor değildir, motora sahiptir; doğru ilişki oluşumdur.
- Her sınıf kendi verisini yönetir. Üst sınıfın alanlarına
super(...)ve üst sınıfın metotlarıyla değer verilir; alanlarprivatetutulur. - Genel metotlar geçersiz kılınır.
toStringveequals, onunla birlikte dehashCode, her anlamlı sınıfta geçersiz kılınmalıdır. - Ortak kavramlar soyut sınıfa. “Hayvan” tek başına bir nesne olmamalıysa, yani hiç kimse belirsiz bir “hayvan” oluşturmamalıysa, alt sınıfların ortak kavramı soyut (abstract) bir sınıfla temsil edilir.
Kalıtım ile oluşum arasındaki seçim ilk ilkenin uygulanmasıdır. Tablo 4.3.9 iki yolu birkaç ölçüte göre karşılaştırır.
Kalıtım (extends) | Oluşum (alan olarak nesne) | |
|---|---|---|
| İlişki | is-a: “her X bir Y'dir” | has-a: “X'in bir Y'si vardır” |
| Bağlılık | Sıkı: alt sınıf, üst sınıfın iç işleyişine de bağlıdır | Gevşek: yalnız içteki nesnenin public metotlarına bağlıdır |
| Yeniden kullanım | Otomatik: bütün public metotlar gelir (istenmeyenler de) | Seçerek: yalnız iletilmek istenen metotlar yazılır |
| Değişiklik | Üst sınıf derleme zamanında sabittir | Alan, çalışma zamanında başka bir nesneyle değiştirilebilir |
| Üst tip yerine kullanım | Doğrudan: Hayvan h = new Kedi() | Ortak bir arayüz uygulanırsa |
Kalıtım, is-a ilişkisi gerçekten doğruysa ve üst sınıf genişletilmek üzere tasarlanıp belgelendiyse uygundur. Kedi ile Hayvan bu duruma uyar. Oluşum, amaç yalnız başka bir sınıfın kodunu kullanmaksa uygundur. Bir yığın (stack) sınıfı örnek verilebilir. Yigin, öğelerini tutmak için bir ArrayList'ten yararlanabilir. Ama bir yığın bir liste değildir. Yigin extends ArrayList yazılırsa yığın, listenin bütün public metotlarını da alır. Biri add(0, x) ile yığının ortasına eleman ekleyebilir. Bu, yığının “yalnız tepeden ekle ve çıkar” kuralını bozar. Doğrusu, listeyi bir private alanda tutmak ve dışarıya yalnız ekle ile cikar açmaktır.
Değişiklik satırı da önemli bir fark söyler. Araba'nın motoru bir alan olduğu için çalışma sırasında değiştirilebilir. Benzinli motor sökülür, yerine elektrikli motor takılır; ikisi de ortak bir Motor arayüzünü uyguluyorsa Araba'nın kodu değişmez. Kalıtımda ise Araba'nın üst sınıfı kaynak kodda yazılıdır ve bir kez derlendikten sonra değişmez.
4.3.6.5Kod izleme: kırılgan üst sınıf
Kalıtımın sıkı bağlılığı bazen beklenmedik hatalar doğurur. Aşağıdaki örnekte bir liste sınıfı vardır. Amaç, ona eklenen öğe sayısını tutan bir alt sınıf yazmaktır.
class Liste {
private String[] ogeler = new String[10];
private int boyut;
void ekle(String s) { ogeler[boyut++] = s; }
void hepsiniEkle(String[] dizi) {
for (String s : dizi) ekle(s);
}
int boyut() { return boyut; }
}
class SayanListe extends Liste {
int eklemeSayisi = 0;
@Override void ekle(String s) {
eklemeSayisi++;
super.ekle(s);
}
@Override void hepsiniEkle(String[] dizi) {
eklemeSayisi += dizi.length;
super.hepsiniEkle(dizi);
}
}
SayanListe l = new SayanListe(); // main
l.hepsiniEkle(new String[] {"a", "b", "c"});
System.out.println(l.eklemeSayisi + " " + l.boyut());6 3Üç öğe eklendi ve listede gerçekten üç öğe var, ama sayaç 6 diyor. SayanListe'nin iki metodu da tek başına doğru görünür. ekle sayacı bir artırıp işi üst sınıfa bırakır. hepsiniEkle sayaca dizinin uzunluğunu ekleyip işi üst sınıfa bırakır. Hatanın kaynağını Tablo 4.3.10 adım adım gösterir.
| Adım | Çalışan metot | Olay | eklemeSayisi | boyut |
|---|---|---|---|---|
| 1 | SayanListe.hepsiniEkle | Nesne bir SayanListe; alt sınıfın sürümü çağrılır. | 0 | 0 |
| 2 | SayanListe.hepsiniEkle | eklemeSayisi += dizi.length | 3 | 0 |
| 3 | Liste.hepsiniEkle | super.hepsiniEkle(dizi); döngü ekle("a"), yani this.ekle("a") çağırır. | 3 | 0 |
| 4 | SayanListe.ekle | this bir SayanListe: geçersiz kılınmış ekle çalışır, sayaç artar. | 4 | 0 |
| 5 | Liste.ekle | super.ekle(s) öğeyi diziye yazar. | 4 | 1 |
| 6 | (döngü) | "b" ve "c" için 3–5 yinelenir. | 6 | 3 |
| 7 | main | println: yanlış sonuç | 6 | 3 |
hepsiniEkle'si sayaca 3 ekler. (b) Üst sınıfın döngüsündeki ekle(s), nesne bir SayanListe olduğu için alt sınıfa döner; aynı öğe ikinci kez sayılır. (c) super.ekle öğeyi yazar: boyut = 1. (d) Sonuç 6 3.Hata üçüncü adımda doğar. Liste.hepsiniEkle, işini her öğe için ekle(s) çağırarak yapar. Bu, Liste'nin bir gerçekleme ayrıntısıdır; sınıfın belgesinde yazmayabilir. Liste'nin kodundaki ekle(s) aslında this.ekle(s)'dir ve this bir SayanListe'dir. Çağrı geçersiz kılınmış sürüme gider. Her öğe bir kez hepsiniEkle'de, bir kez de ekle'de sayılır.
Kırılgan üst sınıf (fragile base class) sorunu, alt sınıfın doğruluğunun üst sınıfın iç işleyişine bağlı olmasıdır. Üst sınıfta, dışarıdan görünen davranışı değiştirmeyen bir düzenleme bile alt sınıfı bozabilir.
Sorunun kırılganlık yönü şudur. SayanListe'nin programcısı hatayı fark edip hepsiniEkle'nin geçersiz kılınmasını silebilir. Sayaç o zaman doğru çıkar, çünkü her öğe yalnız ekle'de sayılır. Ama bu düzeltme Liste'nin o günkü koduna dayanır. Liste'nin yeni bir sürümü hız için hepsiniEkle'yi diziyi doğrudan kopyalayacak biçimde yazarsa ekle hiç çağrılmaz ve sayaç 0'da kalır. İki durumda da Liste'nin kullanıcılarına sunduğu davranış aynıdır, ama SayanListe bozulur.
Yeni bir SayanListe üzerinde önce ekle("x"), sonra hepsiniEkle(new String[] {"y", "z"}) çağrılıyor. eklemeSayisi ve boyut() nedir?
ekle("x") doğrudan alt sınıfın ekle'sini çalıştırır: sayaç 1, boyut 1. hepsiniEkle önce sayaca 2 ekler: sayaç 3. Sonra üst sınıfın döngüsü iki kez ekle çağırır ve her biri alt sınıfa döner: sayaç 5, boyut 3. Sonuç 5 3'tür. Yalnız ekle kullanılsaydı sonuç doğru çıkardı; hata hepsiniEkle ile ekle'nin birlikte geçersiz kılınmasından doğar.
4.3.6.6Oluşumla çözüm: iletme
Kırılgan üst sınıf sorununun temiz çözümü kalıtımı bırakıp oluşuma geçmektir:
class SayanListe2 {
private final Liste liste = new Liste(); // has-a
private int eklemeSayisi = 0;
void ekle(String s) {
eklemeSayisi++;
liste.ekle(s); // iletir
}
void hepsiniEkle(String[] dizi) {
eklemeSayisi += dizi.length;
liste.hepsiniEkle(dizi); // iletir
}
int getEklemeSayisi() { return eklemeSayisi; }
int boyut() { return liste.boyut(); }
}SayanListe2 l2 = new SayanListe2();
l2.hepsiniEkle(new String[] {"a", "b", "c"});
System.out.println(l2.getEklemeSayisi()
+ " " + l2.boyut());3 3İletme (forwarding), oluşumda gelen bir çağrının içteki nesnenin metoduna aktarılmasıdır. Dıştaki sınıfın her metodu önce kendi işini yapar, sonra çağrıyı içteki nesneye iletir.
Yeni sınıf bir Liste'den türemez; bir Liste'yi içinde tutar. liste.hepsiniEkle(...) çağrısının içindeki ekle çağrıları, düz bir Liste nesnesi üzerinde çalışır. O nesnenin this'i bir Liste'dir; çağrı SayanListe2'ye geri dönmez. Sayaç doğrudur: 3. Liste'nin iç ayrıntısı değişse de sonuç değişmez. SayanListe2 yalnız Liste'nin public metotlarına bağlıdır.
Bu çözümün bir bedeli vardır. SayanListe2 bir Liste değildir; bir Liste beklenen yere verilemez. Gerekirse Liste ile SayanListe2 ortak bir arayüzü uygular ve kod o arayüz tipiyle yazılır. İletme yöntemi Java kütüphanesinde de yaygındır. Örneğin Collections.unmodifiableList(...) bir listeyi saran ve okuma çağrılarını ona ileten bir nesne döndürür.
4.3.6.7Yerine koyma ilkesi
is-a testi gündelik dilde yapıldığında yanıltıcı olabilir. Matematikte her kare bir dikdörtgendir. Bu cümleye dayanarak Kare extends Dikdortgen yazmak doğal görünür:
class Dikdortgen {
protected int en, boy;
void setEn(int en) { this.en = en; }
void setBoy(int boy) { this.boy = boy; }
int alan() { return en * boy; }
}
class Kare extends Dikdortgen {
@Override void setEn(int k) { en = k; boy = k; }
@Override void setBoy(int k) { en = k; boy = k; }
}
static void genislet(Dikdortgen d) {
d.setEn(5);
d.setBoy(4);
System.out.println("Beklenen 20, bulunan "
+ d.alan());
}
// main içinde:
genislet(new Dikdortgen());
genislet(new Kare());Beklenen 20, bulunan 20
Beklenen 20, bulunan 16Liskov yerine koyma ilkesi (Liskov substitution principle, LSP): üst sınıf tipiyle yazılmış kod, bir alt sınıfın nesnesiyle de doğru çalışmalıdır. Kodun derlenmesi yetmez; davranışın da doğru kalması gerekir.
genislet metodu bir dikdörtgenin enini 5, boyunu 4 yapar ve alanın 20 olmasını bekler. Düz bir Dikdortgen ile beklenti tutar. Bir Kare ile ise setEn(5) iki kenarı da 5 yapar, ardından setBoy(4) iki kenarı da 4 yapar. Alan 16 çıkar. Kod derlendi, hiçbir istisna oluşmadı, ama sonuç yanlış.
Hata nerededir? genislet hatalı değildir. Dikdortgen sınıfı “en ve boy birbirinden bağımsız değişir” sözünü verir: setEn boya dokunmaz. genislet bu söze güvenmiştir. Kare ise bu sözü tutamaz, çünkü karenin iki kenarı birbirinden bağımsız olamaz. Hata, sözü bozan alt sınıftadır. Matematiksel ilişki doğrudur, ama sınıflar arasındaki ilişki yanlıştır.
Bu sorunun iki yaygın çözümü vardır. Birincisi Kare'yi Dikdortgen'den türetmemektir; ikisi de ortak bir Sekil'den türer ve her biri kendi alan()'ını yazar. İkincisi sınıfları değiştirilemez yapmaktır. En ve boy yalnız yapıcıda verilir, setEn ve setBoy hiç olmaz. O zaman “en değişince boy değişmez” diye bir söz de olmaz ve kare gerçekten bir dikdörtgen gibi davranabilir.
Ders şudur: is-a testi gerçek dünyaya değil, sınıfın davranışına uygulanır. Doğru soru “kare bir dikdörtgen midir?” değil, “Kare, Dikdortgen'in her metoduna onun gibi cevap verebilir mi?” sorusudur.
Dikdortgen d = new Kare(); d.setEn(3); d.setBoy(7); satırlarından sonra d.alan() kaçtır?
Değişkenin tipi Dikdortgen, nesne bir Kare'dir. Çağrılar geçersiz kılınmış sürümlere gider. setEn(3): en = boy = 3. setBoy(7): en = boy = 7. Alan 7 × 7 = 49'dur. Son çağrı her iki kenarı belirler. Düz bir dikdörtgende sonuç 3 × 7 = 21 olurdu.
4.3.7Object sınıfı
Java'daki her sınıf, yazılsın ya da yazılmasın, Object'ten türer. Bu kesim Object'in her nesneye verdiği metotları ve bunların en önemli üçünü, toString, equals ve hashCode'u, doğru geçersiz kılmayı anlatır.
4.3.7.1Her sınıfın atası
Her sınıf java.lang.Object sınıfından türer. extends yazılmamışsa dolaylı olarak:
class Nokta { }
// derleyicinin gördüğü:
class Nokta extends Object { }Bu yüzden her nesnede bazı metotlar hazır bulunur. Tablo 4.3.11 en çok kullanılan dördünü verir. Bu metotların varsayılan sürümleri genel amaçlıdır ve çoğu zaman işe yaramaz. Anlamlı sınıflarda ilk üçü geçersiz kılınır.
| Metot | Varsayılan davranış |
|---|---|
String toString() | Sınıf adı + @ + hashCode değerinin onaltılık (hex) yazımı |
boolean equals(Object o) | Referansları karşılaştırır: this == o |
int hashCode() | Nesneye özgü bir tam sayı (genellikle her nesnede farklı) |
Class getClass() | Nesnenin çalışma zamanındaki sınıfı (final; geçersiz kılınamaz) |
Object'in başka metotları da vardır: iş parçacıklarında kullanılan wait, notify ve notifyAll, nesne kopyalamaya yarayan clone gibi. Bunlar bu notun konusu değildir. Object tipindeki bir değişken her nesneyi gösterebilir. Bu yüzden equals'ın parametresi Object'tir: bir nesne, herhangi bir başka nesneyle karşılaştırılabilmelidir.
4.3.7.2toString metodu
public String toString() nesneyi metin olarak gösterir. System.out.println(nesne) ve "..." + nesne biçimindeki birleştirme bu metodu kendiliğinden çağırır. Geçersiz kılınmazsa sınıf adını ve nesnenin hashCode değerini içeren bir metin döner:
class Nokta {
private int x, y;
Nokta(int x, int y) {
this.x = x; this.y = y;
}
}
// main içinde
System.out.println(new Nokta(3, 4));Nokta@28a418fc@ işaretinden sonraki 28a418fc, hashCode() değerinin onaltılık yazımıdır. Değer her çalıştırmada ya da her JVM'de farklı olabilir. Nesnenin içeriği hakkında hiçbir şey söylemez; hata ayıklarken bile işe yaramaz. Geçersiz kılınınca nesne okunaklı yazılır:
class Nokta {
private int x, y;
Nokta(int x, int y) {
this.x = x; this.y = y;
}
@Override
public String toString() {
return "Nokta[x=" + x + ", y=" + y + "]";
}
}
// main içinde
Nokta n = new Nokta(3, 4);
System.out.println(n);
System.out.println("Konum: " + n);Nokta[x=3, y=4]
Konum: Nokta[x=3, y=4]İkinci satırda + işlecinin bir yanı String olduğu için Java öbür yanı da metne çevirir ve bunun için toString()'i çağırır. toString()'in dönüş tipi String, erişimi public'tir. Geçersiz kılarken public yazmayı unutmak, Tablo 4.3.6'da görülen erişimi daraltma hatasını verir.
Bir ayrıntı daha vardır. null bir referans yazdırılırsa toString çağrılmaz; “null” yazılır:
Nokta n = null;
System.out.println(n);
System.out.println("Konum: " + n);null
Konum: nullprintln ve dizgi birleştirme null'ı özel olarak ele alır. Ama n.toString() açıkça çağrılsaydı NullPointerException oluşurdu; null referans üzerinde metot çağrılamaz.
4.3.7.3== mi, equals mı?
İki nesneyi karşılaştırmanın iki yolu vardır ve ikisi farklı sorular sorar. ==, “bu iki referans aynı nesneyi mi gösteriyor?” diye sorar. equals ise “bu iki nesne eşdeğer mi?” diye sorar. Aşağıdaki Kisi sınıfında equals, adı ve yaşı karşılaştıracak biçimde geçersiz kılınmış olsun:
Kisi k1 = new Kisi("Ali", 25);
Kisi k2 = new Kisi("Ali", 25);
Kisi k3 = k1;
System.out.println(k1 == k2);
System.out.println(k1 == k3);
System.out.println(k1.equals(k2));false
true
true== ile equals izlemesinden iki an. (a) k3 = k1 yalnız adresi kopyalar; k1 ve k3 aynı nesneyi gösterir. (b) k1 == k2: iki ayrı nesne, sonuç false.Satırları sırayla izleyelim. İlk iki new heap'te içeriği aynı iki ayrı nesne oluşturur; k1 ve k2 bunların adreslerini tutar. k3 = k1 yeni nesne oluşturmaz; yalnız adresi kopyalar. Artık k1 ve k3 aynı nesneyi gösterir (Şekil 4.3.19). k1 == k2 iki farklı adresi karşılaştırır ve false verir. k1 == k3 aynı adresi karşılaştırır ve true verir. k1.equals(k2) ise içerikleri karşılaştırır: iki nesnenin de adı "Ali", yaşı 25'tir, sonuç true'dur.
equals geçersiz kılınmamış olsaydı son satır false verirdi. Object'in equals'ı this == o'dan ibarettir. String karşılaştırmasında da kural aynıdır: s1 == s2 değil, s1.equals(s2) yazılır. new String("Java") == new String("Java") false, equals ise true verir.
4.3.7.4equals sözleşmesi
public boolean equals(Object other), iki nesnenin eşdeğer olup olmadığını söyler. Varsayılan olarak yalnız referansları karşılaştırır; geçersiz kılınmazsa == gibi davranır. Geçersiz kılınınca metot iki nesnenin durumunu, yani alanlarını karşılaştırır ve eşitse true döner.
== ve varsayılan equals adresleri karşılaştırır ve false verir. Geçersiz kılınmış equals alanları karşılaştırır ve true verir.equals keyfî yazılamaz. Java kütüphanesi, özellikle koleksiyonlar, equals'ın belirli kurallara uyduğunu varsayar. Bu kurallara equals sözleşmesi denir (Tablo 4.3.12).
| Özellik | Kural |
|---|---|
| Yansıma | x.equals(x) → true |
| Simetri | x.equals(y) ⇔ y.equals(x) |
| Geçişme | x = y ve y = z ise x = z |
| Tutarlılık | Alanlar değişmedikçe sonuç değişmez |
| null | x.equals(null) → false |
Bu kurallar matematikteki eşitliğin özellikleridir. Bozulduklarında hatalar dolaylı yoldan ortaya çıkar. Simetri bozuksa liste.contains(x) bir sırayla true, öbür sırayla false verebilir. Tutarlılık bozuksa bir HashSet'e eklenen nesne sonradan bulunamaz.
4.3.7.5instanceof ile equals
equals'ın parametresi Object tipindedir. Gelen nesne bir Nokta olmayabilir; bir dizgi, bir kişi ya da null olabilir. Bu yüzden alanlara geçmeden önce tip kontrolü yapılır:
public boolean equals(Object nesne) {
if (nesne instanceof Nokta) {
// nesne bir Nokta nesnesidir;
// dönüştürüp karşılaştır
Nokta diger = (Nokta) nesne;
return x == diger.x && y == diger.y;
} else {
// nesne bir Nokta değildir;
// eşit olamaz
return false;
}
}Metot önce nesne instanceof Nokta ile gelen nesnenin bir Nokta olup olmadığını sorar. Öyleyse (Nokta) nesne ile aşağı dönüşüm yapar. Dönüşüm yalnız tip kontrolünden sonra güvenlidir. Sonra iki koordinatı karşılaştırır. Nesne bir Nokta değilse eşit olamaz ve false döner.
Nokta a = new Nokta(3, 4);
Nokta b = new Nokta(3, 4);
System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(a.equals("3,4"));
System.out.println(a.equals(null));false
true
false
falseİlk satır iki ayrı nesneyi == ile karşılaştırır: false. İkincisi içerikleri karşılaştırır: true. Üçüncüsünde "3,4" bir Nokta değil, bir String'dir; instanceof yanlış verir ve false döner. Dönüşüme hiç gelinmediği için ClassCastException da oluşmaz. Dördüncüsünde null instanceof Nokta yanlıştır. instanceof, null için her zaman false verir. Bu yüzden instanceof kullanan bir equals'ta ayrı bir null kontrolüne gerek kalmaz.
Parametre tipi Object olmalıdır. equals(Nokta n) yazmak geçersiz kılma değil, aşırı yüklemedir. Bu hata derleyicinin gözünden kaçar ve sinsi sonuçlar doğurur:
class NoktaA {
int x, y;
NoktaA(int x, int y) { this.x = x; this.y = y; }
public boolean equals(NoktaA n) { // aşırı yükleme!
return x == n.x && y == n.y;
}
}
// main içinde:
NoktaA p = new NoktaA(1, 2), q = new NoktaA(1, 2);
Object qo = q;
System.out.println(p.equals(q) + " " + p.equals(qo));
List<NoktaA> liste = new ArrayList<>();
liste.add(p);
System.out.println(liste.contains(q));true false
falsep.equals(q) doğru sonucu verir, çünkü q'nun derleme anındaki tipi NoktaA'dır ve derleyici equals(NoktaA)'yı seçer. p.equals(qo)'da ise argümanın tipi Object'tir. Derleyici Object'ten gelen equals(Object)'i seçer, o da adresleri karşılaştırır: false. Kütüphane sınıfları her zaman equals(Object)'i çağırır. ArrayList.contains bu yüzden aynı içerikli noktayı bulamaz. @Override yazılsaydı hata derleme anında yakalanırdı: method does not override or implement a method from a supertype.
4.3.7.6getClass() ile equals ve izleme
Tip kontrolünün ikinci bir yolu getClass()'tır:
public class Kisi {
private String ad;
private int yas;
// ... yapıcı ...
@Override
public boolean equals(Object obj) {
// aynı referanssa eşittir
if (this == obj) return true;
// null ya da farklı sınıf: eşit değil
if (obj == null
|| getClass() != obj.getClass())
return false;
// alanları karşılaştır
Kisi kisi = (Kisi) obj;
return yas == kisi.yas
&& ad.equals(kisi.ad);
}
}Bu kalıp üç adımlıdır. Önce this == obj denetlenir. Aynı nesneyse alanlara bakmaya gerek yoktur ve hemen true döner; bu hızlı bir kestirmedir. Sonra obj == null ve sınıf denetimi yapılır. getClass() nesnenin gerçek sınıfını verir; iki tarafın sınıfı birebir aynı olmalıdır. Son olarak alanlar karşılaştırılır. İlkel tipler (int yas) == ile, nesne tipindeki alanlar (String ad) equals ile karşılaştırılır.
İki kalıp alt sınıf nesneleri için farklı davranır (Tablo 4.3.13).
| Tip kontrolü | Alt sınıf nesnesi eşit sayılabilir mi? |
|---|---|
instanceof | Evet (bir Kedi nesnesi bir Hayvan'dır) |
getClass() | Hayır (sınıflar birebir aynı olmalı) |
class Ogrenci extends Kisi { // Kisi'nin equals'ı getClass() kullanır
Ogrenci(String ad, int yas) { super(ad, yas); }
}
// main içinde:
System.out.println(new Kisi("Ali", 20).equals(new Ogrenci("Ali", 20)));falseKisi'nin equals'ı getClass() kullandığı için aynı ad ve yaştaki bir Ogrenci'yi eşit saymaz. instanceof kullanılsaydı true verirdi. Hangisinin doğru olduğu tasarıma bağlıdır. getClass() simetriyi kendiliğinden korur. instanceof ise alt sınıflar equals'ı yeniden yazmadıkça güvenlidir; alt sınıf yeni alanlarla equals'ı geçersiz kılarsa simetri bozulabilir.
Şimdi getClass()'lı equals'ı bir izlemeyle çalıştıralım:
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass())
return false;
Kisi kisi = (Kisi) obj;
return yas == kisi.yas && ad.equals(kisi.ad);
}
// main içinde:
Kisi k1 = new Kisi("Ali", 20);
Kisi k2 = new Kisi("Ali", 20);
Kisi k3 = new Kisi("Ali", 21);
System.out.println(k1.equals(k2));
System.out.println(k1.equals(k3));
System.out.println(k1.equals(null));true
false
false| Çağrı | this == obj | null / sınıf denetimi | yas == kisi.yas | ad.equals(...) | Sonuç |
|---|---|---|---|---|---|
k1.equals(k2) | #1 ≠ #2: yanlış | geçti (Kisi = Kisi) | 20 == 20: doğru | "Ali" = "Ali": doğru | true |
k1.equals(k3) | #1 ≠ #3: yanlış | geçti | 20 == 21: yanlış | çağrılmaz (kısa devre) | false |
k1.equals(null) | yanlış | obj == null doğru; getClass çağrılmaz | — | — | false |
equals izlemesinden dört an. (a) k1.equals(k2): yeni çerçevede this = #1, obj = #2. (b) Aşağı dönüşüm yeni nesne üretmez; kisi aynı #2'yi Kisi tipiyle gösterir. (c) Yaş ve ad eşit: true. (d) k1.equals(k3): yaşlar farklı, && kısa devre yapar.İzlemenin üç ayrıntısı önemlidir. Birincisi, obj'nin tipi Object'tir. obj.yas yazılamaz, çünkü Object'te yas diye bir alan yoktur. Aşağı dönüşüm, aynı nesneye Kisi penceresinden bakan kisi referansını verir (Şekil 4.3.21, b). İkincisi, && kısa devre yapar. Yaşlar farklıysa ad.equals hiç çağrılmaz. Ucuz karşılaştırmanın önce yazılması bu yüzden iyi bir alışkanlıktır. Üçüncüsü, || da kısa devre yapar. obj == null doğruysa sağ taraf değerlendirilmez. Sıra ters yazılsaydı (getClass() != obj.getClass() || obj == null) null argümanla obj.getClass() çalışır ve NullPointerException oluşurdu.
Maliyet açısından equals sabit sayıda alan karşılaştırır. Alanlardan biri String ise onun equals'ı dizginin uzunluğuyla orantılı iş yapar. this == obj kestirmesi, bir nesnenin kendisiyle karşılaştırıldığı sık durumda bu işi tamamen atlar.
4.3.7.7equals ve hashCode
equals'ı geçersiz kılmanın bir yükümlülüğü vardır. Kural şudur: equals geçersiz kılınıyorsa hashCode da geçersiz kılınmalıdır. Eşit nesnelerin hashCode değerleri aynı olmalıdır.
Hash kodu (hash code), hashCode() metodunun bir nesne için döndürdüğü tam sayıdır. Sözleşmesi: a.equals(b) doğruysa a.hashCode() == b.hashCode() olmalıdır. Tersi gerekmez; eşit olmayan iki nesnenin hash kodu aynı olabilir.
import java.util.Objects;
public class Nokta {
private int x, y;
public Nokta(int x, int y) {
this.x = x; this.y = y;
}
@Override public boolean equals(Object o) {
return (o instanceof Nokta n)
&& x == n.x && y == n.y;
}
@Override public int hashCode() {
return Objects.hash(x, y);
}
@Override public String toString() {
return "Nokta[x=" + x + ", y=" + y + "]";
}
}HashSet<Nokta> kume = new HashSet<>();
kume.add(new Nokta(3, 4));
kume.add(new Nokta(3, 4));
System.out.println(kume.size() + " "
+ kume.contains(new Nokta(3, 4)));1 trueBu Nokta sınıfı üç metodu da geçersiz kılar. equals'ta o instanceof Nokta n biçimi kullanılmıştır. Bu, Java 16'dan beri bulunan örüntü eşlemedir: denetim doğruysa n dönüştürülmüş referansı tutar. Klasik biçimi if (o instanceof Nokta) { Nokta n = (Nokta) o; … }'tır. hashCode ise java.util.Objects.hash(x, y) ile alanlardan bir değer üretir.
Neden iki metot birlikte gerekir? HashSet ve HashMap bir öğeyi ararken önce hashCode ile bir “kova” seçer, sonra yalnız o kovadaki öğelerle equals çağırır. hashCode farklıysa eşit nesneler farklı kovalara düşer ve hiç karşılaştırılmaz. Tablo 4.3.15 iki durumu karşılaştırır.
Nokta sınıfında | Çıktı | |
|---|---|---|
yalnız equals | 2 false | ✗ |
equals + hashCode | 1 true | ✓ |
Yalnız equals yazıldığında Object'in hashCode'u kullanılır ve iki ayrı nesneye pratikte farklı değerler verir. İkinci new Nokta(3, 4) başka bir kovaya düşer. Küme onu ilkiyle karşılaştırmaz ve ikinci kez ekler: boyut 2. contains da üçüncü bir kovaya bakar ve bulamaz: false. Derleme hatası yoktur; derleyici iki metodun uyumunu denetlemez. Hata yalnız çalışma anında, yanlış sonuçla görülür.
equals'ı geçersiz kılıp hashCode'u unutmak. Sınıf bir ArrayList'te sorunsuz çalışır, çünkü ArrayList.contains yalnız equals kullanır. Aynı nesneler bir HashSet'e ya da bir HashMap'in anahtarına konunca hatalar başlar: yinelenen öğeler, bulunamayan anahtarlar.
4.3.7.8Kod izleme: HashSet içinde
Kovaların nasıl seçildiğini somut sayılarla izlemek mümkündür. Aşağıdaki kod klasik equals biçimini kullanır:
public boolean equals(Object o) {
if (!(o instanceof Nokta)) return false;
Nokta n = (Nokta) o;
return x == n.x && y == n.y;
}
public int hashCode() {
return Objects.hash(x, y); // 31·(31 + x) + y
}
// main içinde:
HashSet<Nokta> kume = new HashSet<>();
kume.add(new Nokta(3, 4));
kume.add(new Nokta(4, 3));
kume.add(new Nokta(3, 4));
System.out.println(kume.size());
System.out.println(kume.contains(new Nokta(4, 3)));2
trueObjects.hash(x, y), iki alan için 31 · (31 + x) + y değerini hesaplar. Boş bir HashSet 16 kovayla başlar. Öğenin kovası, küçük hash değerleri için hash % 16'dır. (Kütüphane gerçekte hash değerinin üst bitlerini alt bitlerle karıştırır ve hash & 15 alır. 65 536'dan küçük pozitif değerlerde sonuç hash % 16 ile aynıdır.)
| İşlem | hash | kova | Kovada ne var? | Sonuç | size |
|---|---|---|---|---|---|
add(Nokta(3, 4)) | 31 · 34 + 4 = 1058 | 2 | boş | eklendi | 1 |
add(Nokta(4, 3)) | 31 · 35 + 3 = 1088 | 0 | boş | eklendi | 2 |
add(Nokta(3, 4)) (yeni nesne) | 1058 | 2 | (3, 4); equals doğru | eklenmedi, add false | 2 |
contains(Nokta(4, 3)) | 1088 | 0 | (4, 3); equals doğru | true | 2 |
HashSet izlemesinden dört an. (a) (3, 4): hash 1058, kova 2. (b) (4, 3): hash 1088, kova 0. (c) Yeni bir (3, 4) yine kova 2'ye düşer; equals doğru olduğu için eklenmez. (d) contains önce kovayı bulur, sonra equals sorar: true.Tablo 4.3.16 iki önemli noktayı gösterir. Birincisi, aynı sayıların farklı sırası farklı hash verir: (3, 4) için 1058, (4, 3) için 1088. Formüldeki 31 çarpanı bu yüzden vardır; düz toplama (x + y) iki noktayı aynı değere götürürdü. İkincisi, küme her işlemde yalnız bir kovaya bakar. 16 kovaya dağılmış öğeler arasında arama, bütün öğeleri tek tek gezmekten çok daha hızlıdır. İyi dağıtan bir hashCode ile add ve contains ortalama O(1) sürer.
Farklı nesneler aynı kovaya düşebilir; buna çakışma denir. (1, 2) noktasının hash'i 31 · 32 + 2 = 994'tür ve 994 % 16 = 2'dir. Bu nokta da (3, 4) ile aynı kovaya düşer. Küme bu durumda equals sorar, yanlış cevabını alır ve yeni noktayı aynı kovaya ikinci öğe olarak ekler. Hatta hash değerleri bile eşit olabilir: (2, 65) noktasının hash'i de (4, 3)'ünki gibi 1088'dir. Sözleşme buna izin verir; eşit hash, eşit nesne demek değildir. equals son sözü söyler.
Bir sınıfın hashCode'u her nesne için 42 döndürsün; equals doğru yazılmış olsun. Bir HashSet'e (1, 2), (1, 2) ve (2, 1) eklenince boyut kaç olur? Küme doğru çalışır mı?
Sözleşme bozulmaz: eşit nesnelerin hash'i aynıdır, çünkü bütün nesnelerinki aynıdır. Bütün öğeler aynı kovaya düşer. İkinci (1, 2) equals ile yakalanır ve eklenmez; (2, 1) eşit olmadığı için eklenir. Boyut 2'dir, contains doğru cevap verir. Ama her işlem o tek kovadaki bütün öğeleri gezmek zorundadır. Küme doğru çalışır, ama ortalama O(1) yerine O(n) sürer. Kötü bir hashCode doğruluğu değil, hızı bozar.
4.3.8Sık yapılan hatalar
Kalıtımla ilgili hatalar iki gruba ayrılır. Derleme hataları javac'ın iletileriyle hemen görünür. İletileri okumayı öğrenmek bu hataların çoğunu dakikalar içinde çözdürür. Mantık hataları ise derleyiciden geçer ve ancak program yanlış sonuç verince fark edilir. Aşağıdaki iki tablo, notun gövdesinde ayrı ayrı işlenen hataları bir arada verir. İletiler JDK 21'in javac ve java araçlarıyla alınmıştır.
| Hata | javac iletisi | Düzeltme |
|---|---|---|
Üst sınıfta parametresiz yapıcı yokken super(...) çağırmamak | constructor Hayvan in class Hayvan cannot be applied to given types | İlk satıra super(isim); |
super(...)'ı ilk satıra yazmamak; this(...) ile birlikte yazmak | call to super must be first statement in constructor | Tek bir this/super çağrısı, ilk satırda |
private alana alt sınıftan erişmek | saglik has private access in Hayvan | Getter ya da protected metot |
Başka paketten üst tip referansıyla protected üyeye erişmek | isim has protected access in Hayvan | Alt sınıfın kendi tipi üzerinden erişim |
| Geçersiz kılarken erişimi daraltmak | attempting to assign weaker access privileges; was public | Aynı ya da daha geniş erişim |
| Dönüş tipini değiştirmek | return type String is not compatible with Hayvan | Aynı tip ya da alt tipi |
| Kontrol edilen istisna eklemek | overridden method does not throw Exception | İstisnayı metot içinde yakalamak |
Yanlış imzada @Override: Ciz(), equals(Nokta), private metot | method does not override or implement a method from a supertype | Ad ve parametre listesi birebir aynı |
final metodu geçersiz kılmak | overridden method is final | Başka adla yeni metot |
final sınıftan türetmek | cannot inherit from final Sabit | Oluşum |
| İki sınıftan türetmek | '{' expected | Oluşum ya da arayüz |
| Üst tip değişkeniyle alt sınıf metodu çağırmak | cannot find symbol … method kislikMi() | instanceof ve aşağı dönüşüm |
| Hata | Sonuç | Düzeltme |
|---|---|---|
Nesneleri == ile karşılaştırmak | İçeriği aynı iki nesne için false | equals |
equals var, hashCode yok | HashSet'e aynı nokta iki kez eklenir: 2 false | Objects.hash(x, y) |
equals(Nokta) yazmak (@Override'sız) | contains eşit nesneyi bulamaz | equals(Object) ve @Override |
@Override'sız yazım hatası (Ciz) | Üst sınıfın sürümü sessizce çalışır | Her geçersiz kılmada @Override |
private metodu geçersiz kıldığını sanmak | Üst sınıfın kodu kendi private sürümünü çağırır | @Override bu yanılgıyı yakalar |
| Kontrolsüz aşağı dönüşüm | ClassCastException | Önce instanceof |
| Üst yapıcıdan geçersiz kılınabilen metot çağırmak | Alt sınıfın alanları henüz varsayılan değerde: b=0 s=null | Yapıcıdan yalnız private/final metot |
| Kendini çağıran metotları birlikte geçersiz kılmak | Sayaç iki kez artar: 6 3 | Oluşum ve iletme |
Kod paylaşmak için kalıtım (Yigin extends ArrayList) | İstenmeyen public metotlar gelir, ortadan ekleme yapılabilir | Oluşum |
Sözü bozan alt sınıf (Kare extends Dikdortgen) | Üst sınıf için yazılmış kod yanlış sonuç verir: 16 | Ortak üst sınıf ya da değiştirilemez sınıflar |
Bu hataların büyük kısmı iki alışkanlıkla önlenir. Birincisi, her geçersiz kılmada @Override yazmaktır; ikinci tablodaki ilk beş hatadan üçünü derleme hatasına çevirir. İkincisi, kalıtım kurmadan önce is-a testini sınıfın davranışına uygulamaktır. Test olumsuzsa ya da şüpheliyse oluşum seçilir.
4.3.9Özet
- Kalıtım, var olan bir sınıfın üyelerini alıp yenilerini ekleyerek yeni bir sınıf oluşturmaktır.
class Alt extends Ustyazılır;extends'ten sonra tek bir sınıf gelir.extendsyazılmayan sınıfObject'ten türer. - Alt sınıf üst sınıfın
publicveprotectedüyelerini kendi üyeleri gibi kullanır.privatealanlar nesnede vardır, ama doğrudan erişilemez. Yapıcılar kalıtımla alınmaz. newtek bir nesne oluşturur; üst sınıf kısmı bu nesnenin içindedir.superayrı bir nesne değil, aynı nesneye üst sınıfın kodu üzerinden ulaşma yoludur.super(...)üst sınıf yapıcısını çağırır ve Java 21'de yapıcının ilk deyimidir. Yazılmazsa derleyicisuper();ekler.super.m()üst sınıfın sürümünü çağırır ve derleme anında bağlanır.- Yapıcı çağrıları aşağıdan yukarı gider, gövdeler
Object→ … →Ust→Altsırasıyla tamamlanır. Nesne kurmanın sırası şudur: üst sınıf alanları, üst yapıcı, alt sınıf alanları, alt yapıcı.staticbloklar sınıf ilk kullanılırken bir kez, üstten alta çalışır. - Geçersiz kılma, alt sınıfta aynı imzalı metodu yeniden yazmaktır. Hangi sürümün çalışacağına nesnenin gerçek sınıfı karar verir. Aşırı yükleme ise farklı parametre listesidir ve derleme anında seçilir.
- Geçersiz kılan metot üst sınıfın sözünü daraltamaz. Dönüş tipi aynı ya da alt tip, erişim aynı ya da geniş, kontrol edilen istisnalar aynı ya da dar olur.
@Overridebu kuralları derleyiciye denetletir. private,staticvefinalmetotlar geçersiz kılınmaz.privateile aynı adlı metot yeni bir metottur;staticmetot yalnız gizlenir.protected= sınıf + aynı paket + alt sınıflar. Başka paketteki alt sınıfprotectedüyeye yalnız kendi tipinden bir referansla erişir. Alanlarprivatekalır, alt sınıflaraprotectedmetotlar açılır.finalmetot geçersiz kılınamaz,finalsınıftan alt sınıf türetilemez (String,Math,Integer).- Yukarı dönüşüm kendiliğindendir ve nesnenin görünümünü daraltır: çağrılabilecek metotları değişkenin tipi belirler. Aşağı dönüşüm açıkça yazılır ve önce
instanceofile denetlenir. - is-a kalıtımdır, has-a oluşumdur. Java sınıflarda çoklu kalıtıma izin vermez (elmas problemi); yerine oluşum ve arayüzler kullanılır.
- Kalıtım sıkı bağlılık getirir: kırılgan üst sınıf sorunu ve yerine koyma ilkesinin bozulması bunun sonuçlarıdır. Amaç yalnız kod paylaşmaksa oluşum ve iletme seçilir.
Objecther sınıfın atasıdır.toStringnesneyi okunur yazar.equalsiçerik karşılaştırır ve sözleşmeye uyar.equalsgeçersiz kılınıyorsahashCodeda kılınır; eşit nesnelerin hash kodu aynıdır.==referansları,equalsiçerikleri karşılaştırır.HashSetöncehashCodeile kova seçer, sonraequalssorar; iyi birhashCodeile işlemler ortalama O(1) sürer.
4.3.10Kavram listesi
- kalıtım inheritance
- üst sınıf / alt sınıf superclass / subclass
- super super keyword
- yapıcı zinciri constructor chaining
- varsayılan yapıcı default constructor
- ilklendirici blok initializer block
- geçersiz kılma method overriding
- aşırı yükleme method overloading
- metot imzası method signature
- etiket annotation
- kovaryant dönüş tipi covariant return type
- erişim belirleyici access modifier
- yukarı dönüşüm / aşağı dönüşüm upcasting / downcasting
- dinamik bağlama dynamic binding
- is-a / has-a ilişkisi is-a / has-a relationship
- oluşum composition
- çoklu kalıtım multiple inheritance
- kırılgan üst sınıf fragile base class
- iletme forwarding
- yerine koyma ilkesi Liskov substitution principle
- Object sınıfı Object class
- hash kodu hash code
4.3.11Kendini Dene
class X { X() { print("X "); } },class Y extends X { Y() { this(1); print("Y "); } Y(int a) { print("Y" + a + " "); } }veclass Z extends Y { Z() { super(5); print("Z "); } }verilsin.new Z()ne yazar?Cevap
Z()'nin ilk satırısuper(5)'tir ve doğrudanY(int)'i çağırır;Y()hiç çalışmaz.Y(int)'te gizlisuper()vardır: önceX()çalışır veXyazar. SonraY(int)'in gövdesiY5, en sonZ()'nin gövdesiZyazar. Sonuç:X Y5 Z.Yyazısı çıkmaz, çünküthis(1)içeren yapıcı çağrılmamıştır.class P { int f() { return 1; } int g() { return f() * 10; } }veclass Q extends P { int f() { return 2; } int g() { return super.g() + 1; } }verilsin.P p = new Q();içinp.g()kaçtır?Cevap
Nesne bir
Q'dur;Q.g()çalışır vesuper.g()ileP.g()'yi çağırır.P.g()'nin içindekif(),this.f()'tir vethishâlâ birQ'dur:Q.f()= 2 döner.P.g()= 2 · 10 = 20,Q.g()= 20 + 1 = 21.superyalnızg'nin hangi sürümünün çağrılacağını belirler; o sürümün içindeki çağrılar yine nesneye göre seçilir.new P().g()ise 10 verir.Hangi çiftler is-a, hangileri has-a ilişkisidir? Transistör–Yarı iletken eleman, Devre–Direnç, Telefon–Batarya, Elektrikli süpürge–Ev aleti.
Cevap
is-a: Transistör–Yarı iletken eleman (“her transistör bir yarı iletken elemandır”) ve Elektrikli süpürge–Ev aleti. has-a: Devre–Direnç (bir devrenin dirençleri vardır; dirençler bir liste alanında tutulur) ve Telefon–Batarya (telefonun bir bataryası vardır; batarya değiştirilebilir, bu da oluşumun alanın sonradan değiştirilebilmesi özelliğine uyar).
class U { U() { System.out.println("U(): " + ad()); } String ad() { return "U"; } }veclass V extends U { String s = "V"; String ad() { return s; } }verilsin.new V()ne yazar?Cevap
U(): null.V()'nin gizlisuper()'ıU()'yu çalıştırır.U()içindekiad()geçersiz kılınmıştır ve nesne birVolduğu içinV.ad()'a gider. Ama alt sınıfın alan ilk değerlerisuper()döndükten sonra atanır. O andashâlânull'dır.KisiIsınıfınınequals'ıgetClass()yerineinstanceof KisiIkullanıyor ve adı ile yaşı karşılaştırıyor.OgrenciI extends KisiIyeni alan eklemiyor veequals'ı geçersiz kılmıyor.new KisiI("Ali", 20).equals(new OgrenciI("Ali", 20))ve ters sıradaki çağrı ne verir?Cevap
İkisi de
true'dur. BirOgrenciIbirKisiI'dir;instanceofiki yönde de doğru çıkar ve alanlar eşittir. Simetri korunur, çünkü iki çağrı da aynıequals'ı çalıştırır.getClass()kullanılsaydı iki çağrı dafalseverirdi.Ust'teprotected Object kopya()var.Alt extends Ustiçinde@Override public Alt kopya()derlenir mi?@Override Alt kopya()(belirleyicisiz) derlenir mi?Cevap
Birincisi derlenir.
Alt,Object'in alt tipidir (kovaryant dönüş) ve erişimprotected'tanpublic'e genişler. İkincisi derlenmez: belirleyicisiz yazmak paket erişimidir veprotected'tan daha dardır. javacattempting to assign weaker access privileges; was protectedder.class A { final void f() { … } }veclass B extends A { void f(int x) { … } }derlenir mi?Cevap
Derlenir.
f(int)'in parametre listesi farklıdır;A.f()'yi geçersiz kılmaz, aşırı yükler.finalyalnız geçersiz kılmayı yasaklar.Bnesnesinde iki metot da vardır:b.f()A'nın sürümünü,b.f(3)B'ninkini çalıştırır.canli.Hayvan'daprotected String isimvar;canli.Kopek extends Hayvan. Başka pakettekievcil.Kedi extends HayvansınıfınaString f(Kopek k) { return k.isim; }yazılıyor. Derlenir mi?Cevap
Derlenmez:
isim has protected access in Hayvan. Başka paketteki alt sınıf,protectedüyeye yalnız kendi tipinden (Kediya da alt tipleri) bir referansla erişebilir.kbirKopek'tir; bir kedinin bir köpeğin iç verisine dokunmasına izin verilmez.Kedicanlipaketinde olsaydı derlenirdi.Hash kodu
Objects.hash(x, y)olan birNoktasınıfında, boş birHashSet'e önce (3, 4), sonra (1, 2) ekleniyor. İki nokta hangi kovalara düşer?size()kaçtır?Cevap
(3, 4): 31 · 34 + 4 = 1058, 1058 % 16 = 2. (1, 2): 31 · 32 + 2 = 994, 994 % 16 = 2. İki nokta aynı kovaya düşer. İkinci eklemede küme kovadaki (3, 4) ile
equalssorar,falsealır ve (1, 2)'yi de ekler. İkiadddatruedöner;size()= 2. Aynı kova eşit nesne demek değildir.
4.3.12İleri okuma
- Oracle, The Java Tutorials, “Inheritance” bölümü: alt sınıf, geçersiz kılma ve gizleme,
super,Objectsınıfı vefinalkonularını kısa örneklerle anlatır. Java'nın resmî ve ücretsiz başlangıç kaynağıdır. - J. Bloch, Effective Java (3. baskı), Madde 10–11 ve 18–19:
equalsilehashCode'un nasıl yazılacağını anlatır. Oluşumun neden kalıtıma tercih edildiğini ve kalıtım için tasarlamanın kurallarını da tartışır. Kırılgan üst sınıf örneğinin kaynağı da bu kitaptır. - C. S. Horstmann, Core Java, Volume I: Fundamentals (12. baskı), Bölüm 5 “Inheritance”:
equals'tagetClass()ileinstanceofseçimini ve simetri sorununu ayrıntılı tartışır. - OpenJDK, JEP 513 “Flexible Constructor Bodies”: Java 25'te
super(...)'dan önce deyim yazmaya izin veren değişikliği ve gerekçesini anlatır. Yapıcı kurallarının neden konduğunu da açıklar.
4.3.13Kaynaklar
- Oracle, The Java Tutorials: Inheritance; Overriding and Hiding Methods; Using the Keyword super; Object as a Superclass.
- J. Gosling ve diğerleri, The Java Language Specification, Java SE 21 Edition, Oracle, 2023 (8.1.4 Superclasses and Subclasses, 8.4.8 Inheritance, Overriding, and Hiding).
- P. Deitel ve H. Deitel, Java: How to Program, 11. baskı, Pearson (Bölüm 9: Object-Oriented Programming: Inheritance).
- K. Sierra ve B. Bates, Head First Java, 3. baskı, O'Reilly, 2022 (Bölüm 7: Inheritance and Polymorphism).
- C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, Pearson, 2022 (Bölüm 5: Inheritance).
- J. Bloch, Effective Java, 3. baskı, Addison-Wesley, 2018 (Madde 10–11: equals ve hashCode; Madde 18: oluşumu kalıtıma tercih etmek).
- OpenJDK, JEP 513: Flexible Constructor Bodies, JDK 25, 2025. https://openjdk.org/jeps/513
İlgili sayfalar: Bölüm 4.3 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.