Sunday, July 16, 2017

Java'da Çok Şekillilik Üzerine Bir Örnek

John Connor: Wait a minute here. You're telling me that this thing can imitate anything it touches?
The Terminator: Anything it samples by physical contact. 

Çok şekillik, Nesneye Dayalı Programlamanın en önemli mekanizmasıdır. Aşağıda çok şekilliliğin Java'da nasıl çalıştığını incelememizi sağlayacak bir kod örneği verilmiştir:

package com.example.polymorphism;

/**
 * 
 * @author Binnur KURT (binnur.kurt@gmail.com)
 *
 */
public class Question {

    public static void main(String[] args) {
        C c = new C(2);
        A a = c;
        B b = c;
        System.out.println(a.x);
        System.out.println(b.x);
        System.out.println(c.x);
        System.out.println(((A) c).x);
        System.out.println(((B) c).x);
        System.out.println(((A) b).x);
        System.out.println(a.getX());
        System.out.println(b.getX());
        System.out.println(c.getX());
        System.out.println(((A) c).getX());
        System.out.println(((B) c).getX());
        System.out.println(((A) b).getX());
    }
}

class A {

    public int x;

    public A(int x) {
        this.x = x;
    }

    public int getX() {
        return x;
    }
}

class B extends A {

    public int x;

    public B(int x) {
        super(3 * x);
        this.x = x;
    }

    @Override
    public int getX() {
        return x;
    }
}

class C extends B {

    public int x;

    public C(int x) {
        super(2 * x);
        this.x = x;
    }

    @Override
    public int getX() {
        return x;
    }
}


Öncelikli olarak C c= new C(2) satırında C tipinden bir nesne yaratıldığında bellekte oluşan yapıyı anlamamız gerekir. Aşağıdaki şekilde A, B ve C tipinden referansları ve C tipinden nesne arasındaki bellekteki ilişki verilmiştir:
Önce C tipinden c referansı yaratılır. c değişkeni bir fonksiyon bloğunda tanımlandığı için yığında (=stack) yaratılacaktır. O nedenle kutunun üzerine S etiketi koyduk. c bir referans değişkeni olduğu için bir nesnenin referansını taşıyacaktır. Bu nedenle kutunun içine @ sembolü koyduk. Ancak kutu henüz geçerli bir nesnenin adresini taşımıyor!

