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

18 Mayıs 2011 Çarşamba

Nesnelerin İlklenmesi

İki önceki yazımızda🔎 programcının yeni bir sınıf tanımlamak yoluyla Java platformunu nasıl geliştirebileceğine ve sınıfların nesnelerinin nasıl yaratılıp kullanılabileceğine değinmiştik. Bu yazımızda da, nesnelerin yaratılmaları esnasında ne şekilde ilkleneceğine göz atacağız.

Java'da, diğer nesne paradigmasını destekleyen dillerde olduğu gibi, nesnenin yaratılması iki aşamada tamamlanır: i) nesne için yığın bellekten (İng., heap memory) yerin ayrılması, ii) ayrılan belleğin tutarlı değerlerle ilklenmesi ve yığın bellek dışındaki kaynakların elde edilmesi. Nesnenin yaşam döngüsünü başlatan bu iki adımın atomik olarak icra edildiği ve iki adımdan birinin başarısızlığa uğraması durumunda nesne yaratmanın mümkün olmayacağı unutulmamalıdır. Örneğin, sınırlı bir kaynak olan yığın belleğin tükenmesi olasılığına karşılık programcı, OutOfMemoryError hatasının oluşabileceğinin farkında olmalıdır. Benzer şekilde, ikinci aşamada elde edilmek istenen bir dış kaynağın elde edilememesi de ilkleme esnasında durumu özetleyen bir ayrıksı durumun atılmasına neden olabilir.

Yığın belleğin JSM'nin parçası olan bir bileşen tarafından ayrılması nedeniyle, nesne yaratmak isteyen programcının ilk aşamanın yerine getirilmesi için new işlecini kullanmak dışında bir şey yapması gerekmez. Buna karşılık, ilkleme ve dış kaynak elde edilmesinin söz konusu olduğu ikinci aşama sınıf gerçekleştirimcisinin sağladığı yapıcı metotlardan birinin çağrılması ile tamamlanır. İki aşama arasındaki bağlantı ise derleyicinin new işleci ve uygun bir yapıcıyı ilişkilendirilmesi ile sağlanır. Aşağıdaki örnekler üzerinden anlamaya çalışalım.
char[] karakterDizisi = new char[]{'D', 'i', 'z', 'i'};
String katar1 = new String(karakterDizisi);
String katar2 = new String(karakterDizisi, 0, 3);
İlk satırda oluşturulan dört karakterli dizi kullanılarak yaratılan her iki nesne de yerlerinin ayrılmasının ardından new işleci sonrasında sağlanan sınıftaki (String) yapıcılardan biri kullanılarak ilkleniyor. Buna göre, katar1 tutacağı tarafından temsil edilen nesne, char[] bekleyen yapıcıda dizi içindeki tüm elemanlar kullanılarak ilklenirken, katar2'nin gösterdiği nesne, ilk argümanında geçirilen karakter dizisinin ikinci argümanda verilen konumdan başlayarak üçüncü argümanda sağlanan sayıda karakteri kullanılarak bir başka yapıcıda ilkleniyor.

Genel çerçeveyi çizdikten sonra, nesne ilkleme bağlamında bilinmesi gereken diğer noktalara değinelim. Nesnenin kimi altalanlarına ilk değer sağlanmaması durumu ile başlayalım. İlişkin altalana ait belleğin o anki rasgele içeriğini ilk değer olarak kabul eden C++ dilinin aksine, Java'da ilklenmeyen altalanlar aşağıdaki tabloda verilen değerlere sahip olacak şekilde ilklenir.

Altalanların varsayılan ilk değerleri
Türİlk değer
Tamsayı türler0
Kayan noktalı türler0.0
char'\u0000'
booleanfalse
Bileşke türlernull

Buna göre, aşağıdaki sınıfın nesnesinin yaratılması durumunda _yaşadığıÜlke adlı altalan null değerine sahip olacaktır.
public class Vatandaş {
  public Vatandaş(String ad, String anneAdı, String babaAdı) {
    _ad = ad;
    _anneAdı = anneAdı;
    _babaAdı = babaAdı;
  } // yapıcı(String, String, String)
  ...
  private String _ad, _anneAdı, _babaAdı;
  private String _yaşadığıÜlke;
} // Vatandaş sınıfının sonu
Altalanların hiçbirine değer sağlanmaması halinde, gerçekleştirimci tarafından yapıcı metot yazılması pek anlamlı olmaz. Boş bir gövdeye sahip boş parametre listeli bir yapıcı ile kendini gösteren böylesine bir durumda derleyici araya girer ve gerçekleştirimciyi yapıcı metot yazma yükümlülüğünden kurtararak tüm altalanları varsayılan değerler ile ilkleyen bir yapıcı sentezler. Ancak, bu dost eli sadece yapıcı sağlanmadığı takdirde uzanacaktır; gerçekleştirimcinin sınıf tanımında bir veya daha fazla sayıda yapıcıya yer vermesi derleyicinin yapıcı sentezlemesinin önüne geçecektir.

