Java ile Nesne Yönelimli Programlama · Ders Notu

Bölüm 4.4: Çok Biçimlilik

Bir hayvanat bahçesinde bakıcı “herkes ses çıkarsın” der; köpek havlar, kedi miyavlar. Bakıcı her hayvana ayrı komut vermez, her hayvan aynı komuta kendi bildiği gibi karşılık verir. Nesne yönelimli programlamada bu fikrin adı çok biçimliliktir. Kod üst sınıf tipiyle bir kez yazılır; çalışma sırasında her nesne çağrıya kendi sürümüyle cevap verir. Not önce referans tipi ile nesne tipi arasındaki farkı kurar: derleyici birine, Java sanal makinesi (JVM) ötekine bakar. Ardından yukarı ve aşağı dönüşümü, ClassCastException'ı ve instanceof sınamasını işler. Dinamik bağlama, metot araması ve metot tablosu notun merkezindedir. Alanların, static metotların ve aşırı yüklemenin neden derleme zamanında seçildiği ayrıca incelenir. Not şekil hiyerarşisi, çok biçimli parametreler, bir tanksavar oyunu ve equals metodunun simetri tuzağıyla biter.

Öğrenme çıktıları

Kavram
Çok biçimliliği tanımlamak; derleme zamanı ve çalışma zamanı çok biçimliliğini ayırt etmek.
Referans kuralı
Üst sınıf referansına alt sınıf nesnesi atanabildiğini, tersinin neden derleme hatası verdiğini açıklamak.
Tip dönüşümü
Yukarı ve aşağı dönüşümü yazmak; ClassCastException'ı instanceof ile önlemek; dizi kovaryansının doğurduğu ArrayStoreException'ı tanımak.
Dinamik bağlama
Çağrılan metodun nesnenin gerçek sınıfına göre seçildiğini adım adım izlemek; metot aramasını hiyerarşide ve metot tablosunda göstermek.
Aşırı yükleme ve geçersiz kılma
Aşırı yükleme ile geçersiz kılmayı karşılaştırmak; hangisini derleyicinin, hangisini JVM'in seçtiğini bilmek; aşırı yükleme seçiminin üç aşamasını uygulamak.
Tasarım
Şekil dizisi ve tanksavar oyunu örnekleriyle üst sınıf tipiyle genel kod yazmak; equals metodunun simetri tuzağını tanımak.

4.4.1Çok biçimlilik kavramı

Nesne yönelimli programlamanın dört temel ilkesi sarmalama, kalıtım, çok biçimlilik ve soyutlamadır. Sarmalama veriyi ve onu işleyen metotları bir sınıfta toplar. Kalıtım, bir sınıfın başka bir sınıfı genişletmesini sağlar. Çok biçimlilik bu ikisinin üzerine kurulur: bir sınıf hiyerarşisi olmadan çalışma zamanı çok biçimliliği de olmaz. Bu yüzden kalıtımın temel kavramları burada da kullanılır: üst sınıf, alt sınıf, extends, geçersiz kılma ve @Override.

4.4.1.1Çok biçimlilik nedir?

Tanım 4.4.1

Çok biçimlilik (polymorphism), aynı ifadenin farklı nesnelerde farklı davranışa yol açmasıdır. Sözcük Yunanca poly (çok) ve morph (biçim) sözcüklerinden gelir. Java'da bir referans, kendi tipindeki ya da bir alt sınıfa ait bir nesneyi gösterebilir. Bu referans üzerinden çağrılan geçersiz kılınmış metodun hangi sürümünün çalışacağını, gösterilen nesnenin gerçek sınıfı belirler.

Tanımın özü tek bir satırda görülür. hayvan.sesCikar() çağrısı hayvan bir köpeği gösteriyorsa Hav hav!, bir kediyi gösteriyorsa Miyav! yazdırır. Satır aynıdır, davranış nesneye göre değişir. Kod üst sınıf tipiyle bir kez yazılır; her nesne çağrıya kendi sürümüyle cevap verir.

Gündelik hayatta bu fikrin pek çok örneği vardır. Uzaktan kumandadaki aynı “aç” düğmesi televizyonu, klimayı ve projektörü açar; her cihaz bu işi kendi bildiği gibi yapar. B sınıfı sürücü belgesiyle her marka otomobil sürülür. Sürücü yalnız “otomobil” arayüzünü kullanır: direksiyon, pedallar, vites. Motorun ayrıntısı ise markaya aittir. Hayvanat bahçesindeki bakıcı da tek komut verir ve her hayvan kendi sesini çıkarır. Üç örnekte de çağıran taraf ortak bir arayüzü bilir; ayrıntıyı nesne kendisi tamamlar.

Hayvan h1 = new Kopek();
Hayvan h2 = new Kedi();

h1.sesCikar(); // Hav hav!
h2.sesCikar(); // Miyav!

Bu dört satırda iki değişken de Hayvan tipindedir. h1 bir Kopek, h2 bir Kedi nesnesini gösterir. Derleyici iki çağrıyı da aynı biçimde, Hayvan sınıfının sesCikar() metoduna yapılmış çağrı olarak derler. Program çalışırken ise JVM her çağrıda nesneye bakar ve köpek için Kopek'in, kedi için Kedi'nin sürümünü çalıştırır. Çıktı kodun yorumlarında yazıldığı gibidir: önce Hav hav!, sonra Miyav!.

4.4.1.2Çok biçimliliğin iki türü

Java'da “aynı ad, farklı davranış” iki farklı mekanizmayla elde edilir. Birincisi aşırı yüklemedir: aynı sınıfta, aynı adlı ama parametreleri farklı birden çok metot yazılır. Hangisinin çağrılacağına derleyici karar verir. İkincisi geçersiz kılmadır: alt sınıf, üst sınıftan aldığı metodu aynı imzayla yeniden yazar. Hangi sürümün çalışacağına program çalışırken JVM karar verir.

Tanım 4.4.2

Derleme zamanı çok biçimliliği (statik çok biçimlilik), aşırı yüklenmiş metotlar arasındaki seçimi derleyicinin, argümanların sayısına ve bildirilen tiplerine bakarak yapmasıdır. Çalışma zamanı çok biçimliliği (dinamik çok biçimlilik) ise geçersiz kılınmış metotlar arasındaki seçimin program çalışırken, nesnenin gerçek sınıfına bakılarak yapılmasıdır. Bu ikinci seçime dinamik bağlama denir.

İki tür aşağıdaki iki kısa örnekte yan yana görülür. Soldaki örnekte tek bir sınıfta iki topla metodu vardır; argümanlar tam sayıysa int sürümü, ondalıklıysa double sürümü seçilir. Bu seçim program derlenirken yapılır ve bayt koduna yazılır. Sağdaki örnekte ise seçim nesneye bağlıdır ve ancak program çalışırken belli olur.

int topla(int a, int b) { ... }
double topla(double a, double b) { ... }

topla(5, 3);     // int sürümü
topla(3.5, 2.7); // double sürümü
Tablo 4.4.1: Çok biçimliliğin iki türünün karşılaştırması.
Derleme zamanı (statik)Çalışma zamanı (dinamik)
MekanizmaAşırı yüklemeGeçersiz kılma + dinamik bağlama
Seçimi yapanDerleyici (javac)JVM, program çalışırken
Neye bakılır?Argümanların sayısı ve bildirilen tipleriNesnenin gerçek sınıfı
Kalıtım gerekir mi?Hayır, tek sınıfta olurEvet, sınıf hiyerarşisi gerekir
Kısa tarifAynı ad, farklı parametrelerAynı imza, farklı sınıflar

Günlük konuşmada “çok biçimlilik” denince çoğunlukla çalışma zamanı çok biçimliliği kastedilir. Notun büyük kısmı da onu anlatır. Aşırı yükleme de ayrıca incelenir, çünkü iki mekanizma aynı çağrıda birlikte çalışabilir ve en çok karışıklık bu durumda çıkar.

4.4.1.3Örnek sınıf hiyerarşisi

Notun büyük kısmında aynı küçük hiyerarşi kullanılır. Hayvan sınıfının üç metodu vardır: sesCikar(), kos() ve uyu(). Kopek ve Kedi bu sınıftan türer. Her sınıf gibi Hayvan da Java'daki bütün sınıfların atası olan Object'ten türer. Şekil 4.4.1 bu yapıyı UML sınıf diyagramıyla gösterir.

class Hayvan {
  public void sesCikar() { System.out.println("Hayvan sesi"); }
  public void kos()      { System.out.println("Hayvan koşuyor"); }
  public void uyu()      { System.out.println("Hayvan uyuyor"); }
}
class Kopek extends Hayvan {
  @Override public void sesCikar() { System.out.println("Hav hav!"); }
  @Override public void kos() { System.out.println("Köpek koşuyor"); }
  public void havla() { System.out.println("Köpek havlıyor"); }
}
class Kedi extends Hayvan {
  @Override public void sesCikar() { System.out.println("Miyav!"); }
  public void tirmala() { System.out.println("Kedi tırmalıyor"); }
}
Object+toString(): String+equals(Object): boolean+getClass(): ClassHayvan+sesCikar(): void+kos(): void+uyu(): voidKopek+sesCikar(): void+kos(): void+havla(): voidKedi+sesCikar(): void+tirmala(): void
Şekil 4.4.1: Örnek sınıf hiyerarşisi. Kopek ve Kedi, Hayvan'dan; Hayvan da Object'ten türer. Oklar alt sınıftan üst sınıfa yönelir.

İki alt sınıfın üst sınıfla ilişkisi farklıdır. Kopek, sesCikar() ve kos() metotlarını geçersiz kılar ve kendine özgü havla() metodunu ekler. Kedi yalnız sesCikar()'ı geçersiz kılar ve tirmala()'yı ekler. uyu()'yu iki alt sınıf da geçersiz kılmaz; ikisi de onu Hayvan'dan olduğu gibi alır. kos() ise yalnız köpekte değişmiştir; kedi Hayvan'ın sürümünü kullanır.

Bu ayrıntıların her biri bir kurala karşılık gelir. havla() yalnız Kopek'te bulunduğu için Hayvan tipindeki bir referansla çağrılamaz. uyu() alt sınıflarda yazılmadığı için metot araması Hayvan'a kadar çıkar. @Override etiketi (annotation) derleyiciye “bu metot üst sınıftaki bir metodu geçersiz kılıyor” bilgisini verir. Ad ya da parametre yanlış yazılırsa derleyici hata verir.

Kalıtımdaki is-a (bir türüdür) ilişkisi buradaki bütün kuralların temelidir. Köpek de kedi de bir hayvandır. Bu yüzden ikisi de Hayvan tipindeki bir değişkende tutulabilir. Tersi doğru değildir: her hayvan bir köpek değildir.

4.4.2Referanslar ve tip uyumu

Çok biçimliliği anlamak için önce Java'daki değişkenlerin nesneyi değil, nesneye bir referansı tuttuğunu hatırlamak gerekir. Nesne heap'te yaşar. Değişken ise yığındaki bir çerçevede durur ve heap'teki nesnenin yerini gösterir. Bir nesneyi birden çok referans gösterebilir; bu referansların tipleri de farklı olabilir.

4.4.2.1Bir nesne, üç referans

Aşağıdaki kod tek bir Kopek nesnesi oluşturur ve onu üç farklı tipte referansla gösterir.

// Köpek nesnesine referans
Kopek kopek = new Kopek();
// Hayvan referansı Kopek'i gösterebilir
Hayvan hayvan = kopek;
// Object referansı da gösterebilir
Object nesne = hayvan;
System.out.println(kopek == hayvan);
System.out.println(hayvan == nesne);
System.out.println(nesne.getClass());

Program şunları yazar:

true
true
class Kopek

Kodu satır satır izleyelim. İlk satırdaki new Kopek(), heap'te bir nesne oluşturur. kopek referansı bu nesneyi gösterir; referansın tipi Kopek'tir. İkinci atama yeni bir nesne oluşturmaz. hayvan, aynı nesneyi gösteren ikinci bir referanstır. Atama geçerlidir, çünkü köpek bir hayvandır. Üçüncü atamada Object tipindeki nesne de aynı nesneyi gösterir. Her sınıf Object'ten türediği için bu da geçerlidir.

Sonraki iki satır == işlecini kullanır. Referans tiplerinde == nesnelerin içeriğini değil, referansların aynı nesneyi gösterip göstermediğini karşılaştırır. Üç referans da aynı nesneyi gösterdiği için iki karşılaştırma da true verir. Son satırdaki getClass(), nesnenin çalışma zamanındaki sınıfını döndürür. Referansın tipi Object olsa da nesnenin sınıfı değişmemiştir: class Kopek.

Referans değişkenleriHeap'teki nesnekopektip: Kopekhayvantip: Hayvannesnetip: ObjectKopek nesnesiObject kısmıHayvan kısmıKopek kısmı
Şekil 4.4.2: Üç referans, tek nesne. kopek, hayvan ve nesne heap'teki aynı Kopek nesnesini gösterir. Nesnenin içinde Object'ten, Hayvan'dan ve Kopek'ten gelen kısımlar vardır.

Şekil 4.4.2 nesnenin iç yapısını da gösterir. Bir Kopek nesnesi, kalıtım zincirindeki her sınıftan gelen kısımları taşır: Object kısmı, Hayvan kısmı ve Kopek kısmı. Bu yüzden nesne, zincirdeki her tipin referansıyla gösterilebilir. Her referans nesnenin tamamını gösterir, ama nesneye kendi tipinin penceresinden bakar.

4.4.2.2Referans tipi ve nesne tipi

Önceki örnek, her referans değişkeninin iki ayrı tipi olduğunu gösterir. Biri değişkenin tanımında yazan tiptir. Öteki, değişkenin o anda gösterdiği nesnenin sınıfıdır.

Tanım 4.4.3

Bir referans değişkeninin bildirilen tipi (declared type), tanımında yazan tiptir ve değişkenin ömrü boyunca değişmez. Gerçek tipi (actual type) ise değişkenin o anda gösterdiği nesnenin sınıfıdır ve atamayla değişebilir. Bildirilen tipe kısaca referans tipi, gerçek tipe nesne tipi de denir.

Hayvan h = new Kopek();
// bildirilen tür: Hayvan
// gerçek tür:     Kopek
Hayvan hreferansKopek nesnesiheap'tebildirilen tipderleyici bakargerçek tipJVM bakar
Şekil 4.4.3: Hayvan h = new Kopek(); satırının iki tipi. Bildirilen tip Hayvan'dır ve ona derleyici bakar. Gerçek tip Kopek'tir ve ona JVM bakar.

İki tipin işlevi farklıdır ve bu ayrım notun geri kalanının anahtarıdır. Derleyici program çalışmadan önce kodu denetler. O anda hiçbir nesne yoktur; derleyici yalnız değişkenlerin tanımlarını bilir. Bu yüzden derleyicinin bütün kararları bildirilen tipe dayanır. JVM ise program çalışırken gerçek nesneyle karşılaşır ve nesnenin hangi sınıftan olduğunu bilir. Tablo 4.4.2 üç temel soruyu ve cevabı veren tarafı özetler.

Tablo 4.4.2: Referans tipi ve nesne tipi: hangi soruya kim karar verir?
SoruKim karar verir?
Bu metot çağrılabilir mi?Derleyici, referans tipine bakar.
Hangi sürüm çalışır?JVM, nesne tipine bakar.
Hangi atamalar geçerli?Derleyici, referans tipine bakar.

İlk sorunun sonucu aşağıdaki kodda görülür. Nesne gerçekten bir köpektir, ama değişkenin tipi Hayvan'dır.

Hayvan hayvan = new Kopek();
hayvan.havla(); // derleme hatası
error: cannot find symbol
  symbol:   method havla()
  location: variable hayvan of type Hayvan

Derleyici hayvan.havla() satırına geldiğinde yalnız şunu bilir: hayvan, Hayvan tipinde bir değişkendir. Hayvan sınıfında ve onun üst sınıfı Object'te havla() adlı bir metot yoktur. Derleyici, değişkenin hangi nesneyi gösterdiğini bilemez. Program çalışırken o satıra gelindiğinde değişken bir kediyi de gösteriyor olabilir. Bu yüzden çağrıya izin vermez. İletideki location: variable hayvan of type Hayvan satırı da kararın değişkenin tipine göre verildiğini açıkça söyler.

