OOP: Nesne Yönelimli Programlama: Polymorphism (Çok Biçimlilik) Rehberi

Polymorphism (Çok Biçimlilik) Nedir?
Key Takeaways
- Çok biçimlilik, üst tipi bekleyen kodun alt tiplerle çalışabilmesi: aynı çağrı, elindeki nesneye göre farklı davranır.
- İki ayrı biçimi var. Overloading derleme zamanında değişken tipine bakılarak, overriding çalışma zamanında nesnenin gerçek tipine bakılarak çözülür.
- equals metodunu Object yerine kendi tipinizle yazmak bir override değil overload üretir; koleksiyonlar bunu asla çağırmaz. @Override anotasyonu bu hatayı derleme anında yakalar.
- static metotlar ve alanlar çok biçimli değildir. İkisi de değişkenin tanımlı tipine göre çözülür, nesnenin gerçek tipine göre değil.
- Çok biçimliliğin en görünür getirisi, tip kontrolü yapan if/else zincirlerinin ortadan kalkması.
Bir metot çağırıyorsunuz ve hangi kodun çalışacağını bilmiyorsunuz. İlk duyulduğunda kulağa hata gibi geliyor. Oysa nesne yönelimli programlamanın günlük işte en çok karşılığını veren özelliği bu.
Kelime Yunanca kökenli: poly (çok) ve morph (biçim). Yazılımda karşılığı, tek bir arayüzün arkasında birden fazla davranışın durabilmesi.
Serinin kalıtım yazısında method overriding’e değinmiştim. Burada onu tekrar etmek yerine, çok biçimliliğin ikinci ve daha az konuşulan yüzüne, ikisinin karıştırıldığı noktalara ve çalışma zamanının kararı nasıl verdiğine odaklanacağım.
İki tür çok biçimlilik
Java gibi statik tipli dillerde çok biçimlilik iki ayrı yerde, iki ayrı zamanda gerçekleşiyor.
Derleme zamanı: Method Overloading
Aynı isimde ama farklı parametre listeleriyle birden fazla metot tanımlarsınız. Derleyici, çağrıdaki argümanların tipine bakıp hangisini kullanacağına karar verir.
public class Yazdirici { public void yazdir(String metin) { System.out.println("Metin: " + metin); } public void yazdir(int sayi) { System.out.println("Sayı: " + sayi); } public void yazdir(String metin, int tekrar) { for (int i = 0; i < tekrar; i++) { System.out.println(metin); } }}Karar derleme anında verilir ve bir daha değişmez. Derleyici yazdir("merhaba") gördüğünde ilk metodu seçer ve ürettiği bytecode’da çağrıyı (Ljava/lang/String;)V imzasına sabitler. Sınıfı javap -c ile açarsanız bunu doğrudan görebilirsiniz. Çalışma zamanında ortada seçilecek bir şey kalmaz.
Kritik ayrıntı da burada: seçim, değişkenin tanımlı tipine göre yapılır, nesnenin gerçek tipine göre değil. Bu cümle şimdi soyut görünüyor olabilir; birazdan equals örneğinde neye mal olduğunu göreceksiniz.
Çalışma zamanı: Method Overriding
Alt sınıf, üst sınıftaki bir metodu aynı imzayla yeniden yazar. Hangi sürümün çalışacağına derleyici değil, çalışma zamanı karar verir.
public class Sekil { public double alan() { return 0; }} public class Daire extends Sekil { private final double yaricap; public Daire(double yaricap) { this.yaricap = yaricap; } @Override public double alan() { return Math.PI * yaricap * yaricap; }} // KullanımSekil sekil = new Daire(2);System.out.println(sekil.alan()); // 12.566...sekil değişkeninin tipi Sekil. Yine de çalışan kod Daire sınıfındaki metot. Çok biçimlilik dendiğinde çoğu kişinin aklına gelen davranış bu ve serinin kalıtım yazısında ayrıntısıyla ele almıştım.
Klasik tuzak: equals metodunu overload etmek
Overload ile override arasındaki farkın neden önemli olduğunu gösteren en iyi örnek, equals metodu.
Bir sınıfa eşitlik davranışı kazandırmak istediğinizde yazmanız gereken imza equals(Object). Ama parametre olarak kendi tipinizi yazmak çok daha doğal görünür:
public class Kullanici { private final String email; public Kullanici(String email) { this.email = email; } // Bu bir override DEĞİL, overload. public boolean equals(Kullanici digeri) { return this.email.equals(digeri.email); }}Kod sorunsuz derlenir. Dahası, testlerinizde a.equals(b) yazarsanız doğru sonucu bile alırsınız; derleyici her iki değişkenin de Kullanici tipinde olduğunu görüp sizin metodunuzu seçer. Yeşil testlerle işi bitirdiğinizi düşünürsünüz.
Sorun, nesneyi bir koleksiyona koyduğunuzda çıkar.
Set<Kullanici> kume = new HashSet<>();kume.add(new Kullanici("a@ornek.com"));kume.add(new Kullanici("a@ornek.com")); System.out.println(kume.size()); // 1 değil, 2HashSet içeride Object tipiyle çalışır ve equals(Object) çağırır. Sizin yazdığınız equals(Kullanici) bambaşka bir metottur; koleksiyon onu hiç görmez. Devreye giren şey Object sınıfından miras alınan, referans karşılaştırması yapan varsayılan sürümdür.
Bu hatayı yakalamanın yolu tek satır: metodun üstüne @Override yazın. Derleyici hemen itiraz eder, çünkü Object sınıfında equals(Kullanici) diye bir metot yoktur. Anotasyonun varlık sebebi de zaten bu; çalışma zamanına kalacak bir hatayı derleme anına çeker.
Warning
equals metodunu yazdığınızda hashCode metodunu da yazmanız gerekir. İkisi arasındaki sözleşme şudur: eşit iki nesne aynı hash değerini üretmek zorundadır. Aksi halde HashMap ve HashSet nesnenizi kaydettiği kovada bulamaz.Çalışma zamanı hangi metodu seçiyor
Override edilmiş bir metot çağrısı derleyici tarafından invokevirtual komutuna çevrilir. Bu komut hedefi sabitlemez, arama işini çalışma zamanına bırakır.
JVM her sınıf için bir metot tablosu tutar. Nesne üzerinden çağrı yapıldığında nesnenin gerçek sınıfının tablosuna bakılır, doğru adres oradan bulunur. Buna dinamik gönderim (dynamic dispatch) deniyor.
Maliyeti var ama küçük. Üstelik JIT derleyici, çalışma sırasında o çağrı noktasına hep aynı tipin geldiğini görürse çağrıyı doğrudan yerine yazıp aradaki adımı büsbütün kaldırır.
Buradan çıkan sonuç şöyle özetlenebilir: overloading’i derleyici, overriding’i JVM çözer. Biri değişkenin yazılı tipine bakar, diğeri nesnenin gerçek tipine.
Çok biçimlilik if/else zincirlerini siler
Teoriyi bırakıp çok biçimliliğin günlük kodda ne kazandırdığına bakalım. Farklı ödeme yöntemlerini işleyen bir sınıf, tipik olarak şöyle başlar:
public class OdemeIslemcisi { public void isle(String tip, double tutar) { if (tip.equals("KREDI_KARTI")) { // kredi kartı akışı } else if (tip.equals("HAVALE")) { // havale akışı } else if (tip.equals("KAPIDA_ODEME")) { // kapıda ödeme akışı } else { throw new IllegalArgumentException("Bilinmeyen ödeme tipi: " + tip); } }}Her yeni ödeme yöntemi bu metodu yeniden açmayı gerektiriyor. Zincir de tek bir yerde kalmıyor: iade, raporlama ve doğrulama tarafında kopyaları beliriyor. Üç ayrı yerde dört dallı bir if zinciriniz olduğunda, dördüncü yöntemi eklerken birini atlamamak tamamen dikkate kalıyor. Atladığınız gün hata da derleyiciden değil, üretimden geliyor.
Aynı işi çok biçimlilikle kurduğunuzda zincir tamamen kaybolur:
public interface OdemeYontemi { void isle(double tutar);} public class KrediKarti implements OdemeYontemi { @Override public void isle(double tutar) { // kredi kartı akışı }} public class Havale implements OdemeYontemi { @Override public void isle(double tutar) { // havale akışı }} // Çağıran taraf hangi yöntemin geldiğini bilmiyor.public class OdemeIslemcisi { public void isle(OdemeYontemi yontem, double tutar) { yontem.isle(tutar); }}Kapıda ödeme desteği eklemek artık yeni bir sınıf yazmaktan ibaret. OdemeIslemcisi dosyasına dokunulmaz, çalışan kredi kartı akışı risk altına girmez. Serinin dışına çıkıp söyleyeyim: bu, SOLID’in Open-Closed prensibinin uygulanmış hali ve prensipleri ele aldığım S-O-L-I-D Prensipleri Nedir? yazısında ayrıntısıyla bulabilirsiniz.
Çok biçimli olmayan üyeler
Her şey dinamik olarak çözülmüyor. Üç istisnayı bilmek, saatler süren hata avlarını önlüyor.
static metotlar override edilemez. Alt sınıfta aynı imzayla bir static metot yazdığınızda üsttekini gizlemiş olursunuz; buna method hiding deniyor. Hangisinin çalışacağına yine değişkenin tanımlı tipi karar verir.
private metotlar da çok biçimli değildir, çünkü alt sınıf onları görmez bile. Aynı isimde yazdığınız metot bambaşka bir metottur.
Alanlar için de aynı kural geçerli ve bu en sinsi olanı:
class Ust { String ad = "ust";} class Alt extends Ust { String ad = "alt";} Ust nesne = new Alt();System.out.println(nesne.ad); // "ust" yazar, "alt" değilMetot çağrısı olsaydı Alt sürümü çalışırdı. Alan erişimi ise değişkenin tanımlı tipine göre çözülür. Alan gizleme (field hiding) diye geçen bu davranış neredeyse hiçbir zaman istediğiniz şey değildir; alt sınıfta üst sınıftakiyle aynı isimde alan tanımlamaktan kaçının.
Çok biçimliliğin sessiz şartı
Alt sınıfın üst sınıfın yerine geçebilmesi teknik olarak her zaman mümkün. Doğru sonuç vermesi ise değil.
Bir alt sınıf, devraldığı metodu boş bırakıyorsa ya da beklenmedik bir istisna fırlatıyorsa üst tiple çalışan kod sessizce bozulur.
Çok biçimlilik burada çalışmaya devam eder, sadece yanlış davranır. Liskov’un Yerine Koyma Prensibi tam olarak bu durumu yasaklıyor; ayrıntısına SOLID yazısında girmiştim.
Pratik ölçüt: alt sınıfınızı, üst sınıfı bekleyen bir metoda verdiğinizde o metodun bütün testleri geçmeye devam ediyor mu? Geçmiyorsa ortada bir kalıtım ilişkisi yok demektir.
Sonuç
Çok biçimliliği anlamanın en kısa yolu, hangi kararın ne zaman verildiğini bilmek. Overloading derleme zamanında ve değişkenin yazılı tipine göre. Overriding çalışma zamanında ve nesnenin gerçek tipine göre.
Bu ayrım akademik bir ayrıntı değil. equals metodunun neden Object parametresi aldığını, @Override anotasyonunun neden yazılması gerektiğini ve neden bir alanı gizlemenin metot geçersiz kılmakla aynı şey olmadığını hep aynı ayrım açıklıyor.
Serinin son yazısında Abstraction (soyutlama) konusunu ele alacağım: abstract sınıf ile arayüz arasındaki kararı, Java 8’in default metotlarla bu kararı nasıl değiştirdiğini ve soyutlamanın ne zaman fayda değil yük ürettiğini. Altı kavramı birlikte gösteren kapanış örneği de orada.
Frequently Asked Questions
- Overloading ile overriding arasındaki fark nedir?
- Kısa ayrım: overloading imza farkına, overriding tip farkına dayanır. Overloading’de aynı isimde ama farklı parametre listeli metotlar vardır ve seçimi derleyici yapar. Overriding’de imza birebir aynıdır, seçimi çalışma zamanı yapar. Pratik bir işaret de var: overriding @Override ile işaretlenebilir, overloading işaretlenemez.
- equals metodunu neden Object parametresiyle yazmak zorundayım?
- Çünkü Object sınıfındaki imza equals(Object) ve koleksiyonlar sizinle o imza üzerinden konuşur. Kendi tipinizi parametre yaparsanız ayrı bir metot tanımlamış olursunuz; HashSet, HashMap ve List.contains onu hiç görmez. Belirti de karakteristiktir: birim testleriniz geçer, ama nesneyi bir koleksiyona koyduğunuz anda eşitlik çalışmaz.
- static metotlar override edilebilir mi?
- Edilemez, yalnızca gizlenebilir. Sebebi şudur: static metot sınıfa aittir, nesneye değil, dolayısıyla ortada bakılacak bir nesne tipi yoktur. Bir static metoda @Override koymayı denerseniz derleyici hata verir. Alt sınıfta aynı isimde static metot yazma ihtiyacı duyuyorsanız, o metot muhtemelen static olmamalı.
- Çok biçimlilik performansı düşürür mü?
- Ölçülebilir bir maliyeti var ama gerçek uygulamalarda darboğaz olduğu neredeyse hiç görülmez. HotSpot, çağrı noktasına gelen tipleri sayar; tek tip geliyorsa çağrıyı sabitler, iki tip geliyorsa ikisi arasında hızlı bir dal kurar. Sorun ancak bir noktaya çok sayıda farklı tip geldiğinde başlar, o da ölçülerek anlaşılır. Profil almadan çok biçimlilikten kaçınmak, çözdüğünden fazla tasarım borcu üretir.
- Arayüz kullanmadan çok biçimlilik olur mu?
- Olur. Soyut ya da somut bir üst sınıfın metodunu alt sınıfta geçersiz kılmak da çok biçimlilik üretir. Arayüzün avantajı, bir sınıfın Java’da yalnızca tek bir sınıftan türeyebilmesine karşılık istediği kadar arayüz uygulayabilmesi. Kalıtım kotanızı harcamadan çok biçimlilik istiyorsanız arayüz tarafındasınız.
Referanslar
Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley. Madde 10 equals sözleşmesini, Madde 11 hashCode zorunluluğunu, Madde 40 @Override kullanımını, Madde 52 ise aşırı yüklemenin tuzaklarını ele alıyor.
Bloch, J. ve Gafter, N. (2005). Java Puzzlers: Traps, Pitfalls, and Corner Cases. Addison-Wesley. Aşırı yükleme çözümlemesinin sezgiye ters çalıştığı örneklerle dolu. Bu yazıdaki equals tuzağını ilk kez orada okumuştum.
Gamma, E., Helm, R., Johnson, R. ve Vlissides, J. (1994). Design Patterns. Addison-Wesley. Yazıdaki if/else zincirinin yerine geçen yapının adı Strategy ve kanonik anlatımı bu kitapta.
Lindholm, T., Yellin, F., Bracha, G. ve Buckley, A. The Java Virtual Machine Specification, Java SE. Metot çözümlemesi 5.4.3.3, invokevirtual komutunun davranışı ise 6.5 bölümünde tanımlı.

Mustafa Kürşad Başer
Senior Software Engineer
A passionate software engineer who enjoys creating elegant solutions to complex problems. Beyond coding, I am deeply interested in exploring the intersections of technology, art, and human consciousness.

