Comparable etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
Comparable etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

8 Eylül 2011 Perşembe

Kategorize Etmenin Yararları ve Arayüzler

Her ne kadar Bülent Ortaçgil, beni kategorize etme, demişse de, kategorizasyon çevremizi anlamamızı kolaylaştıran ve sıklıkla başvurduğumuz çok önemli bir keşif aracıdır. Birbirlerine benzesin ya da benzemesin, değişik türden varlıkların ortak yönlerini gruplamamızı olanaklı kılarak soyutlama yapmamızı sağlayan bu araç, keşfimizin paylaşılabilir olması durumunda, kendimizi daha kısa yoldan anlatmamızı da olanaklı kılar. Örneğin, üçüncü bir şahsın obez olduğu konusundaki gözlemimizi paylaştığımızda, karşımızdaki kişi, hangi ulustan olursa olsun, konuşmaya konu olan kişinin saçının, gözlerinin ne renk olduğunu bilemese de nasıl vücut hatlarına sahip olduğunu kafasında canlandırılabilir. Çünkü, ortak tanımı üzerinde anlaşılmış—yani, standart bir tanımı bulunan—obez olma özelliği kişilerin ait olduğu grupları aşan bir özelliktir; birbirleriyle hiç alakası bulunmayan grupların üyeleri arasında ortak payda olarak kullanılabilir. Yani, kafamızın içindeki gri materyalin tasnif etme donanımı, biz istesek de istemesek de, diğer kişileri müstahdem, öğretmen, yönetici, vb. şeklinde sınıflandırdığı gibi, obez, zeki, işbilir, vb. şeklinde de kategorize etmektedir. Yeri geldiğinde, sınıflarla kategoriler bitiştirilmekte ve obez öğretmen, işbilir yönetici gibi yeni sınıflamalar da yapılabilmektedir.1

İşte bu yazımızda, Java programlama dilinin nesneler alemini tasnif etmekte sınıf ile birlikte kullanılabilecek ikinci üstkavramı olan arayüz kavramına değineceğiz. Ama ilk önce pedagojik senaryomuz. Yer ülkemizdeki liselerden birinin müdüriyeti ve uyuşturucunun ilk paketinin bedava olduğunu bildiği için bazı şirketlerce öğretim ordusunun üyelerine "bedava" dağıtılan programlama dillerini kullanmayı reddederek Java'yı tercih eden müdürümüz, yönetimindeki öğrenci ve öğretmenlerin performansını özetleyecek bir programı nasıl yazabileceğini düşünmekte. Müdürümüzün amacı, öğrenci ve öğretmenleri kendi aralarında uygun bir ölçüte göre sıralayıp listelemek. Çiziktirmelerle dolu sayfalara baktığımızda, müdürümüzün öğrencileri önce ortalamaları sonra—ortalamaların eşit olması durumunda—numaraları karşılaştırırak sıraya dizmeyi düşünürken, öğretmenleri önce öğrenci anketlerindeki konu yeterliliği notunun ortalamasını sonra ders yükünü karşılaştırırak sıraya dizmek istediğini görüyoruz.

Yordamsal programlama alışkanlıkları olduğu anlaşılan müdürümüz, ilk çözüm girişiminde Performans adlı bir sınıftaki dört metot ile işini halletmeye çalışmış. Buna göre, sıralama metotları kendilerine geçirilen veri kümesinin elemanlarını ilişkin karşılaştırma metodunu çağırarak sıraya dizmekte.

Performans.java
public class Performans {
  ...
  public static int karşılaştır(Öğrenci sol, Öğrenci sağ) {
    ...
  } // int karşılaştır(Öğrenci, Öğrenci)

  public static int karşılaştır(Öğretmen sol, Öğretmen sağ) {
    ...
  } // int karşılaştır(Öğretmen, Öğretmen)

  public static void sırala(Öğrenci[] veri) { ... }

  public static void sırala(Öğretmen[] veri) { ... }
  ...
} // Performans sınıfının sonu

PerformansÖğrenci ve Öğretmen sınıflarına bağımlı kılması—Öğrenci ve Öğretmen sınıflarının değişmesi ilişkin karşılaştırma metotlarının değişmesine neden olabilir—ve sıralama metotlarının neredeyse aynı olmasından—ne de olsa, aynı algoritma kullanılıyor—huylanan müdürümüz, static niteleyicisinin nesne paradigmasının düşmanı olduğunu anımsaması ve, düşülen nota inanırsak, İnternet'teki bir yazıyı🔎 okuması sonrasında, sınıf ve kalıtlama kavramlarının kullanıldığı bir çözümü yeğleyerek bu çözümden vazgeçmiş.

Karşılaştırılabilir.java
public abstract class Karşılaştırılabilir {
  public abstract int karşılaştır(Karşılaştırılabilir sağ);
} // Karşılaştırılabilir sınıfının sonu

Öğrenci.java
public class Öğrenci extends Karşılaştırılabilir {
  ...
  public int karşılaştır(Karşılaştırılabilir sağ) {
    Öğrenci sağTaraf = (Öğrenci) sağ;
    ... // ileti alıcı ile sağTaraf'ı karşılaştır.
  } // int karşılaştır(Karşılaştırılabilir)
  ...
} // Öğrenci sınıfının sonu