Uzaktan kumanda benzetmesi bu durumu iyi anlatır. Referans tipi bir kumandadır; kumandada hangi düğmeler varsa yalnız onlar kullanılabilir. Nesne bir köpek olsa da Hayvan kumandasında havla() düğmesi yoktur. Düğmeye basıldığında cihazın ne yapacağını ise cihaz, yani nesne belirler. sesCikar() düğmesi Hayvan kumandasında vardır; köpeğe basılınca havlama, kediye basılınca miyavlama duyulur.

Sık yapılan hata

“Nesne köpek olduğuna göre havla() çağrılabilir” diye düşünmek. Çağrının derlenip derlenmeyeceğine nesne değil, referansın bildirilen tipi karar verir. havla() çağrılmak isteniyorsa referans ya baştan Kopek tipinde tanımlanır ya da aşağı dönüşümle Kopek tipine çevrilir.

4.4.2.3Referans kuralı

Atamaların geçerliliği de bildirilen tiplere göre denetlenir. Java'nın bu konudaki kuralı kısadır ve kalıtımdaki is-a ilişkisinden doğar.

Tanım 4.4.4

Referans kuralı: Bir nesnenin alt tipi, üst sınıf tipindeki bir referansa atanabilir. Üst sınıfın nesnesi ise alt sınıf tipindeki bir referansa atanamaz. Kısaca, alt sınıf referansından üst sınıf referansına atama otomatiktir; tersi açık dönüşüm ister.

// Kopek, Hayvan'ın alt türüdür
Hayvan hayvan = new Kopek();
Object nesne = new Kedi();

Bu iki atama geçerlidir. Her köpek bir hayvandır: bir hayvandan beklenen her şeyi, yani sesCikar(), kos() ve uyu()'yu köpek de yapabilir. Her kedi de bir Object'tir. Üst tipteki referans, alt tipteki nesnenin yapamayacağı bir şey isteyemez. Bu yüzden atama güvenlidir.

// Hayvan, Kopek'in üst sınıfıdır
Kopek kopek = new Hayvan();
error: incompatible types:
  Hayvan cannot be converted to Kopek

Bu atama ise derlenmez. kopek değişkeni üzerinden ileride kopek.havla() çağrılabilir. Düz bir Hayvan nesnesi ise havla() bilmez. Derleyici böyle bir atamaya izin verseydi, program çalışırken var olmayan bir metot çağrılmaya çalışılırdı. Java bu tür hataları baştan, derleme zamanında engeller.

Hayvan hKopek nesnesi✓ geçerliKopek kHayvan nesnesi✗ derlenmez
Şekil 4.4.4: Referans kuralı. Hayvan referansı bir Kopek nesnesini gösterebilir; Kopek referansı bir Hayvan nesnesini gösteremez.

Kuralın en çok şaşırtan sonucu şu iki satırda görülür:

Hayvan h = new Kopek();
Kopek k = h;   // derleme hatası
error: incompatible types: Hayvan cannot be converted to Kopek

Burada h'nin gösterdiği nesne gerçekten bir köpektir. Yine de ikinci satır derlenmez ve aynı ileti çıkar. Derleyici h'nin gösterdiği nesneyi değil, yalnız bildirilen tipini (Hayvan) görür. Bir önceki satırı okuyup “demek ki bu bir köpek” çıkarımını yapmaz; yapması da her durumda mümkün değildir. Çözüm açık dönüşümdür: Kopek k = (Kopek) h;. Bu yazımla programcı derleyiciye “bu referansın bir köpeği gösterdiğinden eminim” der. Dönüşümün doğru olup olmadığı ise çalışma zamanında denetlenir.

Örnek 4.4.1

Kopek ve Kedi, Hayvan'dan türüyor. Aşağıdaki dört atamadan hangileri derlenir?

(a) Hayvan h = new Kedi(); — Derlenir. Kedi bir hayvandır; alt sınıf nesnesi üst sınıf referansına atanır.

(b) Object o = new Kopek(); — Derlenir. Her sınıf Object'in alt sınıfıdır.

(c) Kedi c = new Kopek(); — Derlenmez: incompatible types: Kopek cannot be converted to Kedi. Kopek ile Kedi kardeş sınıflardır; aralarında is-a ilişkisi yoktur.

(d) Kopek k = new Hayvan(); — Derlenmez: incompatible types: Hayvan cannot be converted to Kopek. Üst sınıf nesnesi alt sınıf referansına atanamaz.

4.4.3Tip dönüşümü

İlkel tiplerde dönüşüm tanıdık bir işlemdir. int'ten double'a geçiş kendiliğinden olur; (int) 3.7 ise açık dönüşümdür ve değeri değiştirir. Referans tiplerinde de benzer iki yön vardır, ama önemli bir fark taşır: referans dönüşümü nesneyi hiçbir zaman değiştirmez. Değişen yalnız nesneye bakan referansın tipidir.

4.4.3.1Yukarı ve aşağı dönüşüm

Tanım 4.4.5

Yukarı dönüşüm (upcasting), alt sınıf tipindeki bir referansın üst sınıf tipine çevrilmesidir. Otomatiktir ve her zaman güvenlidir. Aşağı dönüşüm (downcasting), üst sınıf tipindeki bir referansın alt sınıf tipine çevrilmesidir. Parantez içinde hedef tip yazılarak açıkça yapılır: (Kopek) h. Başarısı, referansın gösterdiği nesneye bağlıdır.

HayvanKopekyukarıotomatikaşağı(Kopek) ile
Şekil 4.4.5: Yukarı dönüşüm Kopek'ten Hayvan'a kendiliğinden olur. Aşağı dönüşüm Hayvan'dan Kopek'e açık dönüşümle, (Kopek) yazılarak yapılır.
Kopek kopek = new Kopek();
Hayvan h = kopek;    // yukarı: otomatik
Hayvan h1 = (Hayvan) kopek; // gereksiz

İkinci satırda yukarı dönüşüm kendiliğinden yapılır; referans kuralının ta kendisidir. Üçüncü satırdaki (Hayvan) yazımı da geçerlidir, ama gereksizdir. Derleyici bu dönüşümü zaten kendisi yapar.

Hayvan h = new Kopek();
Kopek k = (Kopek) h; // aşağı: açık
k.havla();           // Köpek havlıyor

Burada ters yöne gidilir. h'nin bildirilen tipi Hayvan'dır; (Kopek) h ifadesinin tipi ise Kopek'tir. Bu ifade Kopek tipindeki k'ye atanabilir ve k.havla() derlenir. Program çalışırken JVM, h'nin gösterdiği nesnenin gerçekten bir Kopek olup olmadığını denetler. Nesne bir köpek olduğu için dönüşüm başarılı olur ve Köpek havlıyor yazılır.

Tablo 4.4.3: Yukarı ve aşağı dönüşümün karşılaştırması.
Yukarı dönüşümAşağı dönüşüm
YönAlt → üstÜst → alt
YazımOtomatik (örtük)Açık: (Kopek) h
Güvenli mi?Her zamanNesneye bağlı
Diğer adıGenişletme dönüşümü (widening)Daraltma dönüşümü (narrowing)

Tablodaki “genişletme” ve “daraltma” adları ilkel tiplerden tanıdıktır. Java dil belirtimi referans dönüşümlerini de bu adlarla anar. Yukarı dönüşüm, referansın görebildiği metot kümesini daraltsa da “genişletme” sayılır. Ad, tipin kapsadığı nesne kümesinin genişlemesinden gelir: Hayvan tipi bütün köpekleri, kedileri ve öteki hayvanları kapsar.

Sık yapılan hata

“Dönüşüm nesneyi değiştirir” sanmak. (Hayvan) kopek yazmak köpeği düz bir hayvana çevirmez. ((Hayvan) kopek).sesCikar() çağrısı yine Hav hav! yazar, çünkü çalışacak sürümü nesne belirler. Dönüşüm yalnız derleyicinin o ifadeyi hangi tipte gördüğünü değiştirir.

4.4.3.2Aşağı dönüşüm ve öncelik tuzağı

Aşağı dönüşümün amacı, üst sınıfta bulunmayan, yalnız alt sınıfa ait metotları çağırmaktır. Üst sınıf referansı ancak bu dönüşümle bir alt sınıf değişkenine atanabilir. Dönüşüm iki biçimde yazılır: sonucu bir değişkene atanır ya da aynı satırda doğrudan kullanılır.

Hayvan h2 = new Kopek();
Kopek k = (Kopek) h2;
k.havla();
((Kopek) h2).havla(); // tek satırda
Köpek havlıyor
Köpek havlıyor

Program iki kez Köpek havlıyor yazar. İlk yazımda dönüşümün sonucu k'ye atanır ve k.havla() çağrılır. İkinci yazımda ((Kopek) h2) ifadesi bir Kopek referansı üretir ve .havla() doğrudan bu ifadeye uygulanır. Dıştaki parantez çifti zorunludur. Nedeni, Java'daki işleç önceliğidir.

Tanım 4.4.6

Öncelik kuralı: Üye erişimi ve metot çağrısı (.), dönüşüm işlecinden önce uygulanır. (Kopek) h2.havla() ifadesi (Kopek) (h2.havla()) olarak okunur: dönüşüm, metot çağrısının sonucuna uygulanmak istenir.

(Kopek) h2.havla();   // hatalı
error: not a statement

Derleyici bu satırı ayrıştırırken önce h2.havla() çağrısını, sonra onun sonucunun Kopek'e dönüştürülmesini görür. Böyle bir dönüşüm ifadesi tek başına bir deyim olamaz; derleyici not a statement der. Satır başka bir biçimde düzeltilse bile ikinci bir sorun kalır: h2'nin bildirilen tipi Hayvan'dır ve Hayvan'da havla() yoktur.

İfade bir atamanın sağında kullanıldığında ikinci sorun doğrudan görünür:

String s = (Kopek) h2.isim();
// h2.isim() önce aranır → Hayvan'da yok
error: cannot find symbol
  symbol:   method isim()
  location: variable h2 of type Hayvan

Burada da önce h2.isim() çözülmeye çalışılır. Derleyici Hayvan sınıfında isim() bulamaz ve hata verir. JDK 21'in javac'ı bu satır için ayrıca ikinci bir ileti de verir: incompatible types: Kopek cannot be converted to String. Dönüşümün sonucu bir Kopek'tir ve String değişkenine atanamaz. Doğrusu her durumda aynıdır: dönüşüm parantez içine alınır, nokta parantezin dışına yazılır: ((Kopek) h2).havla();.

4.4.3.3ClassCastException

Aşağı dönüşüm, derleyiciye verilmiş bir sözdür. Derleyici bu sözün doğru olup olmadığını bilemez ve kabul eder. Söz tutulmazsa hata program çalışırken ortaya çıkar.

Hayvan h4 = new Kedi();
Kopek k4 = (Kopek) h4; // derlenir!
k4.havla();
Exception in thread "main" java.lang.ClassCastException:
class Kedi cannot be cast to class Kopek

İkinci satır derlenir. Derleyicinin bakış açısından bunda bir sorun yoktur: Hayvan tipindeki bir referans pekâlâ bir Kopek'i gösteriyor olabilir. Program çalışırken ise JVM dönüşüm anında nesnenin gerçek sınıfını denetler. Nesne bir Kedi'dir ve kedi bir köpek değildir. JVM bu durumda ClassCastException fırlatır; yakalayan olmadığı için program durur. Üçüncü satır hiç çalışmaz.

Tanım 4.4.7

ClassCastException, bir referansın, gösterdiği nesnenin sınıfıyla uyumsuz bir tipe açıkça dönüştürülmek istendiğinde JVM'in fırlattığı çalışma zamanı istisnasıdır. (T) r dönüşümü, ancak r'nin gösterdiği nesne T'den ya da T'nin bir alt sınıfından ise başarılı olur. r null ise dönüşüm her zaman başarılıdır ve sonuç yine null'dır.

JDK 21'de iletinin tamamı biraz daha uzundur: class Kedi cannot be cast to class Kopek (Kedi and Kopek are in unnamed module of loader 'app'). Ayraç içindeki kısım sınıfların hangi modülden ve hangi sınıf yükleyiciden geldiğini söyler; bu derste önemli değildir. İletinin baş kısmı ise her şeyi anlatır: hangi sınıftan hangi sınıfa dönüşüm istendiği.

Aynı istisna, düz bir üst sınıf nesnesi alt sınıfa dönüştürülmek istendiğinde de çıkar:

Kopek k5 = (Kopek) new Hayvan(); // derlenir!
java.lang.ClassCastException:
class Hayvan cannot be cast to class Kopek

Bu satır da derlenir, çünkü ifadenin tipi Hayvan'dır ve Hayvan'dan Kopek'e dönüşüm derleyicinin gözünde mümkündür. Ama oluşturulan nesne bir köpek değildir ve hiçbir zaman köpek olmayacaktır. Buradan çıkan genel sonuç şudur: üst sınıfın kendi nesnesi alt sınıf referansına hiçbir zaman atanamaz. Dönüşüm, yalnız nesne gerçekten o alt sınıftansa başarılı olur.

Derleyicinin hiç kabul etmediği bir dönüşüm türü de vardır. Aralarında kalıtım ilişkisi olmayan iki tip arasında dönüşüm yazılırsa derleyici bunun hiçbir nesne için doğru olamayacağını bilir:

Kopek kopek = new Kopek();
Kedi c = (Kedi) kopek;   // derleme hatası
error: incompatible types: Kopek cannot be converted to Kedi

Kopek ile Kedi kardeş sınıflardır. Bir nesne aynı anda hem köpek hem kedi olamaz; Java'da bir sınıfın yalnız bir üst sınıfı vardır. Bu yüzden derleyici dönüşümü derleme zamanında reddeder. Sınıflar için kural kısaca şöyledir: dönüşüm, kaynak ve hedef tipten biri ötekinin alt tipi değilse derlenmez; alt tipiyse derlenir ve çalışma zamanında denetlenir.

Örnek 4.4.2

Hayvan h4 = new Kedi(); verilsin. Aşağıdaki üç dönüşüm için derleyicinin ve JVM'in kararı nedir?

(a) (Kedi) h4: Kedi, Hayvan'ın alt tipidir; derlenir. Nesne bir kedidir; JVM onaylar. Başarılı.

(b) (Kopek) h4: Kopek, Hayvan'ın alt tipidir; derlenir. Nesne bir kedidir; JVM ClassCastException fırlatır.

(c) (Object) h4: yukarı dönüşümdür, derlenir ve her zaman başarılıdır. Nesne değişmez; referansın tipi Object olur.

4.4.3.4Kod izleme: hatalı aşağı dönüşüm

Aşağıdaki program bir başarılı ve bir başarısız aşağı dönüşüm içerir. Satırlar 1'den 7'ye numaralanırsa izleme daha kolay okunur.

Hayvan h1 = new Kopek();
Hayvan h2 = new Kedi();
Kopek k1 = (Kopek) h1;
k1.havla();
Kopek k2 = (Kopek) h2;
k2.havla();
System.out.println("Bitti");
Tablo 4.4.4: Hatalı aşağı dönüşüm programının izlemesi.
SatırDerleyiciJVM (çalışırken)Durum / çıktı
1Yukarı dönüşüm, geçerliKopek nesnesi #1 oluşurh1 → #1
2Yukarı dönüşüm, geçerliKedi nesnesi #2 oluşurh2 → #2
3Hayvan → Kopek mümkün, derlenir#1 bir Kopek mi? Evetk1 → #1
4Kopek'te havla() varKopek.havla()Köpek havlıyor
5Hayvan → Kopek mümkün, derlenir#2 bir Kopek mi? Kedi → Hayvan → Object: hayırClassCastException, k2'ye atama yapılmaz
6DerlenirÇalışmaz—
7DerlenirÇalışmazBitti yazılmaz
YığınHeapmain()h1h2k1k2?Kopek #1Kedi #2JVM denetimi: Kedi bir Kopek mi?KediHayvanObject ✗
Şekil 4.4.6: 5. satırda JVM'in denetimi. h2'nin gösterdiği nesnenin sınıfından yukarı çıkan zincirde (Kedi → Hayvan → Object) Kopek yoktur; dönüşüm başarısız olur ve k2 boş kalır.