İlkleme bağlamında bilinmesi yararlı olacak bir diğer konu, ilkleme bloklarının kullanımıdır. Yukarıdaki örneği ele alarak anlamaya çalışalım. Vatandaş sınıfının Türkiye'de kullanılacak olduğunu düşündüğümüzde, yaratılacak nesnelerdeki _yaşadığıÜlke adlı altalanın çoğunlukla "Türkiye" olacağı öngörülebilir. Dolayısıyla, derleyicinin bileşke türler için kullandığı null yerine "Türkiye" değerli bir karakter katarının varsayılan ilk değer olması daha yerinde olacaktır. İşte bu gözlemimizi, derleyicinin kullanacağı varsayılan ilk değerlerin değiştirilmesi için her nesne yaratılması noktasında, nesnenin ilklenmesi öncesinde işlenen bir nesne ilkleme bloğu içinde belirtmemiz gerekir. Aşağıdaki değiştirilmiş sınıf tanımı bunun nasıl yapılabileceğini gösteriyor.
public class Vatandaş {
  // Nesne ilkleme bloğu.
  { 
    System.out.println("Nesne yaratılıyor...");
    _yaşadığıÜlke = "Türkiye";
  }
  public Vatandaş(String ad, String anneAdı) {
    _ad = ad;
    _anneAdı = anneAdı;
  } // yapıcı(String, String)

  public Vatandaş(String ad, String anneAdı, String babaAdı) {
    _ad = ad;
    _anneAdı = anneAdı;
    _babaAdı = babaAdı;
  } // yapıcı(String, String, String)

  public Vatandaş(String ad, String anneAdı, String babaAdı, String ülke) {
    _ad = ad;
    _anneAdı = anneAdı;
    _babaAdı = babaAdı;
    _yaşadığıÜlke = ülke;
  } // yapıcı(String, String, String, String)
  ...
  // Nesne ilkleme bloğu.
  { _babaAdı = "Mehmet"; }
  private String _ad, _anneAdı, _babaAdı;
  private String _yaşadığıÜlke;
} // Vatandaş sınıfının sonu
Dikkat ederseniz, metot gövdeleri dışında bulunan kıvrımlı ayraç çifti arasındaki komutlardan oluşan nesne ilkleme blokları, sınıfın herhangi bir yerine konulabileceği gibi tanım içinde birden çok kez de geçebiliyor. Ortaya çıkacak toplam etki, sınıfın başından sonuna doğru tüm blokların geçiş sırasına göre işlenmesi ile elde edilir. Dolayısıyla, yukarıdaki sınıfta geçen nesne ilkleme bloklarının etkisi aşağıdaki blokla da elde edilebilir.
{
  System.out.println("Nesne yaratılıyor...");
  _yaşadığıÜlke = "Türkiye";
  _babaAdı = "Mehmet";
}
İlkleme amacıyla kullanılabilecek bir diğer programlama aracı, söz konusu sınıfın ilk kullanılışı öncesinde bir kereliğine işlenen sınıf ilkleme bloğudur. Örneğin, aşağıdaki tanıma göre, Vatandaş sınıfının ilk nesnesinin yaratıldığı veya nesne yaratılmadan kullanılabilecek static öğelerinden birinin kullanıldığı nokta öncesinde standart çıktıya "Vatandaş'ın ilk kullanımı..." ve "Lütfen Vatandaş'ı özenle kullanınız..." mesajları basılacak, [daha sonra yaratılacak] nesnelerin paylaşacağı ortak bir özellik olan _vatandaşSayısı 1 ile ilklenecektir. Aynı sınıfın nesnesinin yaratıldığı her noktada işlenen nesne ilkleme bloğu sayesinde ise, çağrılan yapıcı metodun başlaması öncesinde, ilklenmekte olan nesnenin sabit değerli _vatNo altalanı _vatandaşSayısı'nın o anki değeri ile ilklenecektir.
public class Vatandaş {
  // Sınıf ilkleme bloğu.
  static {
    System.out.println("Vatandaş'ın ilk kullanımı...");
    System.out.println("Lütfen Vatandaş'ı özenle kullanınız...");
  }
  // Nesne ilkleme bloğu.
  { _vatNo = _vatandaşSayısı++; }
  ...
  public static long vatandaşSayısı() {
    return _vatandaşSayısı - 1;
  } // long vatandaşSayısı() sonu
  ...
  static { _vatandaşSayısı = 1; }
  private String _ad, _anneAdı, _babaAdı;
  private String _yaşadığıÜlke;
  private final long _vatNo;
  private static long _vatandaşSayısı;
} // Vatandaş sınıfının sonu

