Nesne yönelimli programlama: kalıtım, dynamic binding ve vtable
OOP'nin üç özelliği, kalıtım ve alt tipler, dynamic binding ile hangi metodun çalışacağını bulmak, override/overload/hiding farkı, çoklu kalıtım ve dynamic binding'in vtable ile uygulanması.
Nesne yönelimli programlama (OOP) üç fikrin birleşimidir: abstract data type (geçen hafta), kalıtım (inheritance) ve dynamic binding. Bu sayfanın sınav karşılığı iki tür soru: “Hangi metot çalışır, ne basılır?” (override, overload, hiding, yapıcı sırası) ve “Dynamic binding nasıl uygulanır?” (vtable). İlk türde kural setini bilmek yeter.
Altın kurallar:
- Override edilen instance metot nesnenin gerçek sınıfına göre seçilir.
- Overload seçimi derlemede, argümanın bildirilen tipine göre yapılır.
- Alanlar ve static metotlar dinamik bağlanmaz, değişkenin tipine bakılır.
- Üst sınıfın yapıcısı önce çalışır.
- vtable: üst sınıfınkini kopyala, override edileni değiştir, yeniyi sona ekle.
Neden kalıtım?
ADT’ler yeniden kullanılabilir ama neredeyse her zaman küçük değişiklik gerekir; kodu kopyalayıp değiştirmek bakım kâbusu. Kalıtım, var olan bir sınıftan (superclass, parent) yeni bir sınıf (subclass, derived) türetir: alt sınıf üst sınıfın alanlarını ve metotlarını alır, yenilerini ekler, bazılarını override eder.
Terimler:
- Class: ADT. Object: sınıfın bir örneği (instance).
- Method: sınıfta tanımlı alt program. Message: bir nesneye metot çağrısı göndermek.
- Instance variable: her nesnenin kendi kopyası. Class variable (Java
static): sınıf başına bir tane. - Alt sınıfın nesnesi üst sınıfın beklendiği her yerde kullanılabiliyorsa alt sınıf bir subtype’tır. Java’da
publickalıtım bunu sağlar.
Kalıtımın dezavantajı: sınıflar arasında bağımlılık yaratır. Üst sınıftaki bir değişiklik tüm alt sınıfları etkileyebilir.
Dynamic binding
Üst sınıf tipinde bir referans, alt sınıflardan birinin nesnesini gösterebilir. Çağrılan metot override edilmişse hangi sürüm çalışır? Nesnenin gerçek sınıfınınki. Bu karar çalışma zamanında verilir: dynamic binding (polimorfizm).
Animal a = new Dog();
System.out.println(a.speak() + " " + a.name + " " + a.kind() + " " + a.intro());
Woof animal Animal I say Woof
| İfade | Seçim kuralı | Sonuç |
|---|---|---|
a.speak() | Override, dinamik: nesne Dog | Woof |
a.name | Alan, bildirilen tip Animal | animal |
a.kind() | Static metot, bildirilen tip | Animal |
a.intro() | Animal’da yazılı, içinde speak() dinamik | I say Woof |
Faydası: for (Animal an : zoo) an.speak(); kodu, ileride eklenecek Cat sınıfıyla da değişmeden çalışır.
Override ile overload karışınca
class P { void m(Object o) {...P.Object} void m(String s) {...P.String} }
class Q extends P { @Override void m(Object o) {...Q.Object} }
P p = new Q(); Object o = "text";
p.m(o); p.m("text");
Q.Object P.String
Tarif: (1) derleme zamanı: p’nin tipi P, argümanın bildirilen tipine göre P’deki imzalardan biri seçilir. o Object tipinde, içinde String olsa bile m(Object) seçilir. (2) çalışma zamanı: seçilen imzanın nesnenin sınıfındaki (Q) sürümü çalışır.
Yapıcı sırası
class A { A() { System.out.print("A "); show(); } void show() { ... "A.show" } }
class B extends A { int v = 7; B() { super(); System.out.print("B "); }
@Override void show() { ... "B.show v=" + v } }
new B();
A B.show v=0 B
Üst sınıf yapıcısı önce çalışır, alt sınıfın alan başlatıcıları ondan sonra. A’nın yapıcısındaki show() dinamik bağlanır ve henüz başlatılmamış v’yi (0) görür.
super ve private
X z = new Z(); // Z.hi → "Z+" + super.hi() → Y.hi → "Y+" + super.hi() → X.hi
System.out.println(z.hi());
System.out.println(new Derived().call()); // private who() override edilmez
Z+Y+X
Base
super.m() her zaman doğrudan üst sınıfın sürümüne gider. private metotlar miras alınmaz; aynı adlı metot alt sınıfta ayrı bir metottur.
Dil tasarım soruları
| Soru | Java | C++ | Smalltalk | C# |
|---|---|---|---|---|
| Her şey nesne mi? | Primitifler hariç | Hayır (hibrit) | Evet | Değer tipleri hariç (boxing var) |
| Varsayılan bağlama | Dinamik (private/static/final hariç) | Statik; virtual ile dinamik | Hep dinamik | Statik; virtual + override |
| Çoklu kalıtım | Sınıfta yok, interface ile | Var | Yok | Sınıfta yok, interface ile |
| Nesne nerede? | Heap | Stack ya da heap | Heap | Heap (class), stack (struct) |
| Bellek geri alma | Garbage collection | Programcı (delete, destructor) | GC | GC |
C++‘ta stack’teki bir nesne üst sınıf tipindeki bir değişkene değerle atanırsa alt sınıfın alanları kesilir (object slicing). Java’da tüm nesneler heap’te ve referansla erişildiği için bu sorun yok.
Abstract sınıf ve çoklu kalıtım
abstract class Shape { abstract double area(); }
Shape s = new Shape(); // error: Shape is abstract; cannot be instantiated
Çoklu kalıtımda iki üst sınıf aynı imzalı iki metot getirirse (diamond problem) hangisi seçilir? C++ programcıya Walker::move ile nitelemeyi zorunlu tutar. Java sınıflarda çoklu kalıtıma izin vermez; interface’lerdeki default metotlar çakışırsa derleyici override ister:
interface Walker { default String move() { return "walk"; } }
interface Swimmer { default String move() { return "swim"; } }
class Duck implements Walker, Swimmer {
public String move() { return Walker.super.move() + "&" + Swimmer.super.move(); }
}
walk&swim
Uygulama: CIR ve vtable
Class instance record (CIR), bir nesnenin bellekteki yapısıdır; record gibi derlemede belirlenir. Alt sınıfın CIR’i, üst sınıfın CIR’ini kopyalar ve kendi alanlarını sona ekler. Böylece üst sınıfın alanları her alt sınıf nesnesinde aynı ofsettedir.
Dinamik bağlanan metotlar için her sınıfın bir virtual method table’ı (vtable) vardır: sınıftaki dinamik bağlanan her metot için kodun adresi. Her CIR, sınıfının vtable’ını gösteren bir pointer taşır.
class Shape { area(); draw(); name(); }
class Circle extends Shape { override area(); new radius(); }
class Ring extends Circle { override draw(); override area(); }
slot Shape vtable Circle vtable Ring vtable
0 Shape.area Circle.area Ring.area
1 Shape.draw Shape.draw Ring.draw
2 Shape.name Shape.name Shape.name
3 - Circle.radius Circle.radius
for (Shape s : new Shape[]{ new Shape(), new Circle(), new Ring() })
System.out.println(s.area() + " " + s.draw() + " " + s.name());
Shape.area Shape.draw Shape.name
Circle.area Shape.draw Shape.name
Ring.area Ring.draw Shape.name
Derleyici s.area() çağrısını “nesnenin vtable’ında slot 0’daki adrese git” koduna çevirir. Hangi sınıf olursa olsun slot 0 hep area’dır; çalışmada sadece hangi tablonun okunduğu değişir. Maliyet: her çağrıda bir dolaylı adresleme. Statik bağlanan metotlar (C++ non-virtual, Java private/final) bu maliyeti ödemez ve vtable’da yer almaz.
Çoklu kalıtımda iş karmaşıklaşır: nesnenin her üst sınıf için ayrı bir vtable göstergesi ve parçası olur.
Sık yapılan hatalar
| Yanlış | Doğrusu |
|---|---|
a.name nesnenin sınıfına göre | Alanlar bildirilen tipe göre (hiding) |
| Static metot override edilir | Gizlenir (hiding), dinamik değil |
| Overload çalışmada seçilir | Derlemede, bildirilen tiple |
| Yapıcıda alt sınıf alanları hazır | Henüz varsayılan değerde (0, null) |
super.hi() dinamik | Doğrudan üst sınıfa statik |
| private metot override edilebilir | Miras alınmaz; ayrı metot |
| C++‘ta tüm metotlar dinamik | Sadece virtual olanlar |
| vtable’da override edilmeyen metot yok | Üst sınıfın sürümü aynı slotta kalır |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
What is printed in Java?
class Animal {
String name = "animal";
String speak() { return "..."; }
static String kind() { return "Animal"; }
String intro() { return "I say " + speak(); }
}
class Dog extends Animal {
String name = "dog";
@Override String speak() { return "Woof"; }
static String kind() { return "Dog"; }
}
// in main:
Animal a = new Dog();
System.out.println(a.speak() + " " + a.name + " " + a.kind() + " " + a.intro());Cevabı göster
Instance metodu dynamic binding ile nesnenin gerçek sınıfına göre seçilir: Dog.speak → Woof. Alanlar ve static metotlar dinamik bağlanmaz, değişkenin bildirilen tipine (Animal) göre seçilir: a.name = "animal", a.kind() = "Animal" (hiding). intro Animal'da yazılı ama içindeki speak() çağrısı da dinamik: "I say Woof".
What is printed in Java?
class A { A() { System.out.print("A "); show(); }
void show() { System.out.print("A.show "); } }
class B extends A { int v = 7;
B() { super(); System.out.print("B "); }
@Override void show() { System.out.print("B.show v=" + v + " "); } }
// in main:
new B();Cevabı göster
Önce üst sınıfın yapıcısı çalışır: "A ". A'nın yapıcısı show() çağırır; nesne aslında B olduğu için B.show çalışır. Ama B'nin alan başlatıcıları (v = 7) henüz çalışmadı, v varsayılan 0. Sonra B'nin yapıcısı: "B ". Yapıcıdan override edilebilir metot çağırmak bu yüzden tehlikeli.
What is printed in Java?
class P { void m(Object o) { System.out.print("P.Object "); }
void m(String s) { System.out.print("P.String "); } }
class Q extends P { @Override void m(Object o) { System.out.print("Q.Object "); } }
// in main:
P p = new Q();
Object o = "text";
p.m(o);
p.m("text");Cevabı göster
İki aşama: (1) Overload seçimi derleme zamanında, argümanın bildirilen tipine göre: o Object → m(Object) imzası; "text" String → m(String). (2) Override seçimi çalışma zamanında, nesnenin gerçek sınıfına göre: m(Object) Q'da override edilmiş → Q.Object; m(String) Q'da yok → P.String.
Fill in the vtable of class Ring: which implementation does each slot point to?
class Shape { area(); draw(); name(); }
class Circle extends Shape { override area(); new radius(); }
class Ring extends Circle { override draw(); override area(); }Cevabı göster
Alt sınıfın vtable'ı üst sınıfınkinin kopyasıyla başlar, slot sırası korunur: [area, draw, name] + Circle'ın eklediği [radius]. Override edilen slotlar yeni sürümle değiştirilir. Ring area ve draw'ı değiştirir; name Shape'ten, radius Circle'dan miras. Çağrı s.area() derlemede "slot 0" olarak çevrilir, çalışmada nesnenin vtable'ından okunur.
What is printed in Java?
class Base { private String who() { return "Base"; }
String call() { return who(); } }
class Derived extends Base { private String who() { return "Derived"; } }
// in main:
System.out.println(new Derived().call());Cevabı göster
private metotlar miras alınmaz ve override edilemez; Derived'daki who tamamen ayrı bir metot. Base.call içindeki who() derleme zamanında Base.who'ya statik bağlanır. Java'da private, static ve final metotlar statik bağlanır; vtable'a girmeleri gerekmez.
What is printed in Java?
class X { String hi() { return "X"; } }
class Y extends X { @Override String hi() { return "Y+" + super.hi(); } }
class Z extends Y { @Override String hi() { return "Z+" + super.hi(); } }
// in main:
X z = new Z();
System.out.println(z.hi());Cevabı göster
Dinamik bağlama Z.hi'yi seçer. super.hi() dinamik değildir: her zaman sınıfın doğrudan üst sınıfındaki sürümü çağırır. Z → Y.hi → X.hi zinciri: "Z+" + "Y+" + "X".
Does this Java code compile? Why?
abstract class Shape { abstract double area(); }
// in main:
Shape s = new Shape();Cevabı göster
Abstract metot gövdesiz bir protokoldür; en az bir abstract metodu olan sınıf abstract'tır ve nesnesi yaratılamaz. Amacı alt sınıflara ortak bir arayüz vermek. Shape s = new Circle(); gibi alt sınıf nesnesiyle kullanılır. C++'ta karşılığı pure virtual function: virtual double area() = 0;.
Two Java interfaces Walker and Swimmer both declare default String move(). A class Duck implements Walker, Swimmer does not override move. What happens, and how is it fixed?
Cevabı göster
Çoklu kalıtımın klasik sorunu (diamond problem): iki üst tipten aynı imzalı iki uygulama geliyor, hangisi? Java derleyici seçim yapmaz, sınıfın açıkça override etmesini ister. Walker.super.move() + "&" + Swimmer.super.move() → "walk&swim". C++ çoklu sınıf kalıtımına izin verir ve belirsizliği Walker::move gibi nitelemeyle çözdürür.
Name the three characteristic features of an object-oriented language.
Cevabı göster
ADT verinin temsilini gizler. Kalıtım (inheritance) var olan bir sınıfı yeniden kullanıp değiştirmeyi sağlar; ADT'lerin yeniden kullanım sorununu çözer. Dynamic binding (polimorfizm) bir üst sınıf referansıyla alt sınıfların kendi metotlarını çağırmayı sağlar; yeni alt sınıf eklemek mevcut kodu değiştirmez.
In C++, method draw is declared in class Shape without the virtual keyword and redefined in Circle. Which version runs for Shape* p = new Circle(); p->draw();?
Cevabı göster
C++'ta varsayılan statik bağlamadır; sadece virtual bildirilen metotlar dinamik bağlanır ve vtable'a girer. virtual olmadan çağrı pointer'ın tipine (Shape*) göre derlemede çözülür. Java'da tersi: private/static/final değilse her metot dinamik bağlanır. C++ bu tercihi verimlilik için yapar.
What is printed in Java?
Animal[] zoo = { new Animal(), new Dog(), new Dog() };
int woofs = 0;
for (Animal an : zoo) if (an.speak().equals("Woof")) woofs++;
System.out.println(woofs + " " + (zoo[1] instanceof Animal) + " " + (zoo[0] instanceof Dog));Cevabı göster
Aynı an.speak() çağrısı her elemanda nesnenin sınıfına göre farklı metodu çalıştırır: polimorfizm. İki Dog → 2. Dog bir Animal'dır (alt tip, subtype): true. Animal nesnesi Dog değildir: false. Alt tip ilişkisi tek yönlü.
What does a class instance record (CIR) contain in a typical implementation, and where are the methods themselves stored?
Cevabı göster
CIR bir nesnenin yapısıdır, record gibi derlemede belirlenir. Alt sınıfın CIR'i üst sınıfınkini kopyalar ve kendi alanlarını sona ekler; bu yüzden alt sınıf nesnesi üst sınıf referansıyla güvenle kullanılabilir. Dinamik bağlanan metotlar için CIR'de vtable göstergesi bulunur. Statik bağlananların CIR'de yeri yoktur.
Doğrulama: Tüm Java çıktıları JDK ile çalıştırıldı (java verify/SEN2001/W11.java); abstract sınıf örnekleme ve çakışan default metot örnekleri javac ile derlenmeye çalışıldı ve beklenen derleme hatalarını verdi (verify/SEN2001/w11b/). vtable tablosu, her sınıfta her metodun hangi sürümünün çalıştığı Java'da gerçekten çağrılarak dolduruldu. C++ virtual/non-virtual ve Smalltalk bilgileri kavramsal; derleyicileri kurulu değil.