Tablonun en önemli satırı 5. satırdır. Derleyici bu satırı 3. satırdan ayırt edemez; ikisinde de bildirilen tip Hayvan, hedef tip Kopek'tir. Fark yalnız çalışma zamanında, nesneye bakılınca ortaya çıkar. JVM'in denetimi Şekil 4.4.6'te görülen zincir yürüyüşüdür: nesnenin sınıfından başlanır ve üst sınıflar sırayla Kopek aranarak dolaşılır. Object'e kadar Kopek bulunmazsa dönüşüm reddedilir.

Programın çıktısı yalnız Köpek havlıyor satırı ve ardından istisna iletisidir. Bitti yazılmaz, çünkü istisna yakalanmadığında main metodu oracıkta sona erer.

4.4.3.5instanceof ile güvenli dönüşüm

Bir referansın hangi sınıftan bir nesneyi gösterdiği önceden bilinmiyorsa aşağı dönüşümden önce sınama yapılır. Java bunun için instanceof işlecini sunar.

Tanım 4.4.8

referans instanceof Tur ifadesi, referansın gösterdiği nesne Tur'den ya da onun bir alt sınıfından ise true, değilse false verir. null instanceof Tur her zaman false'tur. Referansın bildirilen tipiyle Tur arasında hiçbir kalıtım ilişkisi yoksa ifade derlenmez.

Aşağıdaki döngü, hayvanlar = {new Kopek(), new Kedi(), new Hayvan()} dizisindeki her öğe için önce tipi sınar, sonra dönüştürür.

for (Hayvan x : hayvanlar) {
    if (x instanceof Kopek) {
        Kopek kk = (Kopek) x;
        kk.havla();
    } else if (x instanceof Kedi) {
        ((Kedi) x).tirmala();
    } else {
        System.out.println("Özel davranış yok");
    }
}
Köpek havlıyor
Kedi tırmalıyor
Özel davranış yok

Önce sınama, sonra dönüşüm yapıldığı için ClassCastException oluşamaz. x instanceof Kopek doğruysa x'in gösterdiği nesne kesinlikle bir köpektir ve (Kopek) x başarılı olur. Üçüncü öğe düz bir Hayvan'dır; iki sınama da yanlış çıkar ve else dalı çalışır.

Sınamanın sırası da önemlidir. instanceof alt sınıfları da kapsar. x instanceof Hayvan sınaması köpek için de, kedi için de true verir. Bu yüzden zincirde önce en alttaki, en özel sınıflar sınanır. Sıra ters kurulursa genel dal her nesneyi yakalar ve özel dallara hiç sıra gelmez.

Java 16 ile gelen örüntü eşleme (pattern matching) bu kalıbı kısaltır. Sınama doğru çıkarsa dönüştürülmüş değer doğrudan yeni bir değişkene bağlanır:

if (x instanceof Kopek kk) {
    kk.havla(); // dönüşüm otomatik
}

kk değişkeni yalnız if'in doğru dalında tanımlıdır ve tipi Kopek'tir. Dönüşümü derleyici yazar; programcının ayrıca (Kopek) x yazmasına gerek kalmaz. Sınama ile dönüşüm ayrı satırlarda olmadığı için birinin değiştirilip ötekinin unutulması gibi bir hata da yapılamaz.

Aynı döngünün öğe sırası değiştirilmiş bir sürümü izlenirse sınamaların her turda nasıl çalıştığı daha açık görülür.

Hayvan[] dizi = {new Kedi(), new Hayvan(), new Kopek()};
for (Hayvan x : dizi) {
    if (x instanceof Kopek) {
        ((Kopek) x).havla();
    } else if (x instanceof Kedi) {
        ((Kedi) x).tirmala();
    } else {
        System.out.println("Özel davranış yok");
    }
}
Tablo 4.4.5: instanceof zincirinin izlemesi. Dizi: {Kedi, Hayvan, Kopek}.
Turx → nesneinstanceof Kopekinstanceof KediÇalışan dalÇıktı
1Kedi #1falsetrue((Kedi) x).tirmala()Kedi tırmalıyor
2Hayvan #2falsefalseelseÖzel davranış yok
3Kopek #3true— (sınanmaz)((Kopek) x).havla()Köpek havlıyor
dizi•••Kedi #1Hayvan #2Kopek #3x→ Kedi #1x instanceof Kopekfalsex instanceof Keditrue
Şekil 4.4.7: İlk tur. x bir Kedi'yi gösterir; Kopek sınaması yanlış, Kedi sınaması doğru çıkar. Önce sınandığı için (Kedi) x dönüşümü güvenlidir.

İkinci turda düz bir Hayvan nesnesi vardır. Bu nesne hiçbir alt sınıfın örneği değildir; iki sınama da yanlış çıkar. else dalında hiçbir dönüşüm denenmediği için hata riski de yoktur. Üçüncü turda ilk sınama doğru çıkar; else if zincirinde sonraki koşul sınanmaz. Üç turun hiçbirinde ClassCastException oluşmaz.

Uzun instanceof zincirleri ise çoğunlukla bir tasarım uyarısıdır. Yeni bir alt sınıf eklendiğinde (örneğin Kus) zincire yeni bir dal eklenmesi gerekir; unutulursa kuş sessizce else dalına düşer. Daha iyi çözüm, davranışı üst sınıfta tanımlanıp alt sınıflarda geçersiz kılınan bir metoda taşımaktır.

4.4.3.6Diziler ve ArrayStoreException

Referans kuralı dizilere de uygulanır. Kopek bir Hayvan olduğuna göre Kopek[] de bir Hayvan[] sayılır. Java dizilerinin bu özelliğine kovaryans denir.

Tanım 4.4.9

Diziler kovaryanttır (covariant): A, B'nin alt tipiyse A[] de B[]'nin alt tipidir. Bu yüzden Kopek[] nesnesi Hayvan[] referansına atanabilir. Dizi nesnesi ise kendi gerçek öğe tipini bilir. Diziye yazılan her öğe çalışma zamanında bu tiple denetlenir; uymayan öğe için ArrayStoreException fırlatılır.

Kopek[] kopekler = new Kopek[2];
Hayvan[] hayvanlar = kopekler;   // derlenir
hayvanlar[0] = new Kopek();
System.out.println("ilk atama tamam");
hayvanlar[1] = new Kedi();       // derlenir ama...
System.out.println("buraya gelinmez");
ilk atama tamam
Exception in thread "main" java.lang.ArrayStoreException: Kedi

İkinci satırda yukarı dönüşüm yapılır ve derlenir. Dizi nesnesi ise gerçekte bir Kopek[]'dir; hayvanlar yalnız ona bakan ikinci bir referanstır. Üçüncü satırda diziye bir köpek yazılır; JVM'in denetimi geçer ve ilk atama tamam yazılır. Beşinci satırda hayvanlar[1] = new Kedi(); derleyicinin gözünde geçerlidir: Hayvan[] tipindeki bir diziye bir Hayvan yazılır. Ama JVM yazma anında dizinin gerçek öğe tipine bakar. Bir Kopek[] dizisine kedi konamaz; ArrayStoreException: Kedi fırlatılır ve son satır çalışmaz.

Bu durum dizilerin tip güvenliğinde bilinen bir açıktır. Derleyicinin yakalayamadığı bir hatayı JVM her yazmada yaptığı denetimle yakalar. Okuma ise güvenlidir: Kopek[] dizisinden okunan her öğe bir Hayvan'dır. Sorun yalnız yazarken çıkar.

Jenerik koleksiyonlar aynı açığı derleme zamanında kapatır. List<Kopek>, List<Hayvan>'ın alt tipi değildir:

List<Hayvan> l = new ArrayList<Kopek>();
error: incompatible types: ArrayList<Kopek>
  cannot be converted to List<Hayvan>

Derleyici bu atamaya izin verseydi listeye l.add(new Kedi()) ile bir kedi eklenebilirdi. Jenerik tip bilgisi çalışma zamanında tutulmadığı için JVM bunu yakalayamazdı. Java bu yüzden jenerikleri kovaryant yapmaz ve sorunu baştan derleme hatasına çevirir.

Sık yapılan hata

Dizi kovaryansına güvenmek: Hayvan[] d = new Kopek[2]; d[1] = new Kedi(); derlenir, ama çalışırken ArrayStoreException: Kedi verir. Farklı alt sınıflardan nesneler tutacak bir dizi baştan üst sınıf tipinde oluşturulur: new Hayvan[2].

4.4.4Dinamik bağlama

Bir metot çağrısının, çalışacak metot gövdesiyle eşleştirilmesine bağlama denir. Bu eşleştirme iki zamanda yapılabilir. Derleme sırasında yapılırsa statik (erken) bağlama, program çalışırken yapılırsa dinamik (geç) bağlama adını alır. Java'da geçersiz kılınabilen örnek metotları dinamik bağlanır ve çok biçimliliği mümkün kılan da budur.

4.4.4.1Derleyici “var mı?”, JVM “hangisi?” diye sorar

Tanım 4.4.10

Dinamik bağlama (dynamic binding), çağrılacak metot sürümünün değişkenin bildirilen tipine göre değil, nesnenin gerçek sınıfına göre ve çalışma zamanında seçilmesidir. Referans bir alt sınıf nesnesini gösteriyorsa ve alt sınıf metodu geçersiz kılmışsa alt sınıfın sürümü çalışır.

Hayvan h = new Kopek();
h.kos();      // Kopek sınıfının metodu
h.sesCikar(); // Kopek sınıfının metodu
h.uyu();      // Kopek'te yok → Hayvan'ınki
Köpek koşuyor
Hav hav!
Hayvan uyuyor

Üç çağrı da Hayvan tipindeki h üzerinden yapılır. h.kos() ve h.sesCikar() için Kopek'in sürümleri çalışır, çünkü nesne bir köpektir ve bu iki metot Kopek'te geçersiz kılınmıştır. h.uyu() için ise Hayvan'ın sürümü çalışır. Kopek, uyu()'yu geçersiz kılmamıştır; kalıtımla aldığı sürüm Hayvan'ınkidir.

Her çağrı iki aşamada çözülür. Şekil 4.4.8 h.kos() için bu iki aşamayı gösterir.

h.kos()Derleyici: Hayvan'da kos() var mı?evet → derlenir (yoksa hata)JVM: nesne hangi sınıftan?Kopek → Kopek.kos() çalışır
Şekil 4.4.8: h.kos() çağrısının iki aşaması. Derleyici Hayvan'da kos() olup olmadığına bakar; JVM ise nesnenin sınıfına bakar ve Kopek.kos()'u çalıştırır.
  1. Derleme zamanı. Derleyici h'nin bildirilen tipine (Hayvan) bakar ve “Hayvan'da kos() var mı?” diye sorar. Varsa çağrı derlenir; yoksa derleme hatası verilir. Bu aşamada çalışacak gövde seçilmez; yalnız çağrının geçerli olduğu kesinleşir.
  2. Çalışma zamanı. JVM, h'nin gösterdiği nesneye bakar ve “bu nesnenin sınıfında hangi kos() geçerli?” diye sorar. Gerçek sınıf Kopek'tir; Kopek.kos() çalışır.

Kural kısaca şöyle özetlenir: derleyici “var mı?” diye, JVM “hangisi?” diye sorar. Derleyicinin onayı, çalışma zamanında en az bir sürümün bulunacağını garanti eder. Çünkü h hangi alt sınıfın nesnesini gösterirse göstersin, o sınıf Hayvan'ın kos() metodunu ya kalıtımla almış ya da geçersiz kılmıştır.

4.4.4.2Metot araması aşağıdan yukarı

JVM'in “hangisi?” sorusunu nasıl cevapladığı en kolay sınıf hiyerarşisi üzerinde gösterilir. Arama her zaman en alttan, nesnenin gerçek sınıfından başlar. Metot orada bulunursa arama biter. Bulunmazsa bir üstteki sınıfa çıkılır. Bu yürüyüş Object'e kadar sürebilir.

Hayvan h = new Kopek();
h.kos();
h.uyu();
System.out.println(h.toString());
// h.havla(); → derleme hatası
Object+toString(): String+equals(Object): booleanHayvan+sesCikar(): void+kos(): void+uyu(): void✓ bulunduKopek+sesCikar(): void+kos(): void+havla(): void✗ yok, yukarı
Şekil 4.4.9: h.uyu() için metot araması. Nesne bir Kopek'tir; Kopek'te uyu() yoktur. Arama bir üste çıkar ve Hayvan'da biter.

Programdaki dört satırın her biri farklı bir durumu gösterir:

  • h.kos(): arama Kopek'te başlar. kos() orada vardır; arama biter ve Köpek koşuyor yazılır.
  • h.uyu(): Kopek'te uyu() yoktur. Bir üste çıkılır ve metot Hayvan'da bulunur: Hayvan uyuyor (Şekil 4.4.9).
  • h.toString(): ne Kopek'te ne Hayvan'da vardır. Arama en tepeye, Object'e kadar çıkar. Object'in toString()'i sınıf adını, @ işaretini ve onaltılık bir sayıyı (nesnenin karma kodunu) birleştirir; örneğin Kopek@13221655. Sayı çalıştırmaya göre değişir (Şekil 4.4.10).
  • h.havla(): derlenmez. Bu kez aramayı derleyici yapar ve referans tipi Hayvan'dan başlar. Hayvan'da ve Object'te havla() bulunmaz. Kopek'e hiç bakılmaz, çünkü derleyici aşağı inmez.
Object+toString(): String+equals(Object): boolean✓ bulunduHayvan+sesCikar(): void+kos(): void+uyu(): void✗ yokKopek+sesCikar(): void+kos(): void+havla(): void✗ yok
Şekil 4.4.10: h.toString() için metot araması. Kopek'te ve Hayvan'da bulunmayan metot, zincirin tepesindeki Object'te bulunur.

Son madde iki aramanın farkını açıkça gösterir. Derleyicinin araması referans tipinden başlar ve yalnız yukarı gider. JVM'in araması nesnenin sınıfından başlar ve o da yalnız yukarı gider. JVM'in başladığı nokta derleyicininkinden daha aşağıda ya da onunla aynı yerdedir. Bu yüzden derleyicinin bulduğu her metodu JVM de bulur; belki daha aşağıda, geçersiz kılınmış bir sürümünü.

Örnek 4.4.3

Kedi nesnesini gösteren Hayvan c = new Kedi(); için c.sesCikar(), c.kos() ve c.uyu() çağrılarında arama nerede biter?

c.sesCikar(): arama Kedi'de başlar ve orada biter, çünkü Kedi bu metodu geçersiz kılmıştır: Miyav!.

c.kos(): Kedi'de kos() yoktur; arama Hayvan'da biter: Hayvan koşuyor. Aynı çağrı bir köpekte Köpek koşuyor yazar.

c.uyu(): yine Hayvan'da biter: Hayvan uyuyor.

4.4.4.3Kod izleme: çok biçimli dizi

Çok biçimliliğin en sık kullanıldığı yer, üst sınıf tipinde bir dizi ya da listedir. Dizinin öğeleri farklı alt sınıfların nesnelerini gösterir; tek bir döngü hepsine aynı çağrıyı yapar.

Hayvan[] hayvanlar = new Hayvan[3];
hayvanlar[0] = new Kopek();
hayvanlar[1] = new Kedi();
hayvanlar[2] = new Hayvan();
for (int i = 0; i < hayvanlar.length; i++) {
    hayvanlar[i].sesCikar();
}

İlk satır, Hayvan tipinde üç öğeli bir dizi oluşturur. Öğeler nesne değil referanstır ve başlangıçta hepsi null'dır. Sonraki üç atama her öğeye farklı bir nesne bağlar. 0. öğe bir Kopek'i, 1. öğe bir Kedi'yi gösterir; 2. öğe ise düz bir Hayvan'dır. Her atamada yukarı dönüşüm kendiliğinden yapılır. Dizi artık üç farklı biçim tutar.

hayvanlarheap0Kopek nesnesi1Kedi nesnesi2Hayvan nesnesi
Şekil 4.4.11: Döngünün ilk turu. hayvanlar[0] bir Kopek nesnesini gösterir; JVM Kopek.sesCikar()'ı çalıştırır.
Tablo 4.4.6: Çok biçimli dizi döngüsünün izlemesi.
ii < hayvanlar.lengthhayvanlar[i] → nesneÇalışan sürümÇıktı
00 < 3 doğruKopekKopek.sesCikar()Hav hav!
11 < 3 doğruKediKedi.sesCikar()Miyav!
22 < 3 doğruHayvanHayvan.sesCikar()Hayvan sesi
33 < 3 yanlış——döngü biter