20 Nisan 2011 Çarşamba

Bileşke Türler-Diziler

Bir problemin çözümü, problem tanımında geçen varlıkların çözüm ortamındaki araçlar tarafından doğrudan temsil edilmesi durumunda kolaylaşır. Mesela; bir çizimin çizgi, Bézier eğrisi, üçgen, kare gibi çeşitli geometrik nesneleri destekleyen ve bu nesnelere ilişkin çizme, döndürme, öteleme gibi uzmanlaşmış işlemleri sağlayan bir dil kullanılarak oluşturulması daha kolay olacaktır. Matris, denklem gibi matematiksel nesneleri ve ilişkin işlemleri doğrudan destekleyen Maxima ve Matlab gibi dillerin cebir problemleri çözmek için daha uygun olmasının sebebi de aynıdır. Her iki durumda da problem uzayındaki nesneler ile uygulanabilir işlemler ve bu öğelerin soyutlanmasıyla oluşturulan kavramlar ile çözümde kullanılan araçlar arasında eşleme çok basittir. Dolayısıyla, problem uzayını iyi bilen bir programcının üretken olması çok hızlı bir şekilde mümkün olabilmektedir.

Özel amaçlı diller için geçerli olan bu gözlem, Java gibi genel amaçlı olma iddiasındaki diller için geçerli değildir. Böylesine bir yaklaşım, söz konusu dili değişik ilgi gruplarının kavramlarıyla hantallaştıracak ve diğer ilgi gruplarına kullanılmaz hale getirecektir. Bunun yerine programlama dili, ilkel türler🔎 üzerine inşa edilen bir yeni tür tanımlama olanağı sağlar. İşte, yavaş yavaş Java'yı Java yapan bölgeye giriş yapacağımız bu yazıda, Fortran'ın ilk uyarlamalarından bu yana bizimle olan en eski [ve eskimeyen] tür işleciyle (İng., type operator) oluşturulan bileşke türlere, dizilere göz atacağız.1 Ancak, kısaca türdeş verilerin gruplanmasıyla oluşturulan bileşke türler olarak tanımlanabilecek dizilere girmeden önce, diğer bileşke türlerde de sıklıkla kullanacağımız terminolojiyi oluşturacağız.

TutacakVeNesne.java
public class TutacakVeNesne {
  public static void main(String[] ksa) {
    String ad1 = new String("Tevfik");
    String ad2 = new String("Tevfik");
    String ad3 = ad1;
    System.out.println(ad1 == ad2);
  } // void main(String[]) sonu
} // TutacakVeNesne sınıfının sonu
$ javac -encoding utf-8 TutacakVeNesne.java
$ java TutacakVeNesne
false

Yukarıdaki programın çalıştırılması sonrasında 6. satırda yapılan eşitlik denetimi, kimilerinizin beklentilerine aykırı olarak, standart çıktıya—değiştirilmediği müddetçe ekran—false yazacaktır. Bunun sebebi 6.satıra gelinmesiyle oluşan bellek seriliminin aşağıdaki temsili resmi ile açıklanabilir. Görüleceği gibi, 3. ve 4. satırda yaratılan iki nesneye üç değişken kullanılarak atıfta bulunulmaktadır.