Öğretmen.java
public class Öğretmen extends Karşılaştırılabilir {
  ...
  public int karşılaştır(Karşılaştırılabilir sağ) {
    Öğretmen sağTaraf = (Öğretmen) sağ;
    ... // ileti alıcı ile sağTaraf'ı karşılaştır.
  } // int karşılaştır(Karşılaştırılabilir)
  ...
} // Öğretmen sınıfının sonu

Performans.java
public class Performans {
  ...
  public static void sırala(Karşılaştırılabilir[] veri) {
    ...
  } // void sırala(Karşılaştırılabilir[]) sonu
  ...
} // Performans sınıfının sonu

Anlaşılan, yukarıda kaba hatlarıyla verilen çözümün, Java'nın çoklu sınıf kalıtlamayı desteklememesi nedeniyle, Öğrenci ve Öğretmen'in bir diğer sınıftan daha—mesela, iki sınıfın ortak noktalarını içeren Kişi gibi—türetilmesini engellediğini gören ve karşılaştırmanın genel bir işlem olduğunu düşünen müdürümüz, Karşılaştırılabilir'in aslında gereksiz olduğuna karar vermiş ve söz konusu işleme karşılık gelen iletiyi Object sınıfının içinde aramış. Buna gerekçe olarak da, equals (eşitlik denetimi) ve toString (hoş yazım) gibi tüm nesneler için geçerli olan iletilerin tüm sınıfların ortak noktalarını barındıran Object'te yer alması gerektiğini not düşmüş. Görünen o ki, bu varsayımın arama sonucunda yanlışlanması, müdürümüzü Java programlamayı iyi bilen bir matematik hocasının kapısını çalmaya zorlamış. Derdinin çaresinin Object sınıfında değil, arayüz kavramının kullanımında yattığını söyleyen matematik hocası, küme, matris, karmaşık sayı gibi matematiksel nesnelerin aslında karşılaştırılabilir olmadığı konusunda garanti vererek müdürümüzü ikna etmiş. Gelin bundan sonrasını birlikte görelim.2

Birbirleriyle ilintili veya ilintisiz olabilecek kimi kavramların ortak özelliklerini soyutlamaya yarayan kategoriler Java'da arayüz üstkavramı ile karşılık bulurlar; bir arayüzün tanımlanması, Java sınıf kitaplığını o arayüzü gerçekleştiren ve gerçekleştirmeyen sınıflar olmak üzere iki kategoriye ayırır. Bu tanımda altı çizilmesi gereken noktalar, aynı arayüzü gerçekleştiren sınıfların birbirleriyle ilintisiz olma ve kimi sınıfların arayüzü gerçekleştirmeme olasılığıdır. Örneğin, karşılaştırılabilir sınıflar, Öğretmen ve Müstahdem gibi ortak üstsınıfları (Çalışan) yoluyla ilintilendirilebilirken, tamsayılar ve vergi mükellefleri gibi alakasız da olabilirler. Aynı şekilde, tamsayılar karşılaştırılabilirlik özelliği taşırken, disk dosyalarını soyutlayan sınıfların aynı özelliği taşıması beklenmez.3

Karşılaştırılabilir.java
public interface Karşılaştırılabilir {
  public int karşılaştır(Object);
  /* public static final */ String
    BELİRLEYİCİ_KOMİTE = "Ağır Abiler, A.Ş.";
} // Karşılaştırılabilir arayüzünün sonu

Arayüz tanımı, destekleyen sınıfların gerçekleştirmesi zorunlu olan iletilerin imzalarını ve bu sınıfların üyesi olan nesnelerin tümü için değişmeyen özellikleri içerir. Örneğin, yukarıdaki arayüz karşılaştır adlı bir iletiyi gerçekleştiren sınıflara ait nesneleri soyutlarken, bu nesnelerin ele alınması esnasında yararlanılacak bir sabit tanımlamaktadır.

Arayüz tanımının kuru kuruya kullanıcılara sağlanması pek yararlı olmayacaktır; arayüzü belirleyen geliştiricilerin, arayüzün gerçekleştirimleri arasında tutarlılığı sağlamak adına, derleyicinin kullanım doğruluğu denetimleri için gerekli olan yukarıdaki sözdizimsel tanımın yanısıra, arayüzdeki öğelerin anlamlarına dair açıklamaları da sağlaması gerekir. Örneğin, karşılaştır iletisinin imzasının sağlanması yalnız başına bir işe yaramayacaktır; iletinin ne yaptığına, parametrelerin ve döndürülen değerin ne anlama geldiğine aşağıdaki gibi açıklık getirilmelidir. Yani, arayüzün sözdizimsel tanımına ek olarak anlambilimsel açıklamasının da sağlanması gereklidir.
karşılaştır iletisi, hedef nesne ile argümanda geçirilen nesneyi karşılaştırır. Hedef nesnenin büyük olması durumu döndürülen pozitif bir int ile bildirilirken, eşitlik durumu 0 ile, küçük olma durumu ise negatif bir int ile bildirilir. Hedef nesne ile argümandaki nesnenin uyumsuz olması, kullanıcıya ClassCastException ayrıksı durumuyla bildirilmelidir. Benzer şekilde, argümanda geçirilen tutacağın null olmasının, kullanıcıya NullPointerException ayrıksı durumuyla bildirilmesi beklenir.