Döngü gövdesindeki hayvanlar[i].sesCikar() satırı üç turda da aynıdır. Derleyici bu satırı bir kez, Hayvan.sesCikar() çağrısı olarak derler. JVM ise her turda nesnenin sınıfına bakar ve farklı bir gövde seçer. Tek satır kod, üç farklı davranış üretir. Diziye yarın bir Kus nesnesi eklense döngüde hiçbir değişiklik gerekmez; Kus'un sesCikar() sürümü kendiliğinden çalışır.

Sık yapılan hata

Diziyi oluşturup öğelere nesne atamadan çağrı yapmak. new Hayvan[3] üç hayvan oluşturmaz; üç boş referans oluşturur. Atama yapılmadan hayvanlar[0].sesCikar() çağrılırsa program NullPointerException ile durur.

4.4.4.4Perde arkası: metot tablosu

Hiyerarşide aşağıdan yukarı arama, dinamik bağlamayı anlamak için doğru bir modeldir. JVM'ler ise her çağrıda sınıfları tek tek dolaşmaz; bu yavaş olurdu. Gerçekte arama işi büyük ölçüde sınıf yüklenirken, bir kez yapılır.

Tanım 4.4.11

Metot tablosu (virtual method table), JVM'in dinamik bağlamada kullandığı, sınıf başına bir tane tutulan tablodur. Geçersiz kılınabilir her metodun tabloda sabit bir sıra numarası, yani yuvası vardır. Yuvada, o sınıfın nesnelerinde çalışacak metot gövdesinin adresi durur.

Tablolar kalıtım boyunca şöyle kurulur. Alt sınıfın tablosu üst sınıfınkinin kopyasıyla başlar. Geçersiz kılınan metotların yuvalarına yeni gövde yazılır. Alt sınıfın eklediği yeni metotlar ise tablonun sonuna eklenir. Böylece bir metodun yuva numarası bütün alt sınıflarda aynı kalır. Şekil 4.4.12 örnek hiyerarşinin tablolarını gösterir.

Hayvan tablosu[0] sesCikarHayvan[1] kosHayvan[2] uyuHayvanKopek tablosu[0] sesCikarKopek[1] kosKopek[2] uyuHayvan[3] havlaKopekKedi tablosu[0] sesCikarKedi[1] kosHayvan[2] uyuHayvan[3] tirmalaKedisağ sütun: metodungövdesinin bulunduğu sınıfvurgulu ad: gövde busınıfta yazıldısoluk ad: üstten kopya
Şekil 4.4.12: Hayvan, Kopek ve Kedi sınıflarının metot tabloları. Sağ sütun, metodun gövdesinin bulunduğu sınıftır. Vurgulu ad, gövdenin o sınıfta yazıldığını; soluk ad, üst sınıftan kopyalandığını gösterir.

Şekildeki tablolar sadeleştirilmiştir. Gerçek tablolar Object'in geçersiz kılınabilir metotlarıyla (toString, equals, hashCode gibi) başlar ve yuva numaraları JVM'e göre değişir. İlke ise aynıdır. Kopek tablosunda 0 ve 1 numaralı yuvalar (sesCikar, kos) Kopek'in gövdelerini gösterir. 2 numaralı uyu yuvası Hayvan'dan kopyalanmıştır. havla ise sona eklenmiştir. Kedi tablosunda yalnız sesCikar değişmiş, tirmala sona eklenmiştir.

Derleyicinin ürettiği bayt kodu (ara kod, bytecode) bu yapıyı kullanır. Aşağıdaki iki satır, javap -c Ana komutunun çıktısından alınmıştır. Bu komut, derlenmiş bir sınıfın bayt kodunu okunabilir biçimde gösterir.

47: invokevirtual #15  // Method Hayvan.kos:()V
52: invokevirtual #18  // Method Hayvan.sesCikar:()V

İki satırda da derleyici yalnız Hayvan.kos ve Hayvan.sesCikar yazmıştır, çünkü denetimi referans tipine göre yapmıştır. Kopek ya da Kedi adı bayt kodunda geçmez. Hangi gövdenin çalışacağına invokevirtual komutu karar verir. Bu komut çalıştığında JVM şu yolu izler: referans → nesne → nesnenin sınıfı → o sınıfın tablosundaki ilgili yuva → gövde. Arama tek bir dizi erişimidir; hiyerarşide yukarı yürümek gerekmez.

Bütün çağrılar invokevirtual ile yapılmaz. Yapıcılar ve super.m() çağrıları invokespecial, static metotlar invokestatic komutuyla çağrılır. Bu komutlarda çalışacak gövde derleme zamanında bellidir ve tabloya bakılmaz. private örnek metotları Java 11'den beri invokevirtual ile çağrılır. Ama geçersiz kılınamadıkları için tek adayları vardır ve JVM onu doğrudan seçer.

Hayvan[] d = {new Kopek(), new Kedi()};
for (Hayvan h : d) {
    h.kos();
    h.sesCikar();
}
Tablo 4.4.7: Metot tablosuyla çağrının izlemesi. Bayt kodu her çağrıda aynıdır; yalnız nesnenin tablosu değişir.
ÇağrıBayt koduNesneTablodaki yuvaÇıktı
h.kos()Hayvan.kos, yuva 1Kopek #1Kopek.kosKöpek koşuyor
h.sesCikar()Hayvan.sesCikar, yuva 0Kopek #1Kopek.sesCikarHav hav!
h.kos()Hayvan.kos, yuva 1Kedi #2Hayvan.kos (kalıtımla)Hayvan koşuyor
h.sesCikar()Hayvan.sesCikar, yuva 0Kedi #2Kedi.sesCikarMiyav!
Yığınhmain()Kopek #1sınıf →Kopek tablosu[0] sesCikarKopek[1] kosKopek[2] uyuHayvan[3] havlaKopekinvokevirtual Hayvan.kos → yuva [1]Kopek tablosu [1] = Kopek.kos
Şekil 4.4.13: İlk h.kos() çağrısı. Nesne bir Kopek'tir; onun tablosunun 1. yuvasında Kopek.kos vardır.
Yığınhmain()Kedi #2sınıf →Kedi tablosu[0] sesCikarKedi[1] kosHayvan[2] uyuHayvan[3] tirmalaKediinvokevirtual Hayvan.kos → yuva [1]Kedi tablosu [1] = Hayvan.kos (kalıtımla)
Şekil 4.4.14: Aynı bayt kodu, başka nesne. Kedi, kos'u geçersiz kılmadığı için tablosunun 1. yuvası Hayvan.kos'u gösterir.

Programın dört satırlık çıktısı tablonun son sütunudur. Dört çağrının hiçbirinde if ya da tip sınaması yapılmamıştır; seçimi tablolar yapmıştır. Şekil 4.4.13 ile Şekil 4.4.14 karşılaştırıldığında farkın yalnız nesnenin bağlı olduğu tabloda olduğu görülür. Yol, yuva numarası ve bayt kodu aynıdır.

Bu yapı dinamik bağlamanın maliyetinin de küçük olduğunu gösterir. Bir sanal çağrı, düz bir çağrıya göre yalnız bir iki bellek okuması fazladır. Modern JVM'ler çoğu durumda bunu da ortadan kaldırır. Bir çağrı noktasında hep aynı sınıftan nesneler görülüyorsa çalışma sırasında derleyen bileşen (JIT) çağrıyı doğrudan o gövdeye bağlayabilir. Bu yüzden çok biçimlilikten başarım kaygısıyla kaçınmak gerekmez.

4.4.4.5Neler dinamik bağlanmaz?

Dinamik bağlama yalnız geçersiz kılınabilen örnek metotları için geçerlidir. Alanlar, static metotlar, private ve final metotlar statik bağlanır: hangisinin kullanılacağına derleyici, referansın bildirilen tipine bakarak karar verir.

class Hayvan {
    String tur = "hayvan";

    static void bilgi() {
        System.out.println("Hayvan.bilgi()");
    }

    void tanit() {
        System.out.println("Ben bir " + tur);
    }
}

class Kopek extends Hayvan {
    String tur = "köpek"; // gizler, ezmez

    static void bilgi() {
        System.out.println("Kopek.bilgi()");
    }

    @Override
    void tanit() {
        System.out.println("Ben bir " + tur);
    }
}

Bu iki sınıfta iki farklı durum vardır. Kopek, tur adında yeni bir alan tanımlar. Bu alan Hayvan'ın tur alanını ezmez, gizler: bir Kopek nesnesinde iki ayrı tur alanı bulunur. Kopek ayrıca aynı imzalı bir static bilgi() metodu tanımlar. static metotlar da geçersiz kılınmaz, gizlenir. tanit() ise sıradan bir örnek metodudur ve gerçekten geçersiz kılınmıştır.

Tanım 4.4.12

Alt sınıfta üst sınıftakiyle aynı adlı bir alan ya da aynı imzalı bir static metot tanımlanmasına gizleme (hiding) denir. Gizlenen üye silinmez; nesnede ya da sınıfta varlığını sürdürür. Hangisinin kullanılacağı referansın bildirilen tipine göre derleme zamanında belirlenir.

Hayvan h = new Kopek();
System.out.println(h.tur);
h.bilgi();
h.tanit();
hayvan
Hayvan.bilgi()
Ben bir köpek
Tablo 4.4.8: Hangi üye neye göre seçilir?
ÜyeNeye göre seçilir?Bağlama
Örnek metoduNesne tipiDinamik
Alan (değişken)Referans tipiStatik
static metotReferans tipiStatik
private / final metotGeçersiz kılınamaz; tek aday vardırStatik

final metot alt sınıfta geçersiz kılınamaz; denenirse derleyici overridden method is final der. private metot ise alt sınıfa hiç görünmez. Alt sınıf aynı adla bir metot yazarsa bu yeni ve ilgisiz bir metottur. Bu iki durumda da çağrı için tek aday vardır ve seçim derleme zamanında bellidir.

static metot bir nesneye değil, sınıfa aittir. Java, h.bilgi() gibi bir referans üzerinden çağrıya izin verir, ama bu yazım yanıltıcıdır. Derleyici çağrıyı sessizce Hayvan.bilgi()'ye çevirir; referansın gösterdiği nesneye hiç bakılmaz. Referans null olsa bile çağrı çalışır ve NullPointerException çıkmaz. Bu yüzden static metot her zaman sınıf adıyla çağrılır: Kopek.bilgi(). Alt sınıfta üst sınıftakiyle aynı adlı alan tanımlamaktan da kaçınılır.

4.4.4.6Kod izleme: alan gizleme ve static

Aynı fikir, değer döndüren metotlarla yazılmış kısa bir programda adım adım izlenebilir.

class Hayvan {
    String tur = "hayvan";
    static String bilgi() { return "Hayvan.bilgi"; }
    String tanit() { return "Ben bir " + tur; }
}
class Kopek extends Hayvan {
    String tur = "köpek";            // gizler
    static String bilgi() { return "Kopek.bilgi"; }
    String tanit() { return "Ben bir " + tur; }
}
Hayvan h = new Kopek();   // main
System.out.println(h.tur);
System.out.println(h.bilgi());
System.out.println(h.tanit());
System.out.println(((Kopek) h).tur);
hayvan
Hayvan.bilgi
Ben bir köpek
köpek
YığınHeapmain()hKopek #1Hayvan.tur"hayvan"Kopek.tur"köpek"bir nesne, iki ayrı tur alanı
Şekil 4.4.15: Bir nesne, iki ayrı tur alanı. Kopek'teki tur, Hayvan'dakini ezmez; ikisi de nesnede yer alır.
Tablo 4.4.9: Alan gizleme programının izlemesi. h'nin bildirilen tipi Hayvan, gösterdiği nesne Kopek'tir.
İfadeÜye türüSeçim ölçütüSeçilenÇıktı
h.turAlanBildirilen tip HayvanHayvan.turhayvan
h.bilgi()static metotBildirilen tip HayvanHayvan.bilgi()Hayvan.bilgi
h.tanit()Örnek metoduGerçek tip KopekKopek.tanit(); içinde tur = Kopek.turBen bir köpek
((Kopek) h).turAlanBildirilen tip Kopek (dönüşümle)Kopek.turköpek

Üçüncü satır ayrıca dikkat ister. h.tanit() dinamik bağlanır ve Kopek.tanit() çalışır. Bu metodun içindeki tur ifadesi de bir alan erişimidir ve o da statik bağlanır. Ama bu kez bildirilen tip, metodun yazıldığı sınıftır. Kopek sınıfında yazılan kodda tur, this.tur anlamına gelir ve this'in tipi Kopek'tir. Bu yüzden Kopek.tur okunur ve Ben bir köpek yazılır.

YığınHeapKopek.tanit()thismain()hKopek #1Hayvan.tur"hayvan"Kopek.tur"köpek"örnek metodu → gerçek tip (Kopek)
Şekil 4.4.16: h.tanit() çağrısı. Örnek metodu gerçek tipe göre seçilir; Kopek.tanit() içindeki this, Kopek tipindedir ve Kopek.tur'u okur.

Karşılaştırma için tanit() metodunun Kopek'te geçersiz kılınmadığı düşünülsün. O zaman h.tanit(), Hayvan'daki gövdeyi çalıştırır. Bu gövde Hayvan sınıfında yazıldığı için içindeki tur, Hayvan.tur'dur ve çıktı Ben bir hayvan olur. Nesne yine bir köpektir. Alan gizlemenin kafa karıştırıcı yanı budur: aynı nesnede, aynı adla iki değer vardır ve hangisinin okunacağı kodun nerede yazıldığına bağlıdır.

Son satırda dönüşüm, ifadenin bildirilen tipini Kopek yapar. Aynı nesnenin diğer alanı okunur: köpek. Nesne değişmemiştir; değişen yalnız derleyicinin baktığı tiptir.

Sık yapılan hata

Alanların da geçersiz kılındığını sanmak. h.tur her zaman referans tipine göre okunur. Alt sınıfa göre değişen bir bilgi gerekiyorsa alan private yapılır ve bilgiyi döndüren bir metot (getter) yazılır. Metot geçersiz kılınabilir; alan kılınamaz.

4.4.4.7Yapıcıdan geçersiz kılınabilir metot çağırmak

Dinamik bağlama nesne kurulurken de çalışır. Bu, beklenmedik bir tuzak doğurur: üst sınıfın yapıcısı, alt sınıfın henüz hazırlanmamış alanlarını kullanan bir metodu çalıştırabilir.

class Bilesen {
    Bilesen() {
        System.out.println("Bilesen kuruluyor");
        ciz();
    }
    void ciz() { System.out.println("boş bileşen"); }
}
class Etiket extends Bilesen {
    private String metin = "Merhaba";
    Etiket() { System.out.println("Etiket kuruldu"); }
    @Override void ciz() {
        System.out.println("Etiket: " + metin);
    }
}
Etiket e = new Etiket();   // main
e.ciz();
Bilesen kuruluyor
Etiket: null
Etiket kuruldu
Etiket: Merhaba

Çıktının ikinci satırı şaşırtıcıdır. metin alanına tanımında "Merhaba" değeri verilmiştir, ama Etiket: null yazılır. Nedeni, nesne kurulurken işlerin yapıldığı sıradır.

Tablo 4.4.10: new Etiket() çağrısının izlemesi.
AdımÇalışan kodmetinÇıktı
1new nesneye yer ayırır; alanlar varsayılan değeri alırnull—
2Etiket()'in ilk işi gizli super() çağrısıdırnull—
3Bilesen() gövdesi, ilk satırnullBilesen kuruluyor
4ciz() = this.ciz(); nesne bir Etiket → Etiket.ciz()nullEtiket: null
5super() döndü; alt sınıfın alan ilk değerleri çalışır"Merhaba"—
6Etiket() gövdesi"Merhaba"Etiket kuruldu
7main: e.ciz()"Merhaba"Etiket: Merhaba

Kritik nokta 4. ve 5. adımların sırasıdır. Java'da bir alt sınıf yapıcısı önce üst sınıf yapıcısını çalıştırır. Alt sınıfın alanlarına tanımlarında verilen ilk değerler ise üst sınıf yapıcısı döndükten sonra atanır. Bilesen()'deki ciz() çağrısı dinamik bağlanır; nesnenin sınıfı Etiket olduğu için geçersiz kılınmış sürüm seçilir. O anda metin hâlâ varsayılan değeri olan null'dadır. Alt sınıfın metodu yarı kurulmuş bir nesne üzerinde çalışır.