Nesne yaratmak için kullanılan new işleci, belleğin yığın bölgesinde yeri ayrılan nesnenin işlenebilmesi için bir tutacak2 döndürmekte ve nesne döndürülen bu tutacağın değerine sahip tanımlayıcılar yoluyla dolaylı bir biçimde kullanılmaktadır. Buna göre, örneğimizde iki nesne yaratılmakta ve söz konusu nesnelerin tutacakları ad1 ve ad2 değişkenlerini ilklemek için kullanılmaktayken, ad1'in ad3'ü ilklemekte kullanılmasıyla iki tutacak3 da aynı nesneyi gösterir hale getirilmektedir. Bunun doğal bir sonucu olarak, tutacakları karşılaştırarak işini gören 6. satırdaki eşitlik denetimi de false döndürmektedir. Çünkü, temsil ettikleri nesnelerin içeriği aynı olmakla birlikte iki tutacak da farklı nesneleri göstermektedir. Eşitlik denetiminin nesne içeriği göz önüne alınarak yapılması isteniyorsa, ad1 tutacağına temsil ettiği nesneyi ad2 tutacağının temsil ettiği nesne ile karşılaştırmasını söyleyen equals iletisinin gönderilmesi gerekir. Aşağıdaki gibi yapılacak ileti gönderiminin sonrasında, ad1'in arkasındaki nesnenin türü olan String sınıfındaki equals metodu çağrılacak ve işlem tamamlanacaktır.
System.out.println(ad1.equals(ad2));
Programcının, yapısı hakkında tahminde bulunup nesneyi tutarsızlığa yol açabilecek binbir yoldan denetimsiz bir şekilde kullanımının önüne geçen bu özellik—tutacak nesne ayrımı—Java'nın programcının bilerek veya bilmeyerek kod güvenliğini tehlikeye atmasına izin vermeyeceğini gösterir. Her şey, nesnenin yaratılması sonrasında döndürülen tutacak aracılığıyla gönderilecek iletilerin çağrılmasına neden olacağı metotlar vasıtasıyla yapılır.4 Tutacak türü ile uyumlu olmayan ileti gönderimleri derleyici tarafından reddedilecektir.

Gelelim dizilere. Derleyici tarafından özel bir biçimde ele alınan sınıflar olan dizi türlerine ait nesneler, diğer bileşke türlü değerler gibi, yaratılır, kullanılır, ihtiyaç duyulmadıkları ilk noktada çöpe dönüşür ve gerekli olursa kapladıkları bellek alanı tekrar kullanılmak üzere çöp toplayıcı tarafından geri döndürülür. Ayrıca, bir dizi nesnesinin kullanımı sırasında eleman sayısının gereksinime göre artmayacağı, bu özelliğin nesnenin yaratılması noktasında sabitlendiği akıldan çıkarılmamalıdır. Aşağıdaki örnek üzerinden görelim.

Diziler.java
import java.util.Scanner;
import static java.lang.System.out;

public class Diziler {
  public static void main(String[] ksa) {
    Scanner grdKnl = new Scanner(System.in);
    int[] notlar = new int[3];
    for (int i = 1; i <= 3; i++) {
      out.print(i + ". notu giriniz: ");
      notlar[i - 1] = grdKnl.nextInt();
    }
    out.println("İkinci dizinin eleman sayısı: ");
    int[] yeniNotlar = new int[grdKnl.nextInt()];
    yeniNotlar = notlar;
    yeniNotlar[1] = notlar[1] + 5;
  } // void main(String[]) sonu
} // Diziler sınıfının sonu
Dizi nesnelerimizin yaratıldığı satırları ele alalım. 7 nolu satırdaki ilkleme komutunda dizi tutacağının adı olan notlar, int[] türüne sahip tanımlanıyor ve her üç elemanı da 0 ilk değerine sahip bir dizi nesnesini gösterecek şekilde ilkleniyor. 13. satırda, benzer bir işlem standart girdiden sağlanacak sayıda elemana sahip bir başka dizi için tekrarlanıyor. Dikkatinizden kaçmamıştır, her iki tutacak da int[] türlü ilan ediliyor; her iki durumda da eleman sayısına dair bilgi tanımda yer almıyor. Diğer bir deyişle, notlar ve yeniNotlar tutacaklarının ikisi de aynı türe sahip olacak şekilde tanımlanıyor.

Dizilere uygulanabilecek işlemler, eleman değerlerinin sorgulanması ve güncellenmesine sınırlıdır. Her iki işlem de, hedef elemanın sırasını belirleyen aritmetiksel deyimin arasına yerleştirildiği köşeli ayraç çiftini ([ ve ]) kullanır. İki işlemden hangisinin istendiği ayraç çiftinin kullanım yerinden anlaşılır. Örneğin, 15. satırdaki atama komutunun sol tarafındaki kullanım söz konusu diziye güncelleme yapıldığını gösterirken, sağ taraftaki kullanım sorgulama yapıldığına işaret eder. İşleme konu elemanın sırasını tayin eden ve indis olarak da adlandırılan aritmetik deyimin 0-başlangıçlı bir değer döndürmesi gerektiği unutulmamalıdır. Buna bağlı olarak, notlar dizisinin son elemanına aşağıdaki ifadeler ile erişilebilir. Opsiyonları anlamaya çalışırken, her dizi nesnesinde bulunan length adlı altalanın dizinin eleman sayısını tuttuğunu unutmayın.
notlar[2] = 333; // notlar[3] değil!!!
notlar[notlar.length - 1] = 333; // Daha iyi bir seçenek.
Belirlendikten sonra değişmeyen eleman sayısı, yenitNotlar dizisinde olduğu gibi çalışma anında belirlenebileceği için yukarıdaki kullanımlardan ikincinin seçilmesi yerinde olacaktır. Aksine bir tercih, her eleman sayısı değişiminde kodun ilişkin yerlerinde değişiklik yapma ihtiyacını doğurur.