Arayüzün gerçekleştirimci ile kullanıcıları arasında sözleşme görevi gören bir bağlantı noktası oluşturduğu unutulmamalıdır; hem gerçekleştirimci hem kullanıcı, arayüz tanımından değişik amaçlarla da olsa yararlanacaktır. Bu, arayüz tanımındaki bir değişikliğin, birbirlerinden çok farklı yerlerde bulunan pek çok kişinin kodunu değiştirmesi gibi maliyetli bir sonuca neden olabileceği anlamına gelir. Dolayısıyla, değişme ihtimali yüksek olan altalan tanımı ve metot gövdesi gibi gerçekleştirim ayrıntılarının arayüz tanımında yer almasına izin verilmez. Altalan ve metot gerçekleştirimlerinin olması gerektiği durumlarda, arayüz yerine soyut sınıf kullanımı düşünülmelidir.

Arayüz tanımındaki tüm öğelerin public erişimli ve tüm veri öğelerinin derleme anı sabiti—yani, static final ile nitelenmiş—olduğu varsayılır. Bunun nedeni, arayüzün, önceki paragrafta değindiğimiz gibi, birbirinden bağımsız gerçekleştirimci ve kullanıcılar arasında sözleşme görevini görmesidir; arayüzde değişmesi muhtemel ve kamuya açık olmayan gerçekleştirim ayrıntıları bulunamaz. [Müzik setinizin arayüzünde kullandırılmayan bir düğmenin bulunmasını mantıklı bulur musunuz?] Bu sayede kullanıcı, arayüzü hangi sınıf gerçekleştiriyor olursa olsun, arayüz tutacağını arkasındaki nesnenin türünden bağımsız bir şekilde ele alıp kodun yeniden kullanımını sağlayabilecektir. Bunu, aşağıdaki örnek üzerinden görelim.
public class Performans {
  ...
  public static void sırala(Karşılaştırılabilir[] veri) {
    if (veri.length <= 1) return;

    boolean takasYapıldı = false;
    int geçiş = 1;
    do {
      takasYapıldı = false;
      for (int i = 0; j < veri.length - geçiş; i++)
        if (veri[i].karşılaştır(veri[i + 1]) > 0) {
          Karşılaştırılabilir geçici = veri[i];
          veri[i] = veri[i + 1];
          veri[i + 1] = geçici;
          takasYapıldı = true;
        } // if sonu
      geçiş++;
    } while(geçiş < veri.length && takasYapıldı);
  } // void sırala(Karşılaştırılabilir[]) sonu
  ...
} // Performans sınıfının sonu
Yukarıdaki koda göre, sırala metodu Karşılaştırılabilir arayüzünü gerçekleştiren herhangi bir sınıfın nesnelerini gösteren tutacaklardan oluşan bir dizi alabilir. Metot içinde, geçirilen dizinin elemanlarının Karşılaştırılabilir arayüzünde listelenen özellikleri kullanılabilecektir. sırala metodunu nesnenin diğer özelliklerinden koparan bu kısıtlama, bir nesneye hangi iletilerin gönderilebileceğinin tutacak süzgecinden geçmesi gerektiği kuralı ile de uyumludur: arayüz tutacağı ile gösterilen bir nesneye, tutacağın izin verdiği—yani, ilişkin arayüzde listelenen—iletiler gönderilebilir; iletinin gönderilmesi sonrasında nesnenin ait olduğu sınıftaki aynı imzalı metot çağrılacaktır. Örneğin, programın çalıştırılması sonrasında yukarıda işaretlenmiş olan satırdaki gönderinin sonrasında hangi metodun çağrılacağına geçirilen dizinin eleman türüne göre çalışma anında dinamik iletimle karar verilecektir. Bir diğer deyişle, bir sınıftaki iletilerin söz konusu sınıfın kalıtlama [ilişkisi] ile ilişkilendirildiği altsınıfların nesnelerine gönderilmesi durumunda olduğu gibi, bir arayüzde listelenen iletiler, söz konusu arayüzün gerçekleştirme [ilişkisi] ile ilişkilendirildiği sınıfların nesnelerine gönderilmeleri durumunda çokbiçimli olarak davranır. Dolayısıyla, sırala Karşılaştırılabilir arayüzünü gerçekleştiren tüm sınıflar için işini sınıfın özelliklerine göre yaparak çalışacak ve bu sayede kodun yeniden kullanımını sağlayacaktır.