Yığın (stack)HeapEtiket.ciz()Bilesen()Etiket()main()e?Etiket #1metinnullEtiket.ciz() seçildi, metin = null
Şekil 4.4.17: 4. adım. Yığında Etiket(), onun üstünde Bilesen() ve en üstte Etiket.ciz() vardır. Heap'teki nesnenin metin alanı henüz null'dır.
Yığın (stack)HeapEtiket()main()e?Etiket #1metin"Merhaba"super() döndü → alan ilk değerleri
Şekil 4.4.18: 5. adım. super() döndükten sonra alan ilk değeri çalışır ve metin'e "Merhaba" atanır. ciz() ise çoktan çağrılmıştır.

Bu örnekte metot alanı yalnız yazdırdığı için zarar küçüktür. Metot alanı kullansaydı, örneğin metin.length() çağırsaydı, program çökerdi:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "this.metin" is null
	at Etiket.ciz(Ana.java:21)
	at Bilesen.<init>(Ana.java:16)
	at Etiket.<init>(Ana.java:19)

JDK 15'ten beri NullPointerException iletisi varsayılan olarak hangi ifadenin null olduğunu da söyler: "this.metin" is null. İzleme listesindeki (stack trace) satırların sırası da durumu özetler. En üstte hatanın çıktığı Etiket.ciz, altında onu çağıran Bilesen yapıcısı, onun altında da Etiket yapıcısı vardır. <init>, bayt kodunda yapıcının adıdır. Zincir, Etiket'in yapıcısından başlayıp yine Etiket'in bir metoduna dönen bir döngü çizer.

Tanım 4.4.13

Yapıcı kuralı: Bir yapıcıdan yalnız private, final ya da static metotlar çağrılmalıdır. Bu metotlar geçersiz kılınamaz; bu yüzden alt sınıfın henüz kurulmamış durumuna erişemezler.

Sık yapılan hata

Üst sınıf yapıcısında “şablon” niyetiyle geçersiz kılınabilir bir metot çağırmak (ciz(), baslat(), yukle() gibi). Alt sınıf bu metodu geçersiz kılarsa metot, alt sınıfın alanları atanmadan çalışır. Sonuç null, 0 ya da false değerleriyle yanlış çıktı veya NullPointerException'dır. Böyle bir hazırlık işi gerekiyorsa nesne kurulduktan sonra ayrıca çağrılır.

4.4.5Aşırı yükleme

Aşırı yükleme (method overloading), aynı adlı ama parametre listesi farklı birden çok metot tanımlamaktır. Metotlar konusundan tanıdık bir araçtır: Math.abs'ın int, long, float ve double sürümleri vardır; println'in on sürümü vardır. Kalıtım ve dinamik bağlamayla birlikte kullanıldığında ise aşırı yüklemenin bir özelliği önem kazanır: seçim derleme zamanında yapılır.

4.4.5.1Aşırı yükleme derleme zamanında seçilir

static void yazdir(Hayvan h) {
    System.out.println("yazdir(Hayvan)");
}

static void yazdir(Kopek k) {
    System.out.println("yazdir(Kopek)");
}

public static void main(String[] args) {
    Kopek kopek = new Kopek();
    Hayvan hayvan = kopek;
    yazdir(kopek);
    yazdir(hayvan);
    yazdir((Kopek) hayvan);
}
yazdir(Kopek)
yazdir(Hayvan)
yazdir(Kopek)

Programın üç çağrısı da aynı nesneyle yapılır. Yine de ikinci çağrı farklı bir sürüm seçer. İlk çağrıda argüman kopek'tir ve bildirilen tipi Kopek'tir; iki sürüm de uyar, ama yazdir(Kopek) daha özeldir. İkinci çağrıda argüman hayvan'dır. Nesne bir köpek olsa da hayvan'ın bildirilen tipi Hayvan'dır ve derleyici yalnız bunu bilir. Hayvan tipindeki bir argüman yazdir(Kopek)'e uymaz; tek aday yazdir(Hayvan)'dır. Üçüncü çağrıda açık dönüşüm ifadenin bildirilen tipini Kopek yapar ve yine yazdir(Kopek) seçilir.

Tanım 4.4.14

Aşırı yüklenmiş sürümler arasındaki seçimi derleyici, argümanların bildirilen tiplerine göre yapar. Seçilen imza bayt koduna yazılır ve program çalışırken değişmez. Argümanların gösterdiği nesnelerin gerçek sınıfı bu seçimi etkilemez.

Bu kural, geçersiz kılmanın kuralının tam tersidir. Geçersiz kılmada nesnenin gerçek tipi belirleyicidir; aşırı yüklemede argümanın bildirilen tipi. Tablo 4.4.11 iki mekanizmayı yan yana koyar.

Tablo 4.4.11: Aşırı yükleme ile geçersiz kılmanın karşılaştırması.
Aşırı yüklemeGeçersiz kılma
Nerede?Aynı sınıf (ya da sınıf ve alt sınıfı)Alt sınıf
İmzaFarklı parametre listesiAynı imza
Seçimi yapanDerleyiciJVM
ÖlçütArgümanın bildirilen tipiAlıcı nesnenin gerçek tipi
ZamanDerleme zamanıÇalışma zamanı

Tablodaki “alıcı nesne” ifadesi ayrıca açıklanmalıdır. a.m(b) çağrısında a alıcıdır: metodun hangi nesne üzerinde çağrıldığını belirtir. b ise argümandır. Java'da dinamik bağlama yalnız alıcının tipine bakar. Argümanların tipine bakan çalışma zamanı seçimi yoktur; argümanlar için her şey derleme zamanında biter. Bu yüzden Java'nın dinamik bağlamasına tek yönlü seçim (single dispatch) denir.

Bir ayrıntı da dönüş tipiyle ilgilidir. Metot imzası yalnız ad ve parametre tiplerinden oluşur; dönüş tipi imzaya dahil değildir. Bu yüzden yalnız dönüş tipi farklı iki metot aşırı yüklenemez. static int g(int x) ve static long g(int x) aynı sınıfta yazılırsa derleyici method g(int) is already defined der.

4.4.5.2En özel sürüm ve belirsizlik

Bir argüman birden çok sürüme uyabilir. Bu durumda derleyici en özel olanı seçer.

Tanım 4.4.15

İki aday metottan birinin her parametre tipi, ötekinin karşılık gelen parametre tipinin alt tipiyse (ya da aynısıysa), o metot daha özeldir. Derleyici uyan adaylar arasından en özel olanı seçer. Tek bir en özel aday yoksa çağrı belirsizdir ve derleme hatası verir.

static void goster(Object o) { System.out.println("goster(Object)"); }
static void goster(Hayvan h) { System.out.println("goster(Hayvan)"); }
static void goster(String s) { System.out.println("goster(String)"); }
// main içinde:
goster(new Kedi());
goster("a");
Object o = "a";
goster(o);
goster((Hayvan) null);
goster(Hayvan)
goster(String)
goster(Object)
goster(Hayvan)

Dört çağrıyı sırayla inceleyelim. goster(new Kedi()): kedi hem Hayvan'a hem Object'e uyar, String'e uymaz. Hayvan, Object'in alt tipidir, dolayısıyla daha özeldir: goster(Hayvan). goster("a"): argüman String ve Object sürümlerine uyar; String daha özeldir. goster(o): o'nun gösterdiği nesne bir dizgi olsa da bildirilen tipi Object'tir. Object tipindeki bir argüman yalnız goster(Object)'e uyar.

Dördüncü çağrı özel bir değer kullanır. null her referans tipine uyar; bu yüzden goster(null) yazıldığında üç sürüm de aday olur. Object en genel olanıdır ve elenir. Hayvan ile String arasında ise kalıtım ilişkisi yoktur; hiçbiri ötekinden özel değildir. Derleyici seçim yapamaz:

error: reference to goster is ambiguous
  both method goster(Hayvan) in Ana and
  method goster(String) in Ana match

Çözüm, ifadenin bildirilen tipini açık dönüşümle belirlemektir: goster((Hayvan) null). (Hayvan) null ifadesinin değeri yine null'dır, ama tipi Hayvan'dır. Artık String sürümü uymaz ve goster(Hayvan) seçilir. Tablodaki dördüncü çıktı satırı budur.

Uyan sürümler tek bir kalıtım zinciri üzerindeyse belirsizlik olmaz. goster(Object), goster(Hayvan) ve goster(Kedi) varken goster(null) derlenir ve en alttakini, goster(Kedi)'yi seçer. Çünkü Kedi, öteki iki tipin de alt tipidir. Belirsizlik ancak birbirinin alt tipi olmayan iki aday aynı anda uyduğunda çıkar.

Sık yapılan hata

İlişkisiz iki aşırı yükleme varken null geçirmek: goster(null) → reference to goster is ambiguous. Hangi sürümün istendiği açık dönüşümle belirtilir: goster((String) null) ya da goster((Hayvan) null).

4.4.5.3Kod izleme: derleyici mi, JVM mi?

En öğretici durum, iki mekanizmanın aynı çağrıda birlikte çalıştığı durumdur. Aşağıdaki programda yazdir aşırı yüklenmiştir; her sürümün içinde de geçersiz kılınmış sesCikar() çağrılır.

static void yazdir(Hayvan h) {
    System.out.print("yazdir(Hayvan): ");
    h.sesCikar();
}
static void yazdir(Kopek k) {
    System.out.print("yazdir(Kopek): ");
    k.sesCikar();
}
// main içinde:
Hayvan hayvan = new Kopek();
Hayvan diger = new Kedi();
yazdir(hayvan);
yazdir(diger);
yazdir((Kopek) hayvan);
yazdir(Hayvan): Hav hav!
yazdir(Hayvan): Miyav!
yazdir(Kopek): Hav hav!
Tablo 4.4.12: Her çağrıda iki karar. Derleyici yazdir'ın sürümünü, JVM sesCikar()'ın sürümünü seçer.
ÇağrıArgümanın bildirilen tipiDerleyicinin seçimiNesneJVM'in seçimiÇıktı
yazdir(hayvan)Hayvanyazdir(Hayvan)KopekKopek.sesCikar()yazdir(Hayvan): Hav hav!
yazdir(diger)Hayvanyazdir(Hayvan)KediKedi.sesCikar()yazdir(Hayvan): Miyav!
yazdir((Kopek) hayvan)Kopekyazdir(Kopek)KopekKopek.sesCikar()yazdir(Kopek): Hav hav!
Derleyicibildirilen tipe bakarJVMnesnenin sınıfına bakarargüman tipi: Hayvanyazdir(Hayvan)yazdir(Kopek)nesne: KopekKopek.sesCikar()Kedi.sesCikar()derleme zamanıçalışma zamanı
Şekil 4.4.19: İlk çağrıda iki karar. Derleyici bildirilen tipe (Hayvan) bakıp yazdir(Hayvan)'ı seçmiştir; metodun içinde JVM nesneye (Kopek) bakıp Kopek.sesCikar()'ı seçer.

İlk iki çağrıda değişkenlerin bildirilen tipi aynıdır, gösterdikleri nesneler farklıdır. Derleyici iki çağrıda da aynı sürümü, yazdir(Hayvan)'ı seçer. Metodun içindeki h.sesCikar() ise geçersiz kılınmış bir metottur ve JVM her seferinde nesneye bakar: önce Hav hav!, sonra Miyav!. Üçüncü çağrıda açık dönüşüm ifadenin bildirilen tipini değiştirir ve derleyici yazdir(Kopek)'u seçer.

İzlemenin özeti tek cümledir: aşırı yükleme seçimi bildirilen tiple derlemede, geçersiz kılma seçimi gerçek tiple çalışırken yapılır. Bir çağrının ne yazdıracağını bulmak için iki soru sırayla sorulur. Önce “derleyici hangi imzayı seçer?”, sonra “o imzanın hangi sınıftaki gövdesi çalışır?”.

4.4.5.4Aşırı yükleme seçiminin üç aşaması

Argüman ilkel bir değer olduğunda seçim bir adım daha karmaşıklaşır. Bir int, long'a genişletilebilir; Integer'a kutulanabilir; int... parametresine de verilebilir. Derleyici bu yolları aynı anda değil, sırayla dener.

Tanım 4.4.16

Derleyici aşırı yüklenmiş sürümleri üç aşamada arar:

  1. Kutulamasız aşama. Yalnız tam eşleşme, ilkel genişletme (int → long) ve başvuru genişletmesi (String → Object) denenir.
  2. Kutulama aşaması. 1. aşamada aday yoksa otomatik kutulama ve kutudan çıkarma da denenir: double → Double → Object.
  3. Değişken sayıda parametre aşaması. Hâlâ aday yoksa int... gibi değişken sayıda parametreli sürümler devreye girer.

Bir aşamada en az bir aday bulunursa sonraki aşamalara geçilmez. O aşamanın adayları arasından en özel olan seçilir.

Kuralın en çok şaşırtan sonucu şudur: f(long) ve f(Integer) varken f(5) çağrısı f(long)'u seçer. Integer, int'in kendi sarmalayıcı sınıfıdır ve ilk bakışta daha yakın görünür. Ama ona ulaşmak için kutulama gerekir; long'a ise genişletmeyle, 1. aşamada ulaşılır. Genişletmenin kutulamadan önce gelmesinin nedeni tarihseldir. Kutulama Java 5'te eklendi. O zamana kadar f(5), f(long)'u çağırıyordu. Yeni kural eski programların davranışını değiştirmesin diye kutulama ikinci aşamaya kondu.

Daraltma (long → int, double → long) hiçbir aşamada denenmez. Üç aşamada da aday bulunmazsa derleyici no suitable method found der. Örneğin yalnız yazdir(Hayvan) ve yazdir(Kopek) varken yazdir(true) yazılırsa ileti no suitable method found for yazdir(boolean) olur.

Bu sıra derleme zamanında uygulanır ve seçilen imza bayt koduna yazılır. Seçilen imzanın hangi sınıftaki gövdesinin çalışacağı ise yine çalışma zamanında belirlenir. Üç aşama yalnız “hangi imza?” sorusunu cevaplar.

