Geçen hafta boyunca soyutlama kavramın irdelemeye
çalıştım. Bunu yaparken pek zorlanmadım çünkü C# dilinden soyutlamanın ne
olduğunu biliyordum. Ancak bu kavram zaman zaman kafa karıştırıcı bir hal
alabilmekteydi. En büyük sıkıntı soyutlamaların tam olarak nasıl ifade
edildikleri ve kavramsal olarak neyi yada neleri belirtebilecekleriydi. C#
dilinde soyutlamaları anlamaya çalıştığım ilk zamanlarda, aynı satırları 4 hatta 5 kez okuduğumu ve uzun
süreler ne olup bittiğini anlamak için çabaladığımı hatırlıyorum.
Bu sıkıntıyı daha önce yaşadığımdan, şimdi java
dilinide soyutlamaları incelerken de benzer durumlar ile karşılaşabileceğim
korkusu ile bu haftaki soyutlama çalışmalarımı daha bir titiz yapma
zorunluluğunu hissettim. Neyseki C# dilinden pek bir farkı yokmuş.
En önemli konu, soyutlamanın tam olarak sınıflara
ve içerisinde yer alan metodlara uygulandığını bilmek. Peki o halde soyut sınıf
nedemek? Soyut sınıfları, somut olmayan sınıflar olarak tanımlıyarak bu kavramı
açıklamaktan kurtulabilirim. Keza, somut olmayan sınıflarda soyutturlar. Bu
aslında, Japonların yada Çinlilerin (hangi tarafın bu fikrin orjinaline sahip
olduğunu bilmiyorum), Ying Yang felsefesinin bir sonucudur. Ancak
bu tanımlama her nekadar geçerli bir tanımsada, fazla bir şey ifade
etmemektedir.
Asıl olan, soyut sınıfların, kalıtımın bir parçası
olarak normal bir sınıfın sahip olabileceği üyeler dışında, soyut metodlara
sahip oluğu ve bu soyutlamadan türeyen sınıflardada, soyutlanmış metod
bildirimlerini mutlaka ve mutlaka uygulamak zorunda bıraktığı yapılardır. Bu
açıdan bakıldığında, soyut sınıfların, kalıtımda birleştirici bir rol
üstlenmenin yanı sıra, türeyen sınıfların zorunlu olarak izlemeleri gereken bir
rehber oluşturduğunuda söyleyebiliriz.
Soyutlamanın bu uzun ama çok şey ifade eden tanımı
yinede bu konuyu anlamak için yeterli değildir. Nitekim, soyutlamda dikkat
edilmesi gereken ve göze çarpan pek çok nokta vardır. Ama yinede, şu an için,
soyutlamayı gösteren basit bir örnek ile işe girişmenin daha faydalı olacağı
kanısındayım. Her şeyden önce, soyut sınıfların ve metodların nasıl
tanımlandığını bilmemiz gerekiyor. Kaldıki soyutlamanın önemli yapıtaşlarını
oluşturan kurallar bu temeller üzerine kurulu olucaktır. Bu amaçla, aşağıdaki
gibi basit bir örnek geliştirdim.
abstract class SoyutTemel
{
public static void Yaz()
{
System.out.println("Ben soyut temel
sinifim");
}
abstract public void Kimlik();
SoyutTemel()
{
}
int deger=40;
}
class Tureyen1 extends SoyutTemel
{
public void Kimlik()
{
System.out.println("Ben SoyutTemel
siniftan tureyen Tureyen1");
}
}
class Tureyen2 extends SoyutTemel
{
public void Kimlik()
{
System.out.println("Ben SoyutTemel
siniftan tureyen Tureyen2");
}
}
public class Soyutlama
{
public static void main(String[] args)
{
Tureyen1 t1=new Tureyen1();
Tureyen2 t2=new Tureyen2();
t1.Kimlik();
t2.Kimlik();
}
} |
Örneği derleyip çalıştırdığımda aşağıdaki ekran
görüntüsünü elde ettim.

Bu uygulamada ilk dikkati çeken nokta soyutlamanın
uygulandığı sınıfların tanımlanma şeklidir. Bir sınıf soyut olucaksa, abstract
anahtar sözüğü ile tanımlanır.
| abstract class SoyutTemel |
Bu anahtar sözcük ile SoyutTemel sınıfının
soyutlamayı uygulayacağını belirtmiş oluruz. Peki bu uygulamanın getirdiği
etkiler nelerdir? İlk olarak şunu söyleyebilirim. Soyut bir sınıftan herhangibir
nesne örneği türetilemez. Bunun sebebi, soyut sınıfların birleştirici
özelliklerinin yanısıra, abstract metod tanımlamarını içermesidir.
| abstract public void Kimlik(); |
Burada görüldüğü gibi abstract metodlar, sadece
bir metod bildiriminden ibarettir. Öyleki bu metod bildirimi, bu soyut sınıftan
türeyen sınıflarda mutlaka ama mutlaka override edilmelidir. Dolayısıyla, eğer
bir soyut sınıf nesne örneği oluşturulabilseydi, bu metod bildirimlerine
erişilmesi son derece anlmasız olurdu. İşte bu nedenle, soyut sınıfların
örneklendirilmesi yasaklanmıştır.
Bununla birlikte bu yasaklama, sanıldığı gibi, soyut sınıfların
herhangibir yapılandırıcı içeremeyeceğini göstermez. Nitekim yukarıdaki
örneğimizde, abstract sınıfımız için bir adet varsayılan yapıcı metod
yazılmıştır. O halde, java dili abstract bir sınıfın nesne örneğini, new operatörü
kullanıldığı yerde denetleyecektir. Örneğin, uygulamamızın Main metodu
içerisinde SoyutTemel abstract sınıfından bir nesne örneği yaratmaya çalışalım.
Bu durumda java derleyicisi derleme zamanında bizi bir hata ile uyaracaktır.
public static void main(String[] args)
{
Tureyen1 t1=new Tureyen1();
Tureyen2 t2=new Tureyen2();
t1.Kimlik();
t2.Kimlik();
SoyutTemel t=new
SoyutTemel();
} |

Diğer yandan, soyut sınıflara ait nesne örnekleri
tanımlayamasakta, soyut sınıflara ait nesneler tanımlayarak (yani sadece nesne
tanımlama, oluşturmak değil) bu nesnelere, türemiş sınıf nesnelerinin
referanslarını aktarabiliriz. Bu bize, kalıtımın en önemli taşlarından olan çok
biçimliliği uygulama fırsatını verir ve dostumuz bugi'yi hatırlamamızı sağlar.
Nitekim, soyut sınıflar, kalıtımın bir parçası olarak, çok biçimliliğide bir
şekilde desteklemelidirler. Bu kalıtımın doğası gereği ortaya çıkan bir
sonuçtur. Bu anlamda yukarıdaki örneğe çok biçimliliği aşağıdaki gibi uyguladım.
public class Soyutlama
{
public static void Yazalim(SoyutTemel[] t)
{
for (int i=0;i<t.length;i++)
{
t[i].Kimlik();
}
}
public static void main(String[] args)
{
Tureyen1 t1=new Tureyen1();
Tureyen2 t2=new Tureyen2();
SoyutTemel[] dizi=new SoyutTemel[2];
dizi[0]=t1;
dizi[1]=t2;
Yazalim(dizi);
}
} |
Uygulamayı bu haliyle derleyip çalıştırdığımda
aşağıdaki ekran görüntüsünü elde ettim.

Burada çok biçimliliği uygularken, SoyutTemel
sınıfından bir nesne dizisi kullandım. Ancak buradaki new operatörü, bir nesneye
ait yapılandırıcıları çağıran new operatörü ile aynı değildir. Tek yaptığımız,
SoyutTemel sınıfından nesneler taşıyacak iki elemanlı bir dizi tanımlamaktır.
Keza, bu dizinin elemanları, abstract sınıfımızdan türeyen sınıf nesne
örnekleridir. Çok biçimliliği, Yazalim metodunda uyguluyoruz. Burada SoyutTemel
abstract sınıfından nesneler, kendisine atanan türeyen sınıflara ait Kimlik
metodlarını çağırıyor. İşte böylece abstract sınıflarada çok biçimliliği
uygulatabildiğimizi görmüş oluyoruz.
Abstract sınıf tanımlamalarında dikkat çeken bir
diğer unsurda, bu sınıfın abstract metodlar dışındada normal sınıfların sahip
olduğu üyelere sahip olabilmesidir. Normal metodlar veya alanlar. Normal bir
metodu türeyen sınıflara ait nesneler üzerinden kalıtımın bir gereği olarak
çağırabiliriz. Ancak bu metodları, abstract sınıfından bir nesne örneği
üzerinden çağıramıyacağımızda kesindir. Çünkü, abstract sınıflardan bir nesne
örneği oluşturamayız. Böyle bir durumda tanımlandığı sınıfın nesne örneğine
ihtiyaç duymayan static metodları kullanabiliriz. Bu aslında, herhangibir
örneğinin kesinlilikle türetilmesini istemediğimiz nesnelere ait bir takım
metodları uygulatmakta kullanılabileceğimiz bir tekniktir.
Gelelim abstract sınıflardaki yapıcı metodların
işleyişine. Bir abstract sınıfa ait nesne örneğinin kesinlikle
oluşturulamıyacağını biliyoruz. Ancak bir abstract sınıfın yapıcılar
içerebildiğinide bilmekteyiz. Bununla birlike bitmek tükenmek bilmeyen
bilgilerimiz yanında, abstract sınıftan türeyen bir sınıfın nesne örneğinin,
kalıtımın doğası gereği ilk olarak abstract sınıf içerisindeki yapıcı metodu
işleyeceğinide biliyoruz. Buraya kadar aslında benim içinde enterasan
gelebilecek bir olgu yok. Ancak aşağıdaki gibi bir kod ne gibi sonuçlar
doğurabilir?
SoyutTemel()
{
Kimlik();
} |
Burada abstract sınıfın içerisinden, abstract
olarak tanımlanmış Kimlik metodu çağırılmıştır. Bu metodun çağırılacağı kesindir. Çünkü,
abstract sınıftan türetilen bir sınıfın nesne örneği oluşturulurken önce,
abstract sınıftaki yapıcı ve bu yapıcı içindende, türeyen sınıftaki
override edilmiş abstract metod çağırılacaktır. Ancak işin içine, yapıcılar
içerisinde ilk değer atamaları girdiğinde durum biraz garipleşir. Bunu
açıklayabilmem için aşağıdaki gibi bir örnek geliştirmem gerekti.
abstract class SoyutTemel
{
public static void Yaz()
{
System.out.println("Ben soyut temel
sinifim");
}
abstract public void Kimlik();
SoyutTemel()
{
System.out.println("Soyut metoddan
once");
Kimlik();
System.out.println("Soyut metoddan
sonra");
}
int deger=40;
}
class Tureyen1 extends SoyutTemel
{
int No;
public void Kimlik()
{
System.out.println("Ben SoyutTemel
siniftan tureyen Tureyen1 Kimlik No:"+No);
}
Tureyen1()
{
System.out.println("TUREYEN 1
YAPICISI");
No=154528;
}
}
class Tureyen2 extends SoyutTemel
{
int No;
public void Kimlik()
{
System.out.println("Ben SoyutTemel
siniftan tureyen Tureyen2 Kimlik No:"+No);
}
Tureyen2()
{
System.out.println("TUREYEN 2
YAPICISI");
No=458569;
}
}
public class Soyutlama
{
public static void Yazalim(SoyutTemel[] t)
{
for (int i=0;i<t.length;i++)
{
t[i].Kimlik();
}
}
public static void main(String[] args)
{
Tureyen1 t1=new Tureyen1();
Tureyen2 t2=new Tureyen2();
SoyutTemel[] dizi=new SoyutTemel[2];
dizi[0]=t1;
dizi[1]=t2;
Yazalim(dizi);
}
} |
Öncelikle uygulamayı inceleyelim. Tureyen
sınıflardan bir nesne oluşturulduğunda, kalıtımın bir gereği olarak, ilk önce
SoyutTemel sınıfının yapıcısı çağırılacaktır. Burada ise, türeyen sınıflarda
override edilen Kimlik metodu çağırılmıştır. Kimlik metodu, override edildiği
sınıfın (Tureyen1,Tureyen2) yapıcısındaki No değerini ekrana yazdırmalıdır.
Ancak durum böyle olmaz. Çünkü henüz, SoyutTemel abstract sınıfının yapıcısı
sonlandırılmamıştır. Bu nedenle, No alanın değeri, java tarafından integer
değişkenlerin alacağı varsayılan değeridir, yani 0'dır. Dolayısıyla override
edilen metod içinden, No alanının ilgili türeyen sınıf yapıcısında belirlenen
değeri elde edilememiştir. Ancak abstract sınıfın yapıcısı işlevini bitirdikten
sonra, türeyen sınıftaki yapıcı çağırılacak ve No alanının değeri yapıcılarda
belirlenen halini alıcaktır. Bu nedenle uygulamanın ekran çıktısı aşağıdaki biri
olur.

Soyut sınıfların, kalıtımda, türeyen sınıflar için
birleştirici bir rol üstlenerek, onların uygulaması gereken metodları belirten
bir rehber olduğunu söyleyebiliriz. Burada türeyen sınıflara yönelik bir
zorlamada söz konusudur. Nitekim, abstract sınıfların tanımlanması ile
oluşturulan abstract metodlar, mutlaka ve mutlaka türeyen sınıflarda override
edilmelidir. Söz gelimi yukarıdaki örneğimizde, Tureyen1 sınıfında Kimlik
metodunu override etmediğimizi düşünelim. Bu durumda aşağıdaki gibi bir derleme
zamanı hatası alırız.

Görüldüğü gibi derleyici, Kimlik metodunun
abstract olmasına rağmen, türeyen sınıf içerisinde tanımlanmadığını belirtiyor.
Oysaki normal bir sınıfın kalıtımda temel sınıf olarak rol oynamasında, türeyen
sınıfların, temel sınıftaki metodları override etmek gibi bir zorunluluğu
yoktur. İşte buda abstract sınıfları normal sınıflardan ayıran diğer bir
olgudur.
Soyutlama ile ilgili temelleri aştığımı düşündüğüm
bir sırada kafama oldukça güzel sorular geldi. Nerde geldiğini sormayın nitekim
bunları düşünürken, Winston Churchill ile görüşmedeydim. Doğruyu
söylemek gerekirse bu adam her zaman dimamı açıyor.
Acaba bir abstract sınıftan başka abstract
sınıflar türetebilir miydim? Türetirsem, türeyen abstract sınıflarda, abstract
olmanın bir gereği olarak, temel abstract sınıftaki abstract metodları bildirmem
gerekir miydi? Peki ya böyle bir hiyerarşide, en altta yer alan abstract sınıftan
normal bir sınıf türetirsem, bu sınıf içinde, hiyerarşideki abstract metodların
akibeti ne olurdu? Önce işe bir abstract sınıftan başka bir abstract sınıf
oluşturmakla başladım.
abstract class TemelSoyut
{
abstract public void Metod1();
abstract public int Metod2(int a,int b);
abstract public double Sec();
public static void Yaz()
{
System.out.println("BEN ABSTRACT
TEMEL SINIFIM");
}
}
abstract class TureyenSoyut1 extends TemelSoyut
{
}
public class Soyutlama2
{
public static void main(String[] args)
{
}
} |
Öncelikle programın başarılı bir şekilde
derlendiğini söyleyebilirim. İlk aşamada iki sorumun cevabını almıştım. Soyut
sınıflardan yeni soyut sınıflar türetilebiliyordu. Bununla birlikte, türeyen
soyut sınıf içerisinde, temel soyut sınıfta tanımlanmış abstract metodların
override edilme zorunluluğu gibi bir durum söz konusu olmamıştı. Gelelim asıl
önemli noktaya. Yani türeyen abstract sınıftan bir başka sınıf türetmeye. Örneği
biraz daha geliştirdim. Türeyen abstract sınıf içerisinede başka abstract metod
bildirimleri ekledim.
abstract class TemelSoyut
{
abstract public void Metod1();
abstract public int Metod2(int a,int b);
abstract public double Sec();
public static void Yaz()
{
System.out.println("BEN ABSTRACT
TEMEL SINIFIM");
}
}
abstract class TureyenSoyut1 extends TemelSoyut
{
abstract public void YaziYaz();
}
class Deneme extends TureyenSoyut1
{
} |
Bu haliyele uygulamayı derledim ve aşağıdaki gibi
bir hata mesajı aldım.

Oldukça şaşırtıcı bir durumla karşılaşmıştım.
Derleyici şu an için sadece TureyenSoyut1' deki YaziYaz abstract metodunu
override etmem gerektiğini söylüyordu. Oysaki, TureyenSoyut1 abstract sınıfıda
başka bir abstract sınıftan türemişti. Benim umduğum, TemelSoyut sınıfında yer alan ve
override edilmeyen abstract metodlar içinde hatalar alabilmekti. Bana düşen
öncelikle YaziYaz metodunu override etmekti.
class Deneme extends TureyenSoyut1
{
public void YaziYaz()
{
System.out.println("YAZI YAZIYORUM");
}
} |
Şimdi uygulamayı derlediğimde, ilk defa gördüğüm
bir derleme hatası için bu kadar çok sevinmiştim. Bu kez derleyici,
TureyenSoyut1 abstract sınıfının türediği TemelSoyut sınıfındaki Sec metodunun
override edilmediğini belirtiyordu. Dünyalar benim oldu desem yeridir.

Bu hatalar sinsilesinin tek handikapı böyle tek
tek ekrana gelmesiydi. Yani aslında hata mesajından, override edilmesi gereken
tüm metodlar için bir uyarı alamıyordum. Yinede, türetilen abstract sınıflardan
türeyen sınıfların abstract sınıf hiyerarşisindeki tüm metodları override etmesi
gerektiği sonucuna varabilmiştim.
Soyut sınıflar ile ilgili olarak dikkat edilmesi
gereken pek çok nokta var. Bu noktaları iyice kavrayıncaya kadar, aşağıdaki
tabloda görüldüğü gibi not etmeyi uygun görüyorum. Eğitimde ezberciliğe her
zaman karşıyımdır. Üniversite yıllarındayken, kalın not defteleri ile birlikte
öğrendiklerimi yanımda taşırdım. Taki öğrendiklerim üzerinde yorumlar ve
felsefik söylemlerde bulununcaya dek. O günler geldiğinde not defterlerini rafa
kaldırırdım. Şimdi ise, yanımda not defteri yerine Laptop taşıyorum. Ama yinede
ilk öğrendiğim ve üzerlerinde henüz felsefik söylemler yapamadığım kavramları
dosyalarda saklıyor ve yeri geldikçe bakıp hatırlıyorum. İşte aşağıdaki
tabloyuda bu yaklaşım ile hazırladım.
|
Soyutlamada Dikkate Değer Noktalar |
|
1 |
Soyut sınıflardan nesne
örnekleri oluşturulamaz. |
|
2 |
Bir sınıfın soyut olabilmesi
için en azından bir tane soyut metod bildirimine sahip olması gerekir. |
|
3 |
Soyut sınıflardan türetilen
sınıflar, soyut metod bildirimlerini mutlaka override etmelidirler. |
|
4 |
Soyut sınıflardan türeyen
nesne örneklerini, soyut sınıf nesnelerine referans olarak aktararak çok
biçimliliğin uygulanmasını sağlayabiliriz. |
|
5 |
Soyut sınıflardan başka soyut
sınıflarda türetilebilir. |
|
6 |
Soyut metodları private olarak
tanımlayamayız. |
|
7 |
Soyut sınıflara ait yapıcılar
tanımlayabiliriz. Ancak bu yapıcıları soyut tanımlayamayız. |
|
8 |
Static bir soyut metod
bildirimi yapamayız. |
|
9 |
Soyut sınıflar içerisinde soyut metod
bildirimleri dışında, normal metodlar ve alanlar tanımlayabiliriz. |
Bu hafta boyunca soyutlama ile ilgili bu
kavramları inceledikten sonra, nesne yönelimli programlama dillerinde soyutlama
ile çok sık karıştırılan bir kavramı incelemeye karar verdim. Sanırım bir
sonraki kahve molamda, Interface (arayüz) kavramını inceleyeceğim.
Burak Selim ŞENYURT
selim@bsenyurt.com