İlgilenenlerin bir arayüzü gerçekleştirmesi, ilişkin sınıf ile arayüz arasındaki gerçekleştirme ilişkisinin sınıf başlığında belirtilmesi ve arayüzde listelenen tüm iletilere karşılık metotların sağlanması ile mümkün olur. Sınıf ve arayüz arasındaki ilişki, sınıf adından sonra implements anahtar sözcüğü yoluyla belirtilir; kalıtlama ilişkisine dair bir bilginin bulunması durumunda, gerçekleştirme ilişkisine dair bilginin bunu takip etmesi gerekir.
public class Öğrenci implements Karşılaştırılabilir {
  ...
  public int karşılaştır(Object sağ) {
    if (this == sağ) return 0;

    Öğrenci sağTaraf = (Öğrenci) sağ;

    if (_ortalama > sağTaraf._ortalama) return 1;
    else if (_ortalama < sağTaraf._ortalama) return -1;
    if (_no > sağTaraf._no) return 1;
    else if (_no < sağTaraf._no) return -1;
      else return 0;
  } // int karşılaştır(Object) sonu
  ...
  private float _ortalama;
  private long _no;
} // Öğrenci sınıfının sonu
public class Öğretmen implements Karşılaştırılabilir {
  ...
  public int karşılaştır(Object sağ) {
    if (this == sağ) return 0;

    Öğretman sağTaraf = (Öğretmen) sağ;

    if (_ankNotu > sağTaraf._ankNotu) return 1;
    else if (_ankNotu < sağTaraf._ankNotu) return -1;
    if (_dersYükü > sağTaraf._dersYükü) return 1;
    else if (_dersYükü < sağTaraf._dersYükü) return -1;
      else return 0;
  } // int karşılaştır(Object) sonu
  ...
  private float _ankNotu;
  private int _dersYükü;
} // Öğretmen sınıfının sonu
Gerektiği durumlarda, bir sınıf birden çok sayıda arayüz gerçekleştirebilir; bu, gerçekleştirilen arayüzlerin birbirlerinden ',' ile ayrıldığı arayüz listesinin implements'ten sonra belirtilmesi ile mümkün olur. Böyle bir sınıfın kullanıcıları, bu sınıfa ait bir nesneyi sınıf ve üstsınıflarının tutacaklarının yanısıra herhangi bir arayüzünün tutacağı yoluyla da kullanabilir.

Kimi zaman, yeni bir arayüz daha önceden tanımlanmış olan bir arayüzün ileti listesine ekler yapılarak tanımlanabilir. Yazılımların sürüm değişikliği ile birlikte arayüzlerin yeni uyarlamalarının tanımlanması buna tipik bir örnektir. Bu gibi bir durumda, yeni arayüz sıfırdan tanımlanabileceği gibi, eski arayüzden kalıtlayacak şekilde tanımlanabilir. Bu yolu seçmenin getirisi, derleyicinin arayüzler arasındaki kalıtlama ilişkisini sınıflar arasındaki kalıtlamaya benzer bir şekilde yorumlayacak olmasıdır: yeni arayüz (altarayüz) eski arayüz (üstarayüz) gibidir; eski arayüzdeki iletilerin tümünü içeren yeni arayüzün tutacağı, eski arayüz türündeki tutacağın kullanıldığı her yerde kullanılabilir. Ne kastettiğimizi aşağıdaki tanımlar üzerinden görelim.
public interface A1 {
  void i1();
  void i2();
} // A1 arayüzünün sonu
public interface A2 extends A1 {
  void i1(int i);
  void i3();
} // A2 arayüzünün sonu
Sağlanan tanımlara göre, A2 arayüzünü destekleyen bir sınıfın A1'dekileri de içeren dört iletinin karşılığındaki metot gerçekleştirimlerini sağlaması gerekecektir: i1(), i2(), i1(int) ve i3().4 Bu noktada, arayüz kalıtlamanın, sınıf kalıtlamanın aksine, çoklu olduğu unutulmamalıdır. Yani, bir arayüz birden fazla üstarayüzden kalıtlayabilir. Çoklu arayüz kalıtlamadan yararlanıldığı durumlarda, üstarayüzlerden gelen öğelerin ad çakışmalarına dikkat edilmelidir. Aynı imzaya sahip olup değişik dönüş türüne sahip iletilerin kalıtlanması derleme hatasına neden olacağı gibi, aynı ada sahip olup değişik arayüzlerde farklı türlere ait tanımlanan sabitler, kullanıldıkları noktada derleme hatasına yol açacaklardır.

Son olarak, arayüzlerin önemli bir görevine değinerek yazımızı bitirelim: yazılım bileşenleri için standart oluşturmayı mümkün kılmaları. Bileşen standartları, bir bileşenin aynı arayüzleri destekleyen bir diğeri yerine tak-çıkar mantığıyla kodun güncelllenmesini gerektirmeden değiştirilmesine olanak tanır. [Yani, eskimesi nedeniyle doğramanızı değiştirmek istediğinizde duvarın yıkılmasına gerek olmayacaktır.] Bunu aşağıda verilen zamana göre sıraya dizilmiş çizelgeyi izleyerek görelim.

  1. Bir grup uzman, ortaya çıkan ihtiyaç üzerine A1 arayüzünü tanımlar.
  2. Ş1 şirketi, A1'i gerçekleştiren Ş1_A1 sınıfını piyasaya sürer.
  3. Ş2 şirketi, Ş1'den Ş1_A1'i satın alır ve kendi programlarında kullanmaya başlar.
  4. Ş3 şirketi, A1'i gerçekleştiren Ş3_A1 sınıfını piyasaya sürer.
  5. Ş1_A1'in performansından muzdarip Ş2 şirketi, Ş3'ten Ş3_A1'i alır ve Ş1_A1'in yerine kullanmaya başlar.

Dikkat edilecek olursa, Ş3 şirketi Ş3_A1 sınıfını Ş2 sınıfının geliştirdiği programlardan sonra geliştirmiştir. Bir diğer deyişle, Ş2 şirketinin geliştirdiği programlar, kendilerinden sonra geliştirilen bileşenleri kullanmaktadır. Zaman içinde geleceğe yapılan bu yolculuk, arayüzlerin değişmez özellikler barındırması ve sözdizimlerinin yanısıra anlamlarının da tanımlanmak suretiyle gerçekleştirimci kullanıcı arasında bir sözleşme oluşturması ile mümkün olmaktadır.


  1. Obezlik örneğini çok da ilerilere götürmeyiniz. Çünkü, obez olan birisinin zayıflayarak zaman içinde bu özelliğini yitirmesi olanak dahilindedir ki, böylesine bir özelliğin, nesne yönelimli modelleme dünyasının arayüze benzer bir üstkavramı olan rol ile karşılanması daha doğru olacaktır. Arayüzler, ilişkin nesnelerin kaybetmeyeceği özelliklerini tanımlayabilir.
  2. Sağladığımız çözümdeki türlerin soysal tanımlanarak daha güvenli bir çözümün mümkün olduğunun bilinmesinde yarar var.
  3. Aslına bakılacak olursa, karşılaştırılabilirlik kategorisi Java platformundaki Comparable arayüzü tarafından hali hazırda tanımlanmıştır. Dolayısıyla, Karşılaştırılabilir arayüzüne gerek yoktur.
  4. Yeri gelmişken tekrar hatırlatalım: dönüş türü Java'da aynı aduzayındeki iletileri (ve metotları) ayrıştırmak için kullanılmaz. Dolayısıyla, A2'ye int dönüş türlü i1() iletisinin eklenmesi, ortaya çıkan muğlaklık nedeniyle, derleme hatasına yol açacaktır.

24 Mayıs 2011 Salı

Her Sınıfa Lazım Metotlar

Sınıflarınızı Bileşke Türler-Sınıflar adlı yazıda🔎 önerilen veya diğer kaynaklarda okuduğunuz reçetelere uyarak gerçekleştirdikten sonra bile, programlarınızın sıklıkla beklentileriniz dışında davranış sergilediğini gözlemleyebilirsiniz. Bu hayal kırıklığı kimi zaman yaptığınız bir dikkatsizlikten, kimi zamansa öngöremediğiniz bir işlevselliğin yokluğundan veya eksik gerçekleştirilmiş olmasından kaynaklanır. İşte bu yazıda, tüm sınıflarda özel olarak ele alınıp gerçekleştirilmeleri konusunda karar verilmesi gereken bazı özel metotlara değineceğiz ve Kuantum Tanrısı'nın JSM'nin içine kötü niyetli cinler yerleştirdiği kuruntusundan kurtulmaya çalışacağız.


Hoş Yazım


Hatadan arındırma amacıyla başvurduğunuz çıktı komutlarınızın neden anlaşılmaz şeyler yazdığını açıklamakla başlayalım. Aşağıda verilen [oldukça yetersiz] Öğrenci sınıfını kullanarak ne yapmamız gerektiğine bir bakalım.

Öğrenci.java
import static java.lang.System.*;

public class Öğrenci {
  public static void main(String[] ksa) {
    Öğrenci öğr1 = new Öğrenci(123, "Ayşe", "Öztürk", 3.5F);

    out.println(öğr1);
    // metodun devamı...    
  } // void main(String[]) sonu

  public Öğrenci(long no, String ad, String soyad, float ort) {
    _ad = ad;
    _no = no;
    _ortalama = ort;
    _soyad = soyad;
  } // yapıcı(long, String, String, float) sonu

  public String ad() { return _ad; }
  public long no() { return _no; }
  public float ortalama() { return _ortalama; }
  public String soyad() { return _soyad; }

  private String _ad, _soyad;
  private long _no;
  private float _ortalama;
} // Öğrenci sınıfının sonu
$ javac -encoding UTF-8 Öğrenci.java
$ java Öğrenci
Öğrenci@6e1408
...
İşaretli satır tarafından basılanın bir Öğrenci nesnesi olmakla birlikte, haklı olarak, çıktıda nesne içeriğine dair bilgilendirici bir şey olmadığını düşünebilirsiniz. println (ve print) metotlarının işleyişine bakarak bunun neden tam olarak doğruyu yansıtmadığını anlamaya çalışalım. Söz konusu metotlar herhangi bir türden—ister ilkel tür olsun isterse bileşke tür—argüman alacak şekilde aşırı yüklenmiştir. Bu metotlardan char, char[] ve String argüman bekleyenlerin dışındakiler, aldıkları argümanı önce bir String nesnesi haline dönüştürür, ardından bu katar içindeki karakterleri basarak işini görür. Bileşke türlü değerlerden karakter katarına dönüşüm, aşağıdaki denkliklerden de görülebileceği gibi, önce String sınıfındaki valueOf metodunu çağırarak daha sonra ise bu metottan dönüşümü yapılmak istenen nesneye toString iletisi gönderilerek yapılır.
out.println(2 + öğr1);
out.println(String.valueOf(2) + String.valueOf(öğr1));
out.println(String.valueOf(2) + öğr1.toString()));
Dolayısıyla, sorumlunun öğr1'e toString iletisinin gönderilmesi sonucunda çağrılan aynı adlı metot olduğunu söyleyebiliriz. Ancak, öğr1'in üyesi olduğu Öğrenci sınıfına bakıldığında bu ada sahip bir metodun bulunmadığı görülür. O zaman, olmayan bir metodun çağrılmasıyla hata verilmesi gerekirken neden yukarıdaki çıktı üretilmektedir? Yanıt, söz konusu metodun bir diğer sınıftan kalıtlanmış olmasında yatar. Hoş yazım gibi tüm sınıflarda bulunması beklenen bir işlevsellik çok genel bir şekilde tüm sınıfların ortak atası olan Object sınıfındaki toString metodunun gerçekleştirimi ile sağlanmış ve Öğrenci sınıfının kendi özelliklerini yansıtan bir gerçekleştirim sağlamaması sonrasında derleyici tarafından bu metot kullanılmıştır. Tüm sınıfların ortak paydasından yararlanarak içi doldurulan bu metot ise nesnenin ait olduğu sınıfın adı, '@' ve nesnenin kıyım değerini birleştirerek işini görmektedir.1 Nesnelerimizin çıktı ortamına basıldığında sonucun anlaşılır olmasını istiyorsak, toString adlı bu metodu nesnelerimizin özelliklerini düşünerek gerçekleştirmemiz gerekir. Öğrenci sınıfı için uygun bir gerçekleştirim aşağıda verilmiştir.
...
public class Öğrenci {
  ...
  public String toString() {
    return _no + " " + _ad " " + 
           _soyad + " " + _ortalama;
  } // String toString() sonu
  ...
} // Öğrenci sınıfının sonu

Eşitlik Denetimi


Hangi sınıfa ait olursa olsun tüm nesnelere gönderilmesi söz konusu olabilecek bir diğer ileti, hedef nesneye kendisini aynı türden bir diğer nesne ile eşit olup olmadığını denetlemesini söyleyen equals iletisidir. Başta işin, ilkel türlü değerlerde olduğu gibi, == ile halledilebileceğini, aslında equals iletisine ihtiyaç bile duyulmaması gerektiğini düşünenleriniz olabilir. Bu arkadaşlara, diziler konusunu işlediğimiz yazının🔎 aşağıya kopyaladığımız örneğiyle bunun olanaksız olduğunu hatırlatalım. Çünkü == tutacakların eşitliğini denetler. Bu da, eşitlik denetiminin true sonuç üretmesinin ancak aynı nesneyi gösteren iki tutacakla mümkün olacağı anlamına gelir. Bir diğer deyişle, == nesnelerin aynılık denetimini yapar, eşitlik denetimini değil.
String ad1 = new String("Tevfik");
String ad2 = new String("Tevfik");
System.out.println(ad1 == ad2); // => false
O zaman, Object sınıfında sağlanan equals metodu da eşitlik denetimini yapsaymış keşke diyebilirsiniz. Biraz düşündüğünüzde, farklı yapıya sahip belirsiz sayıda sınıfta eşitlik kavramının birbirlerinden çok farklı olacağını anlarsınız ve bunun da toString'de olduğu gibi sınıf gerçekleştirimcisinin müdahelesini gerektirdiğini görürsünüz. Zira, equals iletisinin tüm sınıflar için ortak payda olarak sunulan Object sınıfındaki metot gerçekleştirimi, == gibi davranır ve aynılık denetimi yapar. Yüksek performanslı bu çözümün isteklerinizi karşılamaması durumunda, aşağıdaki gibi amaçlarınıza uygun bir metodu sağlamanız yerinde olacaktır.
...
public class Öğrenci {
  ...
  public boolean equals(Object diğerNesne) {
    Öğrenci diğerÖğr = (Öğrenci) diğerNesne;

    return _no == diğerÖğr._no;
  } // boolean equals(Object) sonu
  ...
} // Öğrenci sınıfının sonu
Dikkat edecek olursanız, metot gövdesinin ilk satırında parametrede geçirilen Object türlü tutacağı biçimlendirmek suretiyle arkadaki nesneyi bir Öğrenci nesnesi gibi kullanmak isteğimizi bildiriyoruz.2 Böylece, biçimlendirmenin sonucu olarak döndürülen Öğrenci türlü tutacak değerini atadığımız diğerÖğr adlı yerel değişkeni, kaplamının (İng., scope) sonuna kadar Öğrenci sınıfının özelliklerini yansıtacak şekilde kullanma hakkını kazanıyoruz. Bunun sebebi, Object sınıfının tüm Java sınıflarının ortak özelliklerini tutuyor olması ve _no altalanının bu ortak özellikler arasında bulunmaması.

equals metodunun ezilmesi yönünde bir karar verilmesi durumunda, hashCode iletisinin de gözden geçirilmesi gerekecektir. Hedef nesnenin Hashtable, HashMap ve HashSet gibi kapların altyapısında yararlanılan doğrudan erişimli veri yapılarında tutulabilmesi için kıyımdan geçirilerek tamsayı karşılığını döndüren bu iletinin Object sınıfındaki sözleşme tanımına bakılacak olursa, eşit olan nesnelerin aynı kıyım değerine sahip olması gerektiği görülür. Dolayısıyla, aynı öğrenciyi temsil eden farklı nesnelerin eşit ilan edildiği Öğrenci sınıfında, hashCode metodunun da bu eşitliğin altını çizen bir biçimde ezilmesi gerekecektir. Buna göre, aynı öğrenci numarasına sahip tüm Öğrenci nesnelerini eşit ilan eden yukarıdaki equals metoduna koşut hashCode gerçekleştirimi aşağıda verilmiştir.
...
public class Öğrenci {
  ...
  public int hashCode() { return (int) _no; }
  ...
} // Öğrenci sınıfının sonu
Dikkat edecek olursanız, long olan numaranın int olarak biçimlendirilmesi sonucu veri kaybı yaşanmış ve farklı iki öğrenciyi temsil eden Öğrenci nesnelerinin aynı kıyım değerine sahip olma olasılığı belirmiştir. long türünün int türünden daha büyük bir değer uzayına sahip olmasından kaynaklanan bu durum, hashCode sözleşmesini ihlal etmemektedir; istenen aynı öğrenciyi temsil eden nesnelerin eşit kıyım değerlerine sahip olmasını garanti etmektir, ayrı öğrencilerin aynı kıyım değerine sahip olmasını engellemek değil.

hashCode iletisinin kullanımı, nesnelerin kıyım tablosu temelli kaplarda tutulmasına sınırlı değildir. Eşitlik denetiminin uzun zaman alan bir işlem olması durumunda, hashCode olumsuz yanıtın hızlı verilmesini olanaklı kılar. Örnek olarak, binlerce elemana sahip ve ön tarafları birbirleriyle aynı olan eşit uzunluklu iki liste düşünün. Listeleri dolaşıp elemanları karşılıklı olarak eşitlik denetimine tabi tutmaktansa, listelerin öncelikle kıyım değerleri karşılaştırılır. Karşılaştırmanın olumsuz sonuç vermesi, listelerin eşit olmaması anlamına geldiği için hızlı bir biçimde yanıtı bulmamızı sağlayacaktır.3 Dolayısıyla, hashCode metodu da equals ile birlikte ele alınmalıdır.

Kopyalama


Programlamaya yeni başlayıp eşitlik denetiminde tatsız bir sürpriz yaşayanların çoğu, nesne kopyalama işleminde de benzer bir sıkıntı yaşarlar. Bunun sebebi, kopyalama amacıyla ilkleme veya atamadan yararlanmaları ve bu işlemlerin nesne yerine tutacağı kopyalamasıdır. Dolayısıyla, nesneler kopyalanmayacak, paylaşılacaktır. İhtiyacını duyduğumuz şey kopyalayan yapıcı gerçekleştirimidir. Kopyalanmak istenen nesnenin türünden bir parametresi bulunan bu metot, nesne kopyalamaktan ne anlaşıldığını gerçekleştiren bir gövdeye sahip olmalıdır. Öğrenci nesnelerinin kopyalanması için kullanılabilecek bir kopyalayan yapıcı gerçekleştirimi aşağıda verilmiştir.
...
public class Öğrenci {
  ...
  public Öğrenci(Öğrenci diğerÖğr) {
    _no = diğerÖğr._no;
    _ad = new String(diğerÖğr._ad);
    _soyad = new String(diğerÖğr._soyad);
    _ortalama = diğerÖğr._ortalama;
  } // kopyalayan yapıcı sonu
  ...
} // Öğrenci sınıfının sonu
Dikkat edersiniz, _ad ve _soyad altalanları için String sınıfının kopyalayan yapıcısı kullanılıyor. Bu durumda, String nesnelerinin değişmez içerikli karakter katarları tuttuğunu bilenler, haklı olarak, kopyalayan yapıcı yerine ilklemenin daha akılcı olacağını söyleyeceklerdir. Ne de olsa, değişmeyen bir şeyin kopyalanması ile paylaşılması arasında bir fark yoktur; paylaşılan nesnenin değişmesi söz konusu olmayacağı için bir tutacak yoluyla yapılan değişikliğin bir diğer tutacak yoluyla görülmesi gibi bir durum asla ortaya çıkmayacaktır. Dolayısıyla, Öğrenci sınıfı için aşağıda verilen daha verimli gerçekleştirim de işimizi görecektir.
...
public class Öğrenci {
  ...
  public Öğrenci(Öğrenci diğerÖğr) {
    _no = diğerÖğr._no;
    _ad = diğerÖğr._ad;
    _soyad = diğerÖğr._soyad;
    _ortalama = diğerÖğr._ortalama;
  } // kopyalayan yapıcı sonu
  ...
} // Öğrenci sınıfının sonu

Varsayılan Yapıcı


Sizi gafil avlayabilecek bir diğer metot, nesnelerin ilklenmesi esnasında derleyicinin sentezlediği kod tarafından arka planda usulca çağrılabilecek olan argümansız yapıcıdır. Programcının talebi ile çağrılmayıp derleyicinin araya girerek yapıcı metot çağrılması gerekliliğine hükmettiği durumlarda çağrıldığı için varsayılan yapıcı olarak adlandırılan bu yapıcı da sınıf gerçekleştirimi sırasında göz önünde bulundurulmalıdır. Mesela, geliştirmekte olduğunuz sınıftan kalıtlayan bir sınıfın nesnesinin yaratılması sırasında sizden habersiz bir şekilde sınıfınızın varsayılan yapıcısı çağrılacaktır. Dolayısıyla, böylesine bir senaryoyu öngörmeniz durumunda varsayılan yapıcıyı sağlamanız yerinde bir hamle olacaktır.
...
public class Öğrenci {
  ...
  public Öğrenci() { }
  ...
} // Öğrenci sınıfının sonu
Bu noktada, derleyicinin varsayılan yapıcı sentezleme noktasındaki yardımını bir kez daha hatırlatarak, bu desteğin programcı tarafından herhangi bir yapıcının sağlanması durumunda geçerli olmadığını yineleyelim. Bundan dolayı, yukarıdaki işlevsiz gibi gözüken yapıcı gerçekleştirimi zorunludur.

Karşılaştırma


Sınıf gerçekleştirimi sırasında gerekliliğine karar vermemiz icap eden bir diğer işlem, sınıfımızın bir nesnesini aynı türden bir diğeri ile karşılaştırma işlemidir. Biraz kolaycı olanlarınız, toString ve equals'da olduğu gibi, aynı türe ait nesnelerin öncelik-sonralık sıralamasını saptayan bu işleme karşılık da Object sınıfında bir metot bulunduğunu ve yapılması gerekenin sınıfımızın ihtiyaçlarına göre bu metodun ezilmesi olduğunu düşünebilirler. Ancak, karşıt bir örnek kolay varılan bu yargının tüm sınıflar için doğru olmadığını gösterir. Örneğin, matemetikteki küme kavramını gerçekleştirmekte olduğumuzu düşünün; kümelerin sıraya dizilmesinin, iki kümeden hangisinin daha önce/sonra geldiği sorusunun pek de mantıklı olmadığı görülecektir. Dolayısıyla, karşılaştırma işlemi hoş yazım ve eşitlik denetimi işlemlerinden farklı ele alınmalıdır.

Derdimizin çaresi, Java alemindeki sınıfları karşılaştırılabilir olanlar ve olmayanlar şeklinde ikiye ayırmaktır. Aradığımız bu çözüm bize arayüz üstkavramınca sağlanan tasnifin ta kendisidir. Birbirleriyle ne kadar alakasız olurlarsa olsunlar, aynı arayüzü gerçekleştiren iki (veya daha fazla sayıda) sınıf, arayüz tutacağı vasıtasıyla görüldükleri takdirde aynı kullanılabilirliğe sahip olacaklardır; söz konusu sınıfların nesneleri kendi türlerinden bir diğer nesne ile karşılaştırılabilecek ve bu işlemin sonucunda sıraya dizilebileceklerdir. Dolayısıyla, yapmamız gereken Java platformundaki nesneler için karşılaştırılabilirlik kategorisini tanımlayan Comparable arayüzünü sınıfımızda gerçekleştirmektir. Bunun örneği aşağıda verilmiştir.
...
public class Öğrenci implements Comparable<Öğrenci> {
  ...
  public int compareTo(Öğrenci diğerÖğr) {
    if (_ortalama > diğerÖğr._ortalama)
      return 1;
    else if (_ortalama < diğerÖğr._ortalama)
      return -1;
    else if (_no > diğerÖğr._no)
      return 1;
    else return -1;
  } // int compareTo(Öğrenci) sonu
  ...
} // Öğrenci sınıfının sonu
Comparable arayüzü, içerdiği yegâne ileti olan compareTo'nun Java dokümantasyonunda ifade edilen anlamına uygun bir biçimde gerçekleştirimini zorunlu koşar. Bu, dönüş değeri olarak hedef nesnenin diğer nesneden "önce" gelmesi durumda artı, "sonra" gelmesi durumunda eksi, aksi takdirde 0 döndürüleceği anlamına gelir.


  1. Bir nesnenin kıyım değeri—yani, nesnenin altalanlarının özeti—nesneye hashCode iletisinin gönderilmesi ile elde edilir.
  2. Burada herhangi bir şekilde nesnenin kopyalanması gibi bir durum olmayacaktır. Aynı nesne değişik türlü iki tutacak vasıtasıyla, diğerNesne ve diğerÖğr, iki farklı arayüze sahipmiş gibi kullanılacaktır.
  3. Anlatılanın geçerli olması için, kap içeriğini değiştirmeye dönük herhangi bir işlemin başarıyla sonuçlanması sonrasında kaba dair kıyım değerinin güncellenmesi gerekir. Bu ise, içerik güncelleme işlemlerinin maliyetini olumsuz bir biçimde etkileyecektir. Dolayısıyla, önerilen yöntem daha ziyade değişmez içerikli kaplara sınırlıdır.