static void f(long x)    { p("f(long)"); }
static void f(Integer x) { p("f(Integer)"); }
static void f(Object x)  { p("f(Object)"); }
static void f(int... x)  { p("f(int...)"); }
// main içinde (p: System.out.println):
byte b = 1;
f(5);
f(b);
f(Integer.valueOf(5));
f("x");
f(2.5);
f();
f(1, 2);
f(long)
f(long)
f(Integer)
f(Object)
f(Object)
f(int...)
f(int...)
Tablo 4.4.13: Yedi çağrının aşama aşama çözümü.
ÇağrıArgüman tipi1. aşama adayları2. aşama3. aşamaSeçilen
f(5)intf(long) (genişletme)——f(long)
f(b)bytef(long) (genişletme)——f(long)
f(Integer.valueOf(5))Integerf(Integer), f(Object)——f(Integer) (daha özel)
f("x")Stringf(Object)——f(Object)
f(2.5)doubleyokf(Object) (Double'a kutulama)—f(Object)
f()—yokyokf(int...)f(int...)
f(1, 2)int, intyokyokf(int...)f(int...)
Argüman: 5 (int)1. kutulamasız2. kutulama3. değişken sayıdaf(long)✓ int → long genişletmef(Integer)✗ kutulama gerekirf(Object)✗ kutulama gerekirf(int...)1. aşamada tek aday: f(long)
Şekil 4.4.20: f(5) için aşamalar. 1. aşamada yalnız genişletmeyle long uyar; Integer ve Object kutulama ister ve bu aşamada denenmez.

Tablonun birkaç satırı ayrıca açıklanmalıdır. f(b)'de argüman byte'tır ve o da long'a genişler. Kutulansaydı bile Byte olurdu, Integer olmazdı; Byte ile Integer arasında hiçbir ilişki yoktur. f(Integer.valueOf(5))'te argüman zaten bir nesnedir. 1. aşamada iki aday vardır: tam eşleşmeyle f(Integer) ve başvuru genişletmesiyle f(Object). Integer, Object'ten özel olduğu için seçilir. f(long) ise kutudan çıkarma gerektirir ve bu aşamada denenmez.

Argüman: 2.5 (double)1. kutulamasız2. kutulama3. değişken sayıdaf(long)✗ double → long daraltmaf(Integer)✗ Double ≠ Integerf(Object)✓ double → Double → Objectf(int...)1. aşama boş → 2. aşamada kutulama
Şekil 4.4.21: f(2.5) için aşamalar. double, long'a daraltılamaz; 1. aşama boş kalır. 2. aşamada Double'a kutulanır ve Object'e uyar.

f(2.5) satırı daraltmanın hiç denenmediğini gösterir. double'dan long'a geçmek bilgi kaybettirir; derleyici bunu kendiliğinden yapmaz. 1. aşama boş kalınca 2. aşamaya geçilir. 2.5, Double'a kutulanır. Double, Integer'a uymaz, ama Object'e uyar. Son iki çağrıda sıfır ya da iki argüman vardır. Bu sayıları yalnız int... parametresi karşılar ve ona en son, 3. aşamada başvurulur.

Örnek 4.4.4

Aynı dört sürüm (f(long), f(Integer), f(Object), f(int...)) için short s = 3; f(s);, f('A'); ve f(Long.valueOf(4)); hangi sürümleri seçer?

f(s): short, long'a genişler; 1. aşamada tek aday f(long)'dur.

f('A'): char da bir tam sayı tipidir ve long'a genişler: f(long). Karakterin kodu, 65, long olarak verilir.

f(Long.valueOf(4)): argüman bir Long nesnesidir. f(long) kutudan çıkarma ister ve 1. aşamada denenmez. Long, Integer'a uymaz. 1. aşamada başvuru genişletmesiyle tek aday f(Object)'tir.

Sık yapılan hata

Kutulamanın önce geldiğini sanmak. f(long) ve f(Integer) varken f(5), f(Integer)'ı değil f(long)'u çağırır: genişletme 1. aşamada, kutulama 2. aşamadadır.

4.4.6Kalıtım ve çok biçimlilik

Çok biçimliliğin asıl değeri, programın tasarımında görülür. Çok biçimli kod, tip sınayan koda göre daha kısa ve yeni sınıflara karşı daha dayanıklıdır. Örneklerin çoğu bir şekil hiyerarşisi üzerine kuruludur.

4.4.6.1Kalıtım ile çok biçimliliğin ilişkisi

İki kavramın ilişkisi tek yönlüdür. Çalışma zamanı çok biçimliliği için kalıtım gereklidir: bir sınıf hiyerarşisi olmadan geçersiz kılma da, dinamik bağlama da olmaz. Kalıtımı kullanmak için ise çok biçimlilik gerekli değildir. Bir sınıf yalnız kodu yeniden kullanmak için de başka bir sınıftan türetilebilir. Kalıtım hiyerarşiyi kurar; çok biçimlilik o hiyerarşiden yararlanır.

B sınıfı A'nın alt sınıfıysa B'nin nesnesi A'nın nesnesi gibi davranabilir; tersi doğru değildir. A referansını B referansına atamak için daraltma dönüşümü gerekir ve dönüşüm parantezle yazılır.

A objA = new B(); // B, A gibi davranır
B objB = (B) objA; // geçerli, ancak
                   // dikkatli kullanılmalı
A objAB nesnesi(B) objA → ✓ başarılıA objAA nesnesi(B) objA → ✗ ClassCastException
Şekil 4.4.22: (B) objA dönüşümünün iki olası sonucu. objA bir B nesnesini gösteriyorsa dönüşüm başarılı olur; düz bir A nesnesini gösteriyorsa ClassCastException fırlatılır.

Son satır derlenir. Çalışırken objA gerçekten bir B nesnesini göstermiyorsa ClassCastException fırlatılır. Koddaki “dikkatli kullanılmalı” yorumu bu riski anlatır.

Java'da çok biçimliliğin tek kaynağı sınıf kalıtımı değildir. Arayüzler (interface) de aynı mekanizmayı sağlar. Bir arayüzü uygulayan (implements) her sınıfın nesnesi, o arayüz tipindeki bir referansa atanabilir. Bir sınıf yalnız bir üst sınıftan türeyebilir, ama birçok arayüzü uygulayabilir. Hayvan h yerine SesCikarabilen s gibi bir arayüz tipiyle de aynı dinamik bağlama çalışır.

4.4.6.2Şekil hiyerarşisi

Bir çizim programında kareler, dikdörtgenler ve çemberler bulunsun. Her şeklin bir adı vardır; her şeklin alanı ve çevresi hesaplanabilir; her şekil çizilebilir. Ama alan ve çevre her şekilde farklı formülle bulunur. Bu ortak yapı soyut bir üst sınıfta toplanır.

abstract class Sekil {
    private String isim;

    public Sekil(String isim) {
        this.isim = isim;
    }

    public String getIsim() {
        return isim;
    }

    public abstract double alanHesapla();

    public abstract double cevreHesapla();

    public void ciz() {
        System.out.println(isim + " çiziliyor.");
    }
}
Sekil-isim: String+getIsim(): String+alanHesapla(): double+cevreHesapla(): double+ciz(): voidKare-kenar: double+alanHesapla()+cevreHesapla()+ciz()Dikdortgen-genislik: double-yukseklik: double+alanHesapla()+cevreHesapla()Cember-yaricap: double+alanHesapla()+cevreHesapla()+ciz()
Şekil 4.4.23: Soyut Sekil sınıfı ve alt sınıfları Kare, Dikdortgen, Cember. Eğik yazılan ad ve metotlar soyuttur. Dikdortgen, ciz()'i geçersiz kılmaz.

Sekil sınıfının başındaki abstract sözcüğü sınıfın soyut olduğunu belirtir. Soyut bir sınıftan doğrudan nesne oluşturulamaz; new Sekil("x") yazılırsa derleyici Sekil is abstract; cannot be instantiated der. alanHesapla() ve cevreHesapla() da soyut metotlardır: gövdeleri yoktur ve her somut alt sınıf onları yazmak zorundadır. ciz() ise gövdeli, sıradan bir metottur. Alt sınıflar onu olduğu gibi kullanabilir ya da geçersiz kılabilir. isim alanı private'tır; alt sınıflar ona yapıcı ve getIsim() üzerinden ulaşır.

Soyut sınıf çok biçimlilik için ideal bir üst sınıftır. “Bir şeklin alanı” sorusunun genel bir cevabı yoktur; cevabı ancak somut şekil verebilir. Soyut metot bu durumu dile getirir: “her şeklin alanı vardır, ama nasıl hesaplanacağını alt sınıf söyler.”

class Kare extends Sekil {
    private double kenar;
    public Kare(double kenar) {
        super("Kare");
        this.kenar = kenar;
    }
    @Override
    public double alanHesapla() {
        return kenar * kenar;
    }
    @Override
    public double cevreHesapla() {
        return 4 * kenar;
    }
    @Override
    public void ciz() {
        System.out.println("Kare çiziliyor: kenar " + kenar);
    }
}

Kare'nin yapıcısı super("Kare") ile ismi üst sınıfa verir ve kenar uzunluğunu kendi alanında tutar. Üç metodu da geçersiz kılar. Kare soyut metotları yazmasaydı kendisi de soyut sayılır ve derlenmezdi.

class Dikdortgen extends Sekil {
    private double genislik;
    private double yukseklik;
    public Dikdortgen(double genislik, double yukseklik) {
        super("Dikdörtgen");
        this.genislik = genislik;
        this.yukseklik = yukseklik;
    }
    @Override
    public double alanHesapla() {
        return genislik * yukseklik;
    }
    @Override
    public double cevreHesapla() {
        return 2 * (genislik + yukseklik);
    }
    // ciz() ezilmedi → Sekil'inki kullanılır
}

Dikdortgen alanı ve çevreyi geçersiz kılar, ciz()'i ise geçersiz kılmaz. Bir dikdörtgen çizildiğinde Sekil.ciz() çalışır ve ismi kullanarak Dikdörtgen çiziliyor. yazar. Kodun sonundaki yorum bunu hatırlatır.

class Cember extends Sekil {
    private double yaricap;
    public Cember(double yaricap) {
        super("Çember");
        this.yaricap = yaricap;
    }
    @Override
    public double alanHesapla() {
        return Math.PI * yaricap * yaricap;
    }
    @Override
    public double cevreHesapla() {
        return 2 * Math.PI * yaricap;
    }
    @Override
    public void ciz() {
        System.out.println("Çember çiziliyor: yarıçap " + yaricap);
    }
}

Cember, alanı ve çevreyi Math.PI sabitiyle hesaplar ve ciz()'i kendi bilgisiyle, yarıçapı da yazarak geçersiz kılar.

4.4.6.3Kod izleme: şekiller dizisi

Sekil[] sekiller = new Sekil[3];
sekiller[0] = new Kare(5);
sekiller[1] = new Dikdortgen(4, 6);
sekiller[2] = new Cember(7);

for (Sekil sekil : sekiller) {
    System.out.println(sekil.getIsim()
        + " alanı: " + sekil.alanHesapla());
    sekil.ciz();
}
Kare alanı: 25.0
Kare çiziliyor: kenar 5.0
Dikdörtgen alanı: 24.0
Dikdörtgen çiziliyor.
Çember alanı: 153.93804002589985
Çember çiziliyor: yarıçap 7.0

Dizi Sekil tipindedir; öğeler üç farklı alt sınıfın nesnelerini gösterir. Döngü her turda aynı iki çağrıyı yapar: sekil.alanHesapla() ve sekil.ciz(). getIsim() ise hiçbir alt sınıfta geçersiz kılınmamıştır; her turda Sekil'in sürümü çalışır ve yapıcıda verilen ismi döndürür.

Tablo 4.4.14: Şekiller dizisi döngüsünün izlemesi.
TurNesnealanHesapla()Alanciz()
1Kare(5)Kare: 5 × 525.0Kare.ciz()
2Dikdortgen(4, 6)Dikdortgen: 4 × 624.0Sekil.ciz() (geçersiz kılınmamış)
3Cember(7)Cember: π × 7 × 7153.93804002589985Cember.ciz()
sekillerheap0Kare (5)1Dikdortgen (4, 6)2Cember (7)
Şekil 4.4.24: İkinci tur. sekiller[1] bir Dikdortgen'i gösterir. Dikdortgen, ciz()'i geçersiz kılmadığı için arama yukarı çıkar ve Sekil.ciz() çalışır.

Çemberin alanı 49π'dir. double aritmetiği bu değeri 153.93804002589985 olarak hesaplar ve println bütün anlamlı basamakları yazar. Daha okunur bir çıktı isteniyorsa printf("%.2f", ...) kullanılır.

4.4.6.4Neden çok biçimlilik?

Çok biçimliliğin değeri, onsuz yazılmış kodla karşılaştırınca görülür. Aşağıdaki metot bir şeklin alanını tip sınayarak bulur.

static double alanKotu(Sekil s) {
    double alan;
    if (s instanceof Kare) {
        alan = ((Kare) s).alanHesapla();
    } else if (s instanceof Dikdortgen) {
        alan = ((Dikdortgen) s).alanHesapla();
    } else if (s instanceof Cember) {
        alan = ((Cember) s).alanHesapla();
    } else {
        alan = 0; // yeni tür unutuldu!
    }
    return alan;
}

Metot doğru çalışır, ama kırılgandır. Hiyerarşiye yeni bir şekil eklendiğinde bu metot da değişmelidir. Bir DikUcgen extends Sekil sınıfı yazılıp metot güncellenmezse alanKotu(new DikUcgen(3, 4)) hiçbir hata vermeden 0.0 döndürür. Programdaki her instanceof zinciri, yeni sınıf eklendiğinde unutulabilecek bir yer demektir. Üstelik metottaki dönüşümler gereksizdir: alanHesapla() zaten Sekil'de tanımlıdır ve dönüşüm yapılmadan da çağrılabilir.

static double toplamAlan(Sekil[] dizi) {
    double toplam = 0;
    for (Sekil sekil : dizi) {
        // her nesne kendi alanını
        toplam += sekil.alanHesapla();
    }
    return toplam;
}

Çok biçimli sürümde hiçbir tip sınaması yoktur. Her nesne kendi alanını kendisi hesaplar. Yeni bir DikUcgen extends Sekil sınıfı eklemek yeter; toplamAlan hiç değişmez. Örneğin {Kare(5), Dikdortgen(4, 6), DikUcgen(3, 4)} dizisi için sonuç 25 + 24 + 6 = 55.0'dır. Kod kısalır ve ClassCastException riski ortadan kalkar.

Tanım 4.4.17

Açık/kapalı ilkesi (open/closed principle): Bir yazılım birimi genişletmeye açık, değiştirmeye kapalı olmalıdır. Yeni davranış, var olan kodu değiştirerek değil, yeni kod (örneğin yeni bir alt sınıf) ekleyerek kazandırılır.

toplamAlan bu ilkeye uyar: yeni şekillerle genişler, ama kendisi değişmez. alanKotu ise ilkeyi çiğner. Büyük programlarda fark çok daha belirgindir. Bir şekil düzenleyicide alan, çevre, çizim, kaydetme, yazdırma gibi onlarca işlem olabilir. Tip sınayan tasarımda yeni bir şekil bu işlemlerin hepsine dokunmayı gerektirir; çok biçimli tasarımda yalnız yeni sınıf yazılır.

Örnek 4.4.5

Şekil hiyerarşisine DikUcgen(a, b) eklensin: alan = a·b/2, çevre = a + b + hipotenüs. ciz() geçersiz kılınmasın ve yapıcı super("Dik üçgen") çağırsın. DikUcgen(3, 4) için alan, çevre ve ciz() çıktısı nedir?

Alan: 3 · 4 / 2 = 6.0. Hipotenüs: √(3² + 4²) = √25 = 5, çevre: 3 + 4 + 5 = 12.0. ciz() geçersiz kılınmadığı için Sekil.ciz() çalışır: Dik üçgen çiziliyor.

Şekil döngüsünde hiçbir şey değişmez; yalnız diziye yeni nesne eklenir. Çevre için kodda a + b + Math.sqrt(a * a + b * b) yazılır.

4.4.6.5Çok biçimli parametre ve dönüş değeri

Çok biçimlilik yalnız dizilerde değil, metot parametrelerinde ve dönüş değerlerinde de kullanılır.

class Veteriner {
    void muayene(Hayvan h) {     // her alt sınıf
        System.out.print("Muayene: ");
        h.sesCikar();
    }
    static Hayvan sahiplen(String tur) { // gizli
        if (tur.equals("kedi")) return new Kedi();
        return new Kopek();
    }
}
// main içinde:
Veteriner v = new Veteriner();
v.muayene(new Kedi());
Hayvan h = Veteriner.sahiplen("köpek");
v.muayene(h);
System.out.println(h.getClass().getSimpleName());
Muayene: Miyav!
Muayene: Hav hav!
Kopek

muayene metodunun parametresi Hayvan tipindedir. Bu yüzden metot, bugün var olan ve ileride yazılacak her alt sınıfın nesnesini kabul eder. İlk çağrıda bir kedi, ikinci çağrıda bir köpek muayene edilir; metodun içindeki h.sesCikar() her seferinde nesnenin kendi sürümünü çalıştırır. Hiyerarşiye yeni bir Kus sınıfı eklendiğinde muayene değişmez.

sahiplen metodunun dönüş tipi de Hayvan'dır. Metot, hangi alt sınıfı ürettiğini çağırandan gizler. Çağıran yalnız Hayvan sözleşmesine güvenir: dönen nesne ses çıkarabilir, koşabilir, uyuyabilir. sahiplen ileride başka bir alt sınıf döndürmeye başlasa kullanan kod etkilenmez. Son satırdaki getSimpleName(), nesnenin gerçek sınıfının paket adı olmadan yazılmış adını verir: Kopek.

Tanım 4.4.18

Fabrika metodu (factory method), nesneyi üretip üst sınıf ya da arayüz tipinde döndüren metottur. Çağıran, hangi somut sınıfın üretildiğini bilmek zorunda kalmaz.

Bu örnekten genel bir ilke çıkar: parametre ve değişken tipleri, işi görmeye yeten en genel tip olarak seçilir. muayene yalnız sesCikar() çağırdığı için Hayvan parametresi yeterlidir; Kopek parametresi gereksiz yere kediyi dışarıda bırakırdı. Java kütüphanesinde de aynı ilke görülür: değişkenler ArrayList<String> yerine çoğunlukla List<String> tipinde tanımlanır.

4.4.6.6Tanksavar oyunu

Çok biçimliliğin bir oyundaki kullanımı daha gerçekçi bir örnektir. Swing ile yazılmış bir tanksavar oyununda ekranın üstünden tanklar düşer; oyuncu ekranın altındaki tanksavarla onlara top atar. Skor 10'un katı olunca tanksavarın türü değişir ve farklı bir top atmaya başlar.

Tanksavarx, y: intrenk: Color+topAt(): TopTS Jawelin+topAt(): TopTS Tow2A+topAt(): TopToptopKonum: Rectanglerenk: ColorTop37mmTop57mm
Şekil 4.4.25: Tanksavar oyununun iki hiyerarşisi. Soyut Tanksavar sınıfının topAt() metodunu iki alt sınıf geçersiz kılar; Top sınıfının da Top37mm ve Top57mm alt sınıfları vardır.

Şekildeki “TS Jawelin” TanksavarJawelin, “TS Tow2A” ise TanksavarTow2A sınıfıdır. Tank sınıfının da TankT90MS ve TankLeopard2A4 alt sınıfları vardır. Oyun döngüsü ise yalnız Tanksavar ve Tank tiplerini bilir.

Tanksavar tanksavar = new TanksavarJawelin(x, y);
...
// hangi top? nesneye bağlı (dinamik bağlama)
toplar.add(tanksavar.topAt());
...
if (tanksavar instanceof TanksavarJawelin) {
    tanksavar = new TanksavarTow2A(tanksavar.x, tanksavar.y);
}

toplar.add(tanksavar.topAt()) satırı hangi topun üretileceğini bilmez. topAt() soyut bir metottur; çağrı dinamik bağlanır ve o anda tanksavar hangi sınıftansa onun sürümü çalışır. Dönüş tipi Top'tur; dönen nesne Top37mm ya da Top57mm olabilir. toplar listesi de Top tipinde nesneler tutar ve hepsini aynı döngüyle hareket ettirip çizer.

Koddaki instanceof sınaması ise başka bir iş görür: tanksavarı yükseltmek. Sınama yalnız yükseltme anında bir kez yapılır; her top atışında yapılmaz. Yeni tanksavar nesnesi eskisinin konumunu (x, y) alır ve tanksavar değişkeni artık onu gösterir. Değişkenin bildirilen tipi değişmemiştir; değişen yalnız gösterdiği nesnedir. Bu andan sonra aynı topAt() çağrısı başka bir gövdeyi çalıştırır.

Top t1 = tanksavar.topAt(); // Jawelin
// → Top37mm, yükseklik 30
tanksavar = new TanksavarTow2A(100, 540);
Top t2 = tanksavar.topAt();
// → Top57mm, yükseklik 40

Oyundan bağımsız bir sınama çıktısı bunu doğrular. Aynı topAt() çağrısı, tanksavar değişince farklı bir top üretir: Jawelin yüksekliği 30 olan bir Top37mm, Tow2A yüksekliği 40 olan bir Top57mm atar. Oyun kodunda topun türüne göre hiçbir if gerekmez. Yeni bir tanksavar türü eklemek için yalnız yeni bir alt sınıf ve yeni bir top sınıfı yazılır.

4.4.6.7equals ve kalıtım: simetri tuzağı

Object'in metotları da dinamik bağlanır. Object o = k1; System.out.println(o); yazıldığında println, o.toString()'i çağırır ve nesnenin kendi toString'i çalışır. o.equals(x) de nesnenin kendi equals'ını çalıştırır. Bu, koleksiyonların her sınıfla doğru çalışmasını sağlar. ArrayList.contains, HashSet.add gibi metotlar yalnız Object tipini bilir, ama öğelerin kendi equals ve hashCode metotlarını çağırır.

equals metodunun uyması gereken kurallar vardır. Bunlardan biri simetridir: x.equals(y) doğruysa y.equals(x) de doğru olmalıdır. Kalıtım işin içine girince bu kural kolayca bozulur.

class Nokta {
    final int x, y;
    Nokta(int x, int y) { this.x = x; this.y = y; }
    @Override public boolean equals(Object o) {
        if (!(o instanceof Nokta)) return false;
        Nokta n = (Nokta) o;
        return x == n.x && y == n.y;
    }
    @Override public int hashCode() { return 31 * x + y; }
}
class RenkliNokta extends Nokta {
    final String renk;
    RenkliNokta(int x, int y, String renk) {
        super(x, y);
        this.renk = renk;
    }
    @Override public boolean equals(Object o) {
        if (!(o instanceof RenkliNokta)) return false;
        RenkliNokta r = (RenkliNokta) o;
        return super.equals(r) && renk.equals(r.renk);
    }
}

Nokta, iki noktayı koordinatları eşitse eşit sayar. RenkliNokta ise buna renk koşulunu ekler ve karşı tarafın da bir RenkliNokta olmasını ister. İki sınıf ayrı ayrı bakıldığında makuldür. Birlikte kullanıldıklarında sorun çıkar:

Nokta p = new Nokta(1, 2);
RenkliNokta k =
    new RenkliNokta(1, 2, "kırmızı");
System.out.println(p.equals(k)
    + " " + k.equals(p));
true false

Hangi equals'ın çalışacağını alıcı nesne belirler. p.equals(k) çağrısında alıcı p'dir ve Nokta'nın metodu çalışır. k bir RenkliNokta, dolayısıyla bir Nokta'dır; koordinatlar eşittir ve sonuç true'dur. k.equals(p) çağrısında ise alıcı k'dir ve RenkliNokta'nın metodu çalışır. p bir RenkliNokta değildir; sonuç false'tur. Simetri bozulmuştur.

Bu bozukluk koleksiyonlarda tutarsız davranışa yol açar. k'yi içeren bir listede contains(p) true verir, çünkü liste p.equals(k)'yi çağırır. p'yi içeren bir listede contains(k) ise false verir. Bir HashSet'e önce p, sonra k eklenirse kümede 2 öğe olur. Önce k, sonra p eklenirse 1 öğe olur. Kümenin içeriği, öğelerin eklenme sırasına bağlı hâle gelmiştir.

Tablo 4.4.15: Simetri bozukluğunun koleksiyonlardaki sonuçları. p = Nokta(1, 2), k = RenkliNokta(1, 2, "kırmızı").
İşlemİçeride çağrılanSonuç
[k].contains(p)p.equals(k)true
[p].contains(k)k.equals(p)false
HashSet: önce p, sonra kk.equals(p)2 öğe
HashSet: önce k, sonra pp.equals(k)1 öğe

Sorunun iki bilinen çözümü vardır. Birincisi, iki sınıfta da instanceof yerine getClass() karşılaştırması kullanmaktır. Böylece yalnız tam aynı sınıftan nesneler eşit sayılır ve p.equals(k) de false olur:

// Nokta'da:
@Override public boolean equals(Object o) {
    if (o == null || getClass() != o.getClass()) return false;
    Nokta n = (Nokta) o;
    return x == n.x && y == n.y;
}
// RenkliNokta'da:
@Override public boolean equals(Object o) {
    if (!super.equals(o)) return false;   // sınıf ve koordinat
    return renk.equals(((RenkliNokta) o).renk);
}

Bu çözümle p.equals(k) ve k.equals(p) ikisi de false verir; iki aynı renkli nokta ise eşit çıkar. Bedeli şudur: Nokta'nın, hiçbir alan eklemeyen sıradan bir alt sınıfı bile artık Nokta'ya eşit sayılmaz.

İkinci çözüm kalıtım yerine oluşumdur (composition). RenkliNokta, Nokta'dan türemez; bir Nokta alanı tutar ve ayrıca bir renk taşır. İki sınıf arasında is-a ilişkisi kalmadığı için birbirleriyle karşılaştırılmaları da anlamını yitirir. Bir renkli noktanın konumu gerekirse konum() gibi bir metotla Nokta olarak alınır.

class RenkliNokta {              // extends yok
    private final Nokta konum;
    private final String renk;
    RenkliNokta(int x, int y, String renk) {
        this.konum = new Nokta(x, y);
        this.renk = renk;
    }
    Nokta konum() { return konum; }
    @Override public boolean equals(Object o) {
        if (!(o instanceof RenkliNokta)) return false;
        RenkliNokta r = (RenkliNokta) o;
        return konum.equals(r.konum) && renk.equals(r.renk);
    }
    @Override public int hashCode() { return 31 * konum.hashCode() + renk.hashCode(); }
}
Sık yapılan hata

equals'ı alt sınıfta tek yönlü genişletmek. Alt sınıf equals'a yeni bir koşul eklerken üst sınıf eski kuralı korursa p.equals(k) true, k.equals(p) false olur. Simetri bozulur ve koleksiyonlar tutarsızlaşır.

4.4.7Çözümlü problemler

Aşağıdaki problemler, notta anlatılan kuralları birlikte kullanmayı gerektirir. Her problemde önce kod verilir, ardından çözüm adım adım açıklanır. Bütün çıktılar JDK 21 ile çalıştırılarak doğrulanmıştır.

Örnek 4.4.6

Problem 1: Çıktı nedir? cagir() yalnız A'da tanımlıdır ve içinden yaz()'ı çağırır. B ise yalnız yaz()'ı geçersiz kılar.

class A {
    void yaz() {
        System.out.println("A.yaz");
    }

    void cagir() {
        yaz(); // this.yaz()
    }
}

class B extends A {
    @Override
    void yaz() {
        System.out.println("B.yaz");
    }
}

// main içinde:
A a = new B();
a.cagir();
new A().cagir();

a.cagir(): a'nın gösterdiği nesne bir B'dir. B'de cagir() yoktur; arama yukarı çıkar ve A.cagir() çalışır. Bu metodun içindeki yaz() aslında this.yaz()'dır. this, metodun çağrıldığı nesnedir, yani bir B nesnesi. this.yaz() da dinamik bağlanır ve B.yaz() çalışır: B.yaz.

new A().cagir(): nesne bir A'dır; A.cagir() içindeki this.yaz(), A.yaz()'ı çalıştırır: A.yaz.

B.yaz
A.yaz

Problemin dersi şudur: üst sınıfın kodu, alt sınıfın geçersiz kıldığı metodu farkında olmadan çağırabilir. A'yı yazan programcı cagir()'ın A.yaz()'ı çağıracağını düşünmüş olabilir. Ama this üzerinden yapılan her çağrı dinamik bağlanır.

Örnek 4.4.7

Problem 2: Derlenir mi, çalışır mı? Her satır için derleme hatası, çalışma zamanı hatası ya da sorunsuz (çıktısıyla) kararı verilecek.

Hayvan h = new Kedi();
Kedi k1 = h;              // (1)
Kedi k2 = (Kedi) h;       // (2)
Kopek k3 = (Kopek) h;     // (3)
h.tirmala();              // (4)
((Kedi) h).tirmala();     // (5)
Object o = new Kopek();
o.sesCikar();             // (6)
((Hayvan) o).sesCikar();  // (7)
#SonuçNeden
1Derleme hatasıHayvan → Kedi için açık dönüşüm gerekir: incompatible types: Hayvan cannot be converted to Kedi.
2SorunsuzNesne gerçekten bir Kedi'dir.
3Çalışma zamanı hatasıDerlenir, ama nesne kedidir: ClassCastException.
4Derleme hatasıHayvan'da tirmala() yoktur: cannot find symbol.
5SorunsuzKedi tırmalıyor yazar.
6Derleme hatasıObject'te sesCikar() yoktur.
7SorunsuzDinamik bağlama: Hav hav! yazar.

Çözümün yöntemi her satırda aynıdır. Önce derleyicinin sorusu sorulur: bildirilen tiplere göre atama ya da çağrı geçerli mi? Geçerliyse JVM'in sorusu sorulur: nesneye bakıldığında dönüşüm başarılı mı, hangi sürüm çalışır? 1. ve 4. satırda derleyici h'nin Hayvan olduğunu bilir, kedi olduğunu bilmez. 7. satırda dönüşüm yukarı doğrudur ve nesne köpektir; Kopek.sesCikar() çalışır.

Örnek 4.4.8

Problem 3: Aşırı yükleme ve geçersiz kılma. Ust'te iki aşırı yüklenmiş f vardır; Alt yalnız f(Object)'i geçersiz kılar. İki çağrı ne yazdırır?

class Ust {
    void f(Object o) {
        System.out.println("Ust.f(Object)");
    }

    void f(String s) {
        System.out.println("Ust.f(String)");
    }
}

class Alt extends Ust {
    @Override
    void f(Object o) {
        System.out.println("Alt.f(Object)");
    }
}

// main içinde:
Ust u = new Alt();
Object o = "merhaba";
u.f(o);
u.f("merhaba");

u.f(o): önce derleyici sürümü seçer. u'nun bildirilen tipi Ust'tür; adaylar f(Object) ve f(String)'dir. o'nun bildirilen tipi Object'tir; yalnız f(Object) uyar. Gösterdiği nesnenin bir dizgi olması bu seçimi değiştirmez. Çalışırken alıcı nesne bir Alt'tır ve f(Object)'i geçersiz kılmıştır: Alt.f(Object).

u.f("merhaba"): argüman String'dir; iki sürüm de uyar, f(String) daha özeldir. Alt bu sürümü geçersiz kılmadığı için Ust.f(String) çalışır.

Alt.f(Object)
Ust.f(String)

Sıra her zaman aynıdır: önce aşırı yükleme (derleme zamanı, bildirilen tip), sonra geçersiz kılma (çalışma zamanı, nesne tipi).

Örnek 4.4.9

Problem 4: Kurulum, aşama, dizi. Aşağıdaki sınıflar için new Alt(); ve ardından new Alt().goster(); ne yazdırır?

class Ust {
    Ust() { goster(); }
    void goster() { System.out.println("Ust"); }
}
class Alt extends Ust {
    private int[] dizi = {1, 2, 3};
    @Override void goster() {
        System.out.println(dizi == null
            ? "dizi yok" : "uzunluk " + dizi.length);
    }
}

Her new Alt() sırasında önce Ust() çalışır ve goster()'i çağırır. Çağrı dinamik bağlanır; Alt.goster() çalışır. O anda dizi alanına ilk değeri henüz atanmamıştır ve null'dır: dizi yok. İkinci satırda yeni bir nesne oluşturulur ve aynı şey yeniden olur: yine dizi yok. Ardından kurulmuş nesne üzerinde goster() çağrılır; artık dizi hazırdır: uzunluk 3. Çıktı üç satırdır: dizi yok, dizi yok, uzunluk 3.

Aynı problemin ikinci sorusu: Object[] d = new String[1]; d[0] = 1; derlenir mi, çalışır mı? Derlenir: Object[] referansına String[] atanabilir ve 1, Integer'a kutulanıp Object olarak yazılabilir. Çalışırken ise dizi gerçekte String[]'dir: ArrayStoreException: java.lang.Integer.

Örnek 4.4.10

Problem 5: Maaşlar. Calisan sınıfının maas() metodu temel maaşı döndürür. Mudur bunu temel × 1,5 ile, Stajyer sabit 5000 ile geçersiz kılar. Calisan[] = {Ayşe 20000, Müdür Mehmet 30000, Stajyer Can} için toplam maaş nedir?

class Mudur extends Calisan {
    Mudur(String ad, double temelMaas) { super(ad, temelMaas); }
    @Override double maas() { return temelMaas * 1.5; }
}
class Stajyer extends Calisan {
    Stajyer(String ad) { super(ad, 0); }
    @Override double maas() { return 5000; }
}
// main içinde:
Calisan[] cs = { new Calisan("Ayşe", 20000), new Mudur("Mehmet", 30000),
                 new Stajyer("Can") };
double toplam = 0;
for (Calisan c : cs) toplam += c.maas();
System.out.println(toplam);   // 70000.0

Döngü yalnız Calisan tipini bilir; her turda c.maas() nesnenin kendi sürümünü çalıştırır. Ayşe düz bir çalışandır: 20000. Mehmet müdürdür: 30000 × 1,5 = 45000. Can stajyerdir: 5000. Toplam 20000 + 45000 + 5000 = 70000.0'dır. Sonuç double olduğu için ondalık noktayla yazılır.

Örnek 4.4.11

Problem 6: X, Y, Z. X'te ad() "X" döndürür, yaz() println(ad()) yapar. Y extends X yalnız ad()'ı "Y" döndürecek biçimde geçersiz kılar. Z extends Y yalnız yaz()'ı println("Z:" + ad()) olarak geçersiz kılar. {new X(), new Y(), new Z()} dizisinde her öğe için yaz() çağrılırsa çıktı nedir?

X: X.yaz() → X.ad(): X. Y: Y'de yaz() yoktur, X.yaz() çalışır; ama içindeki ad(), this bir Y olduğu için Y.ad()'dır: Y. Z: Z.yaz() çalışır; Z'de ad() yoktur, arama Y'de biter: Z:Y.

X
Y
Z:Y
Örnek 4.4.12

Problem 7: instanceof zincirini kaldırmak. instanceof zincirli döngü (Kopek → havla, Kedi → tırmala, diğer → "Özel davranış yok") hiç instanceof kullanmadan nasıl yazılır?

Hayvan'a void ozelDavranis() metodu eklenir ve "Özel davranış yok" yazar. Kopek onu havla()'yı çağıracak, Kedi de tirmala()'yı çağıracak biçimde geçersiz kılar. Döngü tek satıra iner:

// Hayvan'da:
void ozelDavranis() { System.out.println("Özel davranış yok"); }
// Kopek'te:
@Override void ozelDavranis() { havla(); }
// Kedi'de:
@Override void ozelDavranis() { tirmala(); }
// döngü:
for (Hayvan x : hayvanlar) {
    x.ozelDavranis();
}

Çıktı aynı üç satırdır. Yeni bir alt sınıf eklendiğinde döngü değişmez; yeni sınıf isterse ozelDavranis()'i geçersiz kılar, istemezse varsayılan davranışı alır.

Örnek 4.4.13

Problem 8: Object referansıyla çağrı. Object o = new Kopek(); ardından o.sesCikar(); neden derlenmez? yazdir(Hayvan) ve yazdir(Kopek) varken yazdir(o) derlenir mi?

Object'te sesCikar() yoktur: cannot find symbol … location: variable o of type Object. Düzeltme: ((Hayvan) o).sesCikar();. Nesne köpek olduğu için Hav hav! yazar.

yazdir(o) da derlenmez. o'nun bildirilen tipi Object'tir ve iki sürüm de Object kabul etmez: no suitable method found for yazdir(Object). yazdir((Kopek) o) ile derlenir ve yazdir(Kopek) seçilir.

4.4.8Sık yapılan hatalar

Çok biçimlilikle ilgili hatalar iki gruba ayrılır. Derleme hataları, derleyicinin yalnız bildirilen tipe bakmasından doğar ve iletileri açıktır. Çalışma zamanı ve mantık hataları ise derleyiciden geçer; ya istisnayla ya da sessizce yanlış sonuçla ortaya çıkar. 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.

Tablo 4.4.16: Çok biçimlilikle ilgili sık derleme hataları.
Kodjavac iletisiNeden / çözüm
Kopek k = new Hayvan();incompatible types: Hayvan cannot be converted to KopekHer hayvan köpek değildir; ters atama yapılamaz.
Kopek k = hayvan;incompatible types: Hayvan cannot be converted to KopekNesne köpek olsa bile açık dönüşüm gerekir: (Kopek) hayvan.
hayvan.havla();cannot find symbol … variable hayvan of type HayvanReferans tipinde metot yok; önce aşağı dönüşüm gerekir.
(Kopek) h.havla();not a statementParantez eksik: ((Kopek) h).havla();
(Kedi) kopekincompatible types: Kopek cannot be converted to KediKardeş sınıflar arasında dönüşüm yoktur.
@Override void sescikar()method does not override or implement a method from a supertypeAd yanlış yazılmıştır (büyük/küçük harf).
void sesCikar() (üstte public)attempting to assign weaker access privileges; was publicGeçersiz kılan metot erişimi daraltamaz.
goster(null) (ilişkisiz iki sürüm)reference to goster is ambiguousAçık dönüşümle seçilir: goster((Hayvan) null).
List<Hayvan> l = new ArrayList<Kopek>();incompatible types: ArrayList<Kopek> cannot be converted to List<Hayvan>Jenerikler kovaryant değildir.
Tablo 4.4.17: Çok biçimlilikle ilgili sık çalışma zamanı ve mantık hataları.
HataSonuçDüzeltme
Denetimsiz aşağı dönüşüm: (Kopek) h, oysa h bir kedi olabilirClassCastExceptionÖnce instanceof ile sınanır.
“Dönüşüm nesneyi değiştirir” sanmak((Hayvan) kopek).sesCikar() yine Hav hav! yazarDönüşüm yalnız bakılan tipi değiştirir.
Alanların da geçersiz kılındığını sanmakh.tur referans tipine göre okunur: hayvanAlan private yapılır, getter metodu geçersiz kılınır.
Aşırı yüklemeyi geçersiz kılma sanmak: equals(Kisi k)Koleksiyonlar bu metodu çağırmaz; Object.equals çalışırİmza equals(Object) olur; @Override hatayı yakalar.
== ile nesne karşılaştırmakİçerik değil referans karşılaştırılırGeçersiz kılınmış equals kullanılır.
Her yerde instanceof zinciriYeni alt sınıf eklendikçe sessizce yanlış sonuçDavranış geçersiz kılınan bir metoda taşınır.
Yapıcıdan geçersiz kılınabilir metot çağırmaknull çıktısı ya da NullPointerExceptionYapıcıdan yalnız private, final, static metot çağrılır.
Dizi kovaryansına güvenmek: Hayvan[] d = new Kopek[2]; d[1] = new Kedi();ArrayStoreException: KediDizi üst sınıf tipinde oluşturulur.
Kutulamanın önce geldiğini sanmakf(5) → f(long), f(Integer) değilGenişletme 1. aşamadadır.
equals'ı alt sınıfta tek yönlü genişletmekp.equals(k) true, k.equals(p) falsegetClass() karşılaştırması ya da oluşum.

Tablodaki equals(Kisi k) hatası ayrıca açıklanmalıdır. Object'teki metodun imzası equals(Object)'tir. Alt sınıfta equals(Kisi k) yazmak onu geçersiz kılmaz; aynı adla yeni bir aşırı yükleme ekler. Kisi tipinde iki değişkenle yapılan a.equals(b) çağrısında yeni metot seçilir ve doğru çalışıyor gibi görünür. Ama ArrayList.contains ya da HashSet, argümanı Object tipinde verir. Derleyici o kodda yalnız equals(Object)'i görür; Kisi onu geçersiz kılmadığı için Object'in referans karşılaştırması çalışır. Metodun başına @Override yazılsaydı derleyici method does not override or implement a method from a supertype der ve hata baştan yakalanırdı.

4.4.9Özet

  • Çok biçimlilik, aynı ifadenin farklı nesnelerde farklı davranışa yol açmasıdır. Kod üst sınıf tipiyle bir kez yazılır; her nesne çağrıya kendi sürümüyle cevap verir.
  • Derleme zamanı çok biçimliliği aşırı yüklemeyle, çalışma zamanı çok biçimliliği geçersiz kılma ve dinamik bağlamayla sağlanır. İlkini derleyici, ikincisini JVM seçer.
  • Her referans değişkeninin bir bildirilen tipi, bir de gösterdiği nesnenin gerçek tipi vardır. Hangi metotların çağrılabileceğine ve hangi atamaların geçerli olduğuna bildirilen tip; hangi sürümün çalışacağına gerçek tip karar verir.
  • Referans kuralı: üst sınıf referansı alt sınıf nesnesini gösterebilir; tersi derlenmez. Hayvan h = new Kopek(); geçerli, Kopek k = new Hayvan(); hatalıdır.
  • Yukarı dönüşüm otomatik ve her zaman güvenlidir. Aşağı dönüşüm açıkça yazılır; nesne uygun değilse çalışma zamanında ClassCastException fırlatılır. Kardeş sınıflar arasındaki dönüşüm hiç derlenmez.
  • Nokta işleci dönüşümden önce uygulanır: doğru yazım ((Kopek) h).havla()'dır.
  • instanceof, dönüşümden önce nesnenin tipini sınar. Java 16'dan beri örüntü eşleme sınama ile dönüşümü birleştirir: if (x instanceof Kopek kk).
  • Diziler kovaryanttır; yanlış tipte öğe yazmak ArrayStoreException verir. Jenerikler kovaryant değildir ve aynı hatayı derleme zamanında engeller.
  • Metot araması nesnenin gerçek sınıfından başlar ve yukarı çıkar. JVM bunu sınıf başına tutulan metot tablolarıyla, tek bir dizi erişimiyle yapar; bayt kodunda yalnız referans tipinin metodu yazılıdır (invokevirtual).
  • Alanlar, static, private ve final metotlar dinamik bağlanmaz. Alt sınıftaki aynı adlı alan ya da static metot üst sınıftakini gizler, geçersiz kılmaz.
  • Yapıcıdan çağrılan geçersiz kılınabilir metot, alt sınıfın alanları atanmadan çalışır. Yapıcıdan yalnız private, final ya da static metotlar çağrılır.
  • Aşırı yükleme seçimi argümanların bildirilen tipine göre derleme zamanında yapılır. Birden çok sürüm uyarsa en özel olan seçilir; tek bir en özel sürüm yoksa çağrı belirsizdir.
  • Aşırı yükleme üç aşamada çözülür: kutulamasız (genişletme), kutulama, değişken sayıda parametre. Bir aşamada aday bulunursa sonrakilere geçilmez; daraltma hiç denenmez.
  • Çok biçimli kod genişletmeye açık, değiştirmeye kapalıdır: yeni bir alt sınıf eklemek için var olan döngüler ve metotlar değişmez. Parametre ve değişken tipleri işi görmeye yeten en genel tip olarak seçilir.
  • equals alt sınıfta tek yönlü genişletilirse simetri bozulur. Çözüm getClass() karşılaştırması ya da kalıtım yerine oluşumdur.

4.4.10Kavram listesi

4.4.11Kendini Dene

Sorularda örnek hiyerarşi kullanılır: Kopek, sesCikar() ve kos()'u; Kedi yalnız sesCikar()'ı geçersiz kılar.

  1. Hayvan h = new Kedi(); h.kos(); h.sesCikar(); ne yazdırır?

    Cevap

    Hayvan koşuyor ve Miyav!. kos() için arama Kedi'de başlar; Kedi bu metodu geçersiz kılmadığı için Hayvan'da biter. sesCikar() ise Kedi'de bulunur.

  2. Object o = new Kedi(); sonrasında Hayvan h = (Hayvan) o;, Kedi c = (Kedi) o;, Kopek k = (Kopek) o; ve Kedi c2 = o; satırlarının her biri için sonuç nedir?

    Cevap

    İlk üç satır derlenir; Object'ten her sınıfa aşağı dönüşüm yazılabilir. Çalışırken ilk ikisi başarılıdır, çünkü nesne bir kedidir ve kedi bir hayvandır. Üçüncüsü ClassCastException verir. Dördüncü satır hiç derlenmez: açık dönüşüm yoktur (incompatible types: Object cannot be converted to Kedi).

  3. class U { int x = 1; int getX() { return x; } } ve class V extends U { int x = 2; int getX() { return x; } } verilsin. U u = new V(); için u.x + " " + u.getX() + " " + ((V) u).x ne verir?

    Cevap

    1 2 2. u.x alan erişimidir ve bildirilen tipe (U) göre okunur: 1. u.getX() dinamik bağlanır; V.getX() içindeki x, V'nin alanıdır: 2. Dönüşümden sonra bildirilen tip V'dir: 2.

  4. static void g(Object o) "O", static void g(String s) "S" yazdırsın. Object a = "x"; için g(a), g((String) a) ve g(null) ne yazdırır?

    Cevap

    O, S, S. a'nın bildirilen tipi Object'tir; nesnenin dizgi olması önemli değildir. Dönüşümle tip String olur. null iki sürüme de uyar; ama String, Object'in alt tipidir ve iki aday tek bir zincir üzerindedir. Daha özel olan g(String) seçilir; belirsizlik yoktur.

  5. static void h(double d) ve static void h(Integer i) varken h(5), h(Integer.valueOf(5)) ve h(5L) hangi sürümü seçer?

    Cevap

    h(5): 1. aşamada int → double genişletmesi uyar; Integer kutulama ister ve denenmez: double. h(Integer.valueOf(5)): 1. aşamada tam eşleşme h(Integer)'dır; double'a ulaşmak kutudan çıkarma ister: Integer. h(5L): long → double genişler: double.

  6. class A { A() { m(); } void m() { System.out.println("A"); } } ve class B extends A { int n = 7; void m() { System.out.println("B " + n); } } verilsin. new B(); ne yazdırır?

    Cevap

    B 0. A() içindeki m() çağrısı dinamik bağlanır ve nesne bir B olduğu için B.m() çalışır. O anda n'ye ilk değeri henüz atanmamıştır; int'in varsayılan değeri 0'dır. n = 7, A() döndükten sonra çalışır.

  7. Object[] d = new Integer[2]; d[0] = 5; d[1] = "a"; satırlarından hangisi sorun çıkarır?

    Cevap

    Üç satır da derlenir. d[0] = 5 çalışır: 5, Integer'a kutulanır ve dizinin gerçek öğe tipine uyar. d[1] = "a" ise çalışırken ArrayStoreException: java.lang.String verir; dizi gerçekte Integer[]'dir.

  8. class P { String ad() { return "P"; } String yaz() { return ad(); } } ve class Q extends P { String ad() { return "Q"; } String yaz() { return "[" + super.yaz() + "]"; } } verilsin. P p = new Q(); için p.yaz() ne döndürür?

    Cevap

    [Q]. p.yaz() dinamik bağlanır: Q.yaz(). super.yaz(), P.yaz()'ı doğrudan çağırır (invokespecial). Onun içindeki ad() ise this.ad()'dır ve this hâlâ bir Q nesnesidir: Q.ad() → "Q". super yalnız o tek çağrının hedefini sabitler; içerideki çağrılar yine dinamik bağlanır.

4.4.12İleri okuma

  • Oracle, The Java Tutorials, “Polymorphism” ve “Overriding and Hiding Methods” sayfaları: dinamik bağlamayı ve alan ile static metot gizlemeyi kısa örneklerle anlatır. Java'nın resmî ve ücretsiz başlangıç kaynağıdır.
  • J. Bloch, Effective Java (3. baskı), Madde 10 “Obey the general contract when overriding equals” ve Madde 19 “Design and document for inheritance or else prohibit it”: equals simetri tuzağını ve yapıcıdan geçersiz kılınabilir metot çağırmanın tehlikesini ayrıntılı tartışır.
  • P. Deitel ve H. Deitel, Java: How to Program (11. baskı), Bölüm 10 “Object-Oriented Programming: Polymorphism and Interfaces”: bordro örneğiyle çok biçimliliği adım adım kurar; soyut sınıflara ve arayüzlere de geçer.
  • J. Gosling ve diğerleri, The Java Language Specification, Java SE 21 Edition, 5.5 “Casting Contexts” ve 15.12 “Method Invocation Expressions”: dönüşümlerin ve aşırı yükleme seçiminin üç aşamasının kesin tanımıdır.

4.4.13Kaynaklar

  1. Oracle, The Java Tutorials: “Polymorphism”; “Overriding and Hiding Methods”; “Inheritance”.
  2. J. Gosling ve diğerleri, The Java Language Specification, Java SE 21 Edition, Oracle, 2023 (5.5 Casting Contexts; 8.4.8 Inheritance, Overriding, and Hiding; 15.12 Method Invocation Expressions).
  3. P. Deitel ve H. Deitel, Java: How to Program, 11. baskı, Pearson (Bölüm 10: Object-Oriented Programming: Polymorphism and Interfaces).
  4. K. Sierra ve B. Bates, Head First Java, 3. baskı, O'Reilly, 2022 (Inheritance and Polymorphism).
  5. C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, Pearson, 2022 (Bölüm 5: Inheritance).
  6. A. B. Downey ve C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019.
  7. J. Bloch, Effective Java, 3. baskı, Addison-Wesley, 2018.

İlgili sayfalar: Bölüm 4.4 sunumu · Çalışma soruları · Dinamik Bağlama simülatörü · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.