Değineceğimiz bir diğer husus, eleman değerlerinin dizi nesnesinin yaratıldığı noktada sağlanması durumunda kullanılabilecek ve kaynak kodu anlamak açısından olumlu bir katkıda bulunan toptan ilkleme komutudur. Aşağıda örnekleri verilen bu komut sayesinde, dizi elemanlarına tanım noktasından farklı bir yerde atama yapılarak kodun uzamasına gerek kalmaz. Dikkat edecek olursanız, toptan ilklemenin kullanıldığı new işlecine dizimizin eleman sayısının verilmesi söz konusu değildir; programcının hatalı bir biçimde belirleme ihtimali bulunan bu değeri derleyici kendisi belirler.
int[] notlar = new int[]{60, 70, 80};
int[] yeniNotlar =
        new int[]{grdKnl.nextInt(), grdKnl.nextInt()};
Daha sonraki kimi yazılarımızda dönme şansını bulacağımız diziler konusunu, şu uyarıyı yineleyerek kapatalım: dizi türleri, derleyici tarafından özel bir biçimde ele alınan sınıflardır. Buna göre, örneğimizdeki notlar tutacağının yeniNotlar'a atanması, yeniNotlar'ın eskiden gösterdiği dizi nesnesini çöpe dönüştürür ve her iki tutacağın da aynı dizi nesnesini paylaşmasına neden olur. Dolayısıyla, bir dizi tutacağı yoluyla yapılacak güncelleme diğeri aracılığyla da görülecektir. Aşağıdaki temsili bellek seriliminden bunun nedenini daha kolay görebilirsiniz.
yeniNotlar = notlar;
yeniNotlar[1] = notlar[1] + 5;

  1. Dizilerin ilk olma şerefi, çalışmakta olan programın karalama defteri olarak düşünülebilecek birincil belleğin, genelde "kutucuklar" dizisi olarak kavramsallaştırılmasına dayanır. Dolayısıyla, sakın ola ki, bileşke türler içinde ilk olması nedeniyle dizilerin yararsız olduğunu düşünmeyin. Daha sonraki yazılarda değineceğimiz kimi olumsuz yönlerine rağmen, veri yapısı olarak dizilerin de kullanımının uygun olduğu pek çok durum olacaktır.
  2. Diğer dillerden aşırma sözcükleri kullanmayı profesyonelliğin belirleyici özelliği zannedenler, ki bu arkadaşlar genelde bir şey anlatmak için değil anlatmamak için konuşurlar, hendıl (İng., handle) demeyi yeğleyebilirler. Ancak, sözcük tercihimdeki mantığı açıklarsam bu arkadaşları da sanırım tarafımıza kazanabiliriz. Bunun için, bir tavayı yemek pişirmekte nasıl kullandığınızı düşünün. Tavanın tutacağı olan sapıyla değil mi? Dolayısıyla, nasıl ki, aklı başında insanlar tavayı sapı yardımıyla kullanırlar, nesneye aracısız erişim imkanının olmadığı Java'da nesneler tutacakları aracılığıyla kullanılırlar.

    Bu noktada, nadiren de olsa tutacak yerine tutamak sözcüğünü önerenlere de ufak bir tavsiyem var. Bir şeye tutunmakta kullanılan gövde üzerindeki oyuk veya çıkıntıların adı olan tutamak, maalesef, doğru bir seçim değil. Çünkü, adını koymaya çalıştığımız kavram nesnenin gövdesinde bulunmuyor ve yapılan atamalarla değişik zamanlarda değişik nesneleri temsil edebiliyor.
  3. Bundan sonraki anlatımımızda, "... nesnesinin tutacağına sahip tanımlayıcı" demektense, tutacak demeyi tercih edeceğiz.
  4. Nesne paradigmasına sadık kalınarak yapılan bu anlatım her zaman geçerli olmayacaktır. static olarak nitelenen metotlar, sadece tüm sınıf üyelerinin paylaştığı ortak özellikleri kullanarak işini görür ve ileti gönderilmeden doğrudan çağrılırlar.