Java'da tüm nesneler Heap'de yaratılır. Bu nedenle bu nesnelere Heap nesnesi de denir. Java Sanal Makinası bazı özel durumlarda başarım için bir nesneyi yığında da yaratabilir. Bu durumu göz ardı edip, C tipinden nesneyi Heap'de yaratmaya başlayalım. Bir sınıftan nesne yaratılırken sadece öznitelikleri için bellekten yer ayrılır. Her nesnenin bellekteki diziliminde bir başlık kısmı bulunur. Bu başlık kısımında nesnenin sanal tablosuna bir referans, çeşitli sayaçlar gibi bilgiler bulunur. Ancak aşağıdaki çizimlerde, basitlik için bu başlık kısmını gözardı edeceğiz. C sınıfı yalın bir sınıf değil: önce B sınıfından, B sınıfı da A sınıfından türetilmiş durumda. Elbette A sınıfı da Object sınıfından türetilmiş ancak bu durumu şimdilik göz ardı edelim. Önce A sınıfından gelen veriler için bellekte yer ayrılır. Ardından A sınıfından gelen x'i ilklendirmek üzere A sınıfının kurucu fonksiyonu çalılşacaktır. Peki, A sınıfının kurucu fonksiyonu parametre değerini nasıl alır? Java'da temel sınıfın tüm özellikleri kalıtılımla türetilmiş sınıfa geçer, kurucu fonksiyonlar hariç! Temel sınıfın kurucu fonksiyonu türetilmiş sınıfın kurucu fonksiyonu değildir! Türetilmiş sınıf için de bir kurucu fonksiyon yazmak gerekir. Üstelik türetilmiş sınıf için yazdığımız kurucu fonksiyon ilk iş olarak temel sınıfın kurucusu fonksiyonunu  super anahtar kelimesi ile çağırmak zorundadır. Örneğimizde C sınıfının kurucu fonksiyonu super(2*x) ile B'nin kurucu fonksiyonunu ve B'nin kurucu fonksiyonu da super(3*x) ile A'nın kurucu fonksiyonunu çağırmaktadır: C(2 B(4 C(12). Bu sayede A kurucu fonksiyonu 12 parametresi ile çağrılmış olur. O halde A sınıfından gelen x'in değeri 12 olur.
Daha sonra B'den gelen x için Heap'de yer ayrılır ve ardından B'nin kurucusu bu x'e ilk değerini atar: 4.



Son olarak, C'den gelen x için Heap'de yer ayrılır ve ardından C'nin kurucusu bu x'e ilk değerini atar: 12. Böylelikle C sınıfından nesne yaratılması işlemi tamamlanmış oluyor.

A tipinden bir referans hem A tipinden bir nesneye hem de A sınıfından türetilmiş ne kadar sınıf varsa (B ve C sınıfları) hepsini referans edebilir. Bu örnekte A tipinden referansımız olan a değişkeni de C tipinden bir nesneyi gösteriyor.
B tipinden bir referans hem B tipinden bir nesneye hem de B sınıfından türetilmiş ne kadar sınıf varsa (sadece C sınıfı) hepsini referans edebilir. Bu örnekte B tipinden referansımız olan b değişkeni de C tipinden bir nesneyi gösteriyor.


Artık aşağıdaki ifade ile bellekte oluşan yapıyı tanıyoruz:

C c = new C(2);
A a = c;
B b = c;

Şimdi sırayla aşağıdaki kod parçalarının davranışını çalışabiliriz:

  • Referanslar üzerinden özniteliklere erişim
Öncelikli olarak yukarıdaki kod parçasının kötü tasarlanmış olduğunu belirtmeliyim. Nesneye Dayalı Programlamada hiç bir zaman öznitelikleri, referans üzerinden, doğrudan erişime açmamalıyız! Verilerimizi gizlemeliyiz! Ancak bu çalışmadaki amacımız dilin inceliklerini ve arka tarafta çalışmasındaki detayları kavramak olacaktır.
Şimdi aşağıdaki kod parçasının davranışını çalışabiliriz: 
System.out.println(a.x);
System.out.println(b.x);
System.out.println(c.x);
Verilere referanslar üzerinden erişildiğinde önemli olan referansın gösterdiği nesnenin tipi değil, referansın tanımlandığı tipdir. Bu nedenle a.x'de A sınıfında tanımlanan x'in değerine, b.x'de B sınıfında tanımlanan x'e ve en nihayetinde de c.x'de C sınıfındaki x'e erişiyoruz. Bu nedenle yukarıdaki kod parçası çalıştırıldığında ekranda 12, 4 ve 2 değerlerini görürüz.
Şimdi ise referansları farklı tiplere dönüştürürerek yine veriye erişmek istiyoruz:
System.out.println(((A) c).x);
System.out.println(((B) c).x);
System.out.println(((A) b).x);
Yukarıdaki analiz hala geçerlidir: verilere referanslar üzerinden erişildiğinde önemli olan referansın gösterdiği nesnenin tipi değil, referansın tanımlandığı tipdir.  Ancak bu sefer tip dönüşümü yapıldığı için hedef tipin tanımladığı sınıftaki veriye erişeceğiz. Buna göre ekran çıktısı sırasıyla 12, 4 ve 12 olacaktır.
Yukarıdaki örneklerde görüldüğü gibi referanslar üzerinden özniteliklere eişildiğinde çok şekillilik çalışmıyor. Çünkü çok şekillilik sadece fonksiyonlar üzerinde çalışır, veriler üzerinde çalışmaz!
  • Referanslar üzerinden özniteliklere erişim 
Şimdi a, b ve c referanslarını kullanarak getX() isimli metodu önce doğrudan çağıralım:
System.out.println(a.getX());
System.out.println(b.getX());
System.out.println(c.getX());
Her üç referans değişkeni de (ab ve cC tipinden bir nesneyi gösterdiği için her üç durumda da C sınıfındaki getX() metodu çalışacaktır ve ekranda üç satır olmak üzere 2 değerini görürüz.
Aşağıdaki durumda ise ab ve c değişkenleri üzerinde tip dönüşümü yaparak kullanıyoruz:
System.out.println(((A) c).getX());
System.out.println(((B) c).getX());
System.out.println(((A) b).getX());
Bu kullanımlarda değişen bir durum olmayacak ve çok şekillilik her zaman çalışacaktır. Sonuç olarak istediğiniz kadar referans değişken üzerinde tip dönüşümü yapın, referansın gösterdiği nesne hala C tipinden, dolayısı ile C sınıfındaki getX() fonksiyonu çalışacaktır. Yukarıdaki kod parçası çalıştığında ekranda üç satır olmak üzere 2 değerini görürüz  

1 comment: