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. ↑

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

6 Mayıs 2011 Cuma

Java Platformunun Dikkate Değer Bir Dili: Scala-1

Başlık yazının konusunu ele veriyor: sınıf dosyasına derlenmek suretiyle JSM tarafından çalıştırılabilir programlar üretmekte kullanılabilecek Scala dili. .NET'in CIL komutları içeren DLL formatına da derlenebilen, fonksiyonel programlama kavramlarını destekleyen bu nesne yönelimli dil, Java'ya eklenmesi düşünülen pek çok kavram için bir laboratuar ve esin kaynağı olma özelliği taşıyor. Scala programlama diline dair dizimizin ilk bölümü olan bu yazıda da bu dili Java bilenlere tanıtmaya çalışacağız.


Basit Bir Scala Programı


Java'da olduğu gibi, Scala'da da her şey bir sınıf içine yazılmak zorundadır. Aslına bakılırsa, ilkel türleri de sınıflarla temsil eden Scala bu konuda Java'dan daha katıdır diyebiliriz. Bir diğer farklılık, ';' karakterinin pek çok C-temelli dil tarafından kabul edilen komut sonlandırıcılık görevindeki değişikliktir. Satır sonlarının komut sonlandırma noktası olarak algılanması nedeniyle, birden fazla komutun aynı satıra konulmaları durumunda kullanılması dışında, Scala'da komut sonlarına ';' yerleştirilmesine gerek yoktur.

SelamMillet.scala
object SelamMillet {
  def main(args: Array[String]) =
    println("Selam millet!")
} // SelamMillet sınıfının sonu
Yukarıda verilen kaynak kodda main metodunun dönüş türünün eksik olduğunu düşünüyorsanız, Scala'nın bir diğer özelliğini söyleyerek yanıt verelim: Scala, ML ve Haskell programlama dillerinde olduğu gibi, program metninin sağladığı bilgilerden yararlanarak programlama öğelerinin türlerini çıkarsamaya çalışır. Bundan dolayı, Scala programcılarının derleyicinin çıkarsayamadığı yerler dışında hiçbir yere tür bilgisi koymasına gerek yoktur. Derleyici tarafından kabul görmekle birlikte, main metodunun imzasının aşağıdaki şekilde yazılması gerekmez.
def main(args: Array[String]): Unit = 
Örneğimizde sınıf kavramının izlerini arayanlar, kendilerini ikna etmekte zorlanabilirler. Zira, sınıf sözcüğü (class) yerini nesne sözcüğüne (object) bırakmıştır. Sınıfın bir şablon, nesnenin ise söz konusu sınıfın bir örneği olduğunu bilenleriniz bilgilerinden kuşku duymaya başladıysalar üzülmesinler, her şey eskiden olduğu gibi. Beklenen class sözcüğü yerine object sözcüğünün olması, tanımlanmakta olan sınıfın bir özelliğini yansıtmaktadır: Java terminolojisi ile açıklayacak olursak, SelamMillet sınıfının yegâne metodu olan main static'tir. Bir diğer deyişle, main metodu, SelamMillet sınıfının nesnesi yaratılmadan kullanılabilir. Nesne paradigması ile bağdaşmayan bu durum—problem çözümünde merkezi rol oynayan nesnenin bırakın kullanılmasını, yaratılması bile söz konusu değil—kimi zaman çalıştırılabilir sınıfın tek bir nesnesinin yaratılıp bu nesneye ileti gönderilmesi yoluyla aşılır. İşte, tasarım desenleri dünyasında tek örnek[li sınıf] (İng., singleton [class]) deseni olarak adlandırılan bu kullanım, Scala'da object anahtar sözcüğünün kullanımı ile belirtilir. Bu şekilde tanımlanmış sınıfların, tüm öğelerinin static olduğu varsayılır. Bundan dolayıdır ki, Scala'da static anahtar sözcüğüne yer yoktur, programcı bu tür özellikleri tek örnekli sınıflar içinde toplayarak bir object olarak tanımlamalıdır.

Kaynak kodun yazılması sonrasında yapılacaklar Java'daki ile aynıdır: ilk adımda sınıf dosyasına çevrilerek derlenen kod, JSM tarafından çalıştırılmalıdır.
# Derleme aşaması
$ scalac -encoding utf-8 SelamMillet.scala
$ ls SelamMillet*
SelamMillet.class   SelamMillet$.class   SelamMillet.scala
# Yorumlama aşaması
$ scala SelamMillet
Selam millet!
Yukarıdaki listelemenin iki noktası dikkatinizi çekmiştir. Öncelikle, Scala programlama dilinin derleyicisi olan scalac komutunun ürettiği iki tane sınıf dosyası vardır. Bunlardan, SelamMillet.class uygulama dosyası iken, SelamMillet$.class tek örnekli sınıf deseninin gerçekleştirildiği kodu içerir. İkinci nokta ise, JSM'nin java yerine scala komutuyla çağrılmasıdır. Bu, her programlama dili için JSM'nin ayrı ayrı gerçekleştirilmesi gerektiği yanılgısına neden olmamalıdır. Çünkü, scala komutu java komutunun Scala'ya has sınıf dosyalarını yükleyerek işini gören uyarlaması olarak düşünülebilir. Dolayısıyla, yorumlama aşması aşağıdaki şekilde de yapılabilir.
# Yorumlama aşaması
$ java -cp .:/usr/share/java/scala-library.jar SelamMillet
Selam millet!
Bunun güzel bir sonucu, çöp toplama, güvenlik denetimleri, anında derleme gibi JSM tarafından sağlanan pek çok hizmetin Scala programları için de otomatikman sağlanmasıdır; Scala dilinin (ve diğer JSM üzerinde çalışan dillerin) geliştiricilerinin bu hizmetleri ayrıca gerçekleştirmelerine gerek yoktur.

Java Programlama Dili Sınıflarının Kullanımı


JSM dili olmaları nedeniyle, gerek Java gerekse Scala, kurulumunuzun sınıf yolu üzerindeki tüm sınıf dosyalarını, kaynak kodları hangi dilde yazılmış olursa olsun, kullanabilir. Scala programcısı açısından bakıldığında bu, Scala kaynak kodu içinden Java platformunun bildik tüm işlevselliğinden sanki Scala'da yazılmış gibi yararlanılabileceği anlamına gelir. Bu, aşağıdaki örnekte verildiği gibi işlevselliği kullanmak şeklinde olabileceği gibi, platformdaki bir sınıftan kalıtlama veya bir arayüzü gerçekleştirme şeklinde de olabilir. Ancak, Java'dan gelip Scala kaynak kodu üretenlerin yararlanılacak işlevi Scala sözdizimi ile kullanmaları gerektiğini akılda tutmaları gerekir. Aşağıdaki örnek üzerinden görelim.1

Açıklamalara Scala'nın Java ile farklılık gösteren bir özelliğine dikkat çekerek başlayalım: dosya içinde dosya ile aynı ada sahip bir sınıfın bulunması gerekmez; Scala'da böyle bir zorunluluk yok. Ancak, daha sonraki derleme/çalıştırma komutlarında örneği verildiği gibi, derleyiciye geçirilenin dosya adı iken yorumlayıcıya geçirilenin sınıf dosyasının adı olduğu unutulmamalıdır.

Örneğimizin Scala'ya yeni gelenler için dikkat çeken bir yönü, import bildiriminin ilk satırdaki seçimli kullanımı.2 Tekli ve jokerli3 kullanımlara sahip bu bildirim, örneğimizde de olduğu gibi, bir veya daha fazla sayıda türün görünür kılınmasında da kullanılabilir. Dikkat çekecek bir diğer anahtar sözcük ise, simgesel sabit tanımlamak için kullanılan val.4

Örnek2.scala
import java.util.{Scanner, Vector}

object ScaladanJava {
  val grdKnl = new Scanner(System.in);
  println("İkinci Scala örneğine hoş geldiniz...")

  def main(args: Array[String]) = {
    val tekSayılar = new Vector[Int]()
    print("Artı bir tamsayı giriniz: ")
    val j = grdKnl.nextInt()
    for(i <- 1 to j by 2) tekSayılar.add(i)
    println("1 ile" + j + " arasındaki tek sayılar: " + tekSayılar)
  }

  println("Bu örnek Scala kodundan Java'da yazılmış işlevselliği kullanmaya dönük")
} // ScaladanJava sınıfının sonu
Metot gövdeleri dışındaki komutların varlığı da sizi telaşa sürüklemesin; bu komutların sınıf içindeki sıraları korunacak şekilde sınıf ve/veya nesne ilkleme bloğu içine konulduğunu düşünmeniz işinizi kolaylaştıracaktır. Tek örnekli bir sınıf olması nedeniyle tüm öğeleri static varsayılan sınıfımızda, metot tanımları dışındaki her şey sınıf ilkleme bloğuna konulup uygulamanın çalıştırılması sonrasında ilk iş olarak işlenirken, new işleci ile nesnesi yaratılabilen çok örnekli sınıflarda bu tür öğeler nesne ilkleme bloğuna konulacak ve sentezlenen bu nesne ilkleme bloğu her nesne yaratılışı noktasında yapıcı çağrısı öncesinde işlenecektir.
$ scalac -encoding utf-8 Örnek2.scala
$ scala ScaladanJava
İkinci Scala örneğine hoşgeldiniz...
Bu örnek Scala kodundan Java'da yazılmış işlevselliği kullanmaya dönük
Artı bir tamsayı giriniz: 16
1 ile 16 arasındaki tek sayılar: [1, 3, 5, 7, 9, 11, 13, 15]

Diziler Soysal Bir Türün Örneğidir


Verdiğimiz örneklerde komut satırından geçirilen argümanları tutmak için kullanılan dizi ve ikinci örneğimizdeki Vector nesnesinden de görülebileceği gibi, Scala'da soysallık tür parametrelerine karşılık gelen tür argümanlarının köşeli ayraç çifti arasında geçirilmesiyle sağlanır. Buna göre Vector[Int], Int türlü elemanlara sahip bir kabın kullanılacağını gösterir. Bu tanım sonrasında, söz konusu kap parametrik türün (java.util.Vector) sağladığı tüm işlevsellikten yararlanarak manipüle edilebilir. Bu, soysal Array türünün örneği olan diziler için de geçerlidir. Tanımlanan bir dizi, Array sınıfınca sunulan tüm işlevsellikten yararlanabilecektir.


  1. Gözünüzden kaçmış olabilir, işaret etmekte yarar var: ilk örneğimizin aksine ikinci örneğimizin metot gövdesi kıvrımlı ayraç çifti arasına yazılmıştır. Bunun sebebi, tek komuttan oluşması durumunda Scala'daki metot gövdelerinin, tıpkı Java'daki if, while yapılarının gövdeleri gibi, kıvrımlı ayraç çifti arasına yazılmalarının zorunlu olmamasıdır. ↑
  2. java.lang paketindeki türler, herhangi bir bildirime gerek olmadan Scala derleyicisi tarafından görünür hale getirilirler. ↑
  3. Söz konusu paketin tümünü görünür kılan jokerli kullanım, Java'da "*" ile belirtilirken Scala'da '_' ile belirtilir. ↑
  4. Tanımlayıcının güncellenebilir bir içerik tutabilmesi için val yerine var ile nitelenmesi gerekir. ↑

2 Mayıs 2011 Pazartesi

Bileşke Türler-Sınıflar

Bir önceki yazımızda🔎 programlama dilinin sunduğu kavram dağarcığının çözülmekte olan probleminki ile örtüşmesi durumunda çözümün daha kolay olacağına değinmiş ve Java gibi genel amaçlı dillerde problem ve çözüm uzayları arasındaki kusursuz bir uyum beklentisinin genel olma iddiasıyla çelişeceğini söyleyerek, gerçek çözümün programcıya yeni bileşke tür tanımlama olanağı sağlamaktan geçtiğini ifade etmiştik. Bu yönde sunduğumuz ilk araç ise türdeş verilerin gruplanmasına yarayan diziler olmuştu. İkinci yazımızın konusu ise, türdeş olsun ya da olmasın, birbirleriyle ilişkili verilerin gruplanması ve bu veri öbeği üzerindeki makul işlemlerin tutarlı bir biçimde uygulanmasını sağlamak için soyut veri türleri tanımlamayı olanaklı kılan sınıf kavramı olacak.1 Daha fazla uzatmadan başlayalım.

Matris.java
...

public class Matris {
  ...
} // Matris sınıfının sonu
Problem tanımında geçen ve Java platformunca doğrudan desteklenmeyen bir kavramın Java koduna aktarılması, söz konusu kavramın bizi ilgilendiren özelliklerini içeren bir sınıfın tanımlanması ile olur. Bu ise yukarıdaki gibi bir kod iskeletinin uygun tanımlarla doldurulması yoluyla sağlanır. Tanımların sağlanması sırasında, tanımları sağlayacak gerçekleştirimci(ler) ile sınıftan yararlanacak kullanıcıların farklı kitleler olduğu unutulmamalıdır; gerçekleştirimcilerin aksine kullanıcılar ayrıntılarla ilgilenmeyecek, sunulan işlevselliğin nasıl realize edildiğine değil, ne olduğuna bakacaktır.

Ne demek istediğimizi, Matris sınıfının içini matematikteki matris kavramının özellikleriyle doldurarak gösterelim. Ama ilk önce, nereden başlamamız gerektiği konusunda bir uzlaşıya varalım. Bu noktada, acemi arkadaşlar ve C/C++ gibi dillerden gelip bellek serilimleri ile oynamaya alışmış ustalara bir uyarı: soyut veri türü tanımlamaya türün yapısını belirleyen bellek seriliminden başlamak doğru olmaz! Mesela, aceleyle atılıp matris elemanlarını tutmak için iki boyutlu bir dizi tanımlamaya soyunmak yerinde bir hamle olmayacaktır. Zira, matris elemanlarını tutmak için iki boyutlu dizinin yanısıra, Vector içeren Vector veya sıra no-sütun no çifti anahtarıyla erişilen kıyım tablosu da kullanılabilir. Matris içeriğinin özelliklerine göre bir seçim bazen iyiyken bazen kötü olabilir.2 Ayrıca, bu konudaki bir karar değişikliği gerçekleştirimimizdeki pek çok şeyin değişmesini gerektirecektir ki, kodumuzun güvenilirlik düzeyine olumsuz etkide bulunan bu tür şeylerden kaçınılmalıdır. Bunun yerine, işimize Java karşılığını ifade etmeye çalıştığımız kavramın değişmeyen yönlerini, özniteliklerini, belirleyerek başlamalıyız.

Biraz programlama yapmış olanlarınız, koda dökülmekte olan kavrama ait varlıkların belirleyici özellikleri olan özniteliklerin, aslında altalanlara verilen yeni bir ad olduğunu düşünebilir. Bu yanılgıyı, altalanların sınıf şablonu kullanılarak yaratılacak nesnelerin özel belleğini oluştururken, özniteliklerin diğer özniteliklerdan yararlanarak hesaplanmak suretiyle de temsil edilebileceğini vurgulayarak ortadan kaldıralım. Örnek olarak, bir üçgenin özniteliklerini düşünün. Kenar uzunlukları ve kenarlar arasındaki açılar, değil mi? Peki ama, bu altı özniteliğin altısı da üçgen kavramının karşılığındaki sınıfta altalan olarak karşılık bulmalı mı? Tabii ki hayır! İstenecek olursa, Sinüs kuralından yararlanarak işimizi dört altalanla da görebiliriz. Dolayısıyla, yapacağımız şey, her bir öznitelik için altalan tanımlamak değil, altalanların tanımını sonraya bırakarak öznitelik değerlerini döndüren/güncelleyen erişici/değiştirici metotları sağlamak olmalıdır.
public class Matris {
  public int sıraSayısı() { ??? }
  public int sütunSayısı() { ??? }
  public double eleman(int sıra, int sütun) { ??? }
  public void elemanGüncelle(int sıra, int sütun, double yeniDeğer) { ??? }
  ...
} // Matris sınıfının sonu
Kıs kıs güldüğünüzü görür gibi oluyorum. Ne de olsa, yukarıdaki metotların gövdesini sağlayabilmemiz için ne çeşit bir kap kullanacağımıza ve altalanlarımızın hangi öznitelikler olacağına karar vermemiz gerekiyor. Doğru ama, ben biraz sorumsuzluk göstererek bu itirazınızı umursamayacağım ve sanki çok güvendiğim birileri metot gövdelerini sağlamış gibi davranacağım. Bazen, hayal görmek de işe yarayabilir. Kabul ettiyseniz bir sonraki adıma geçelim: matrisler üzerinde uygulanabilecek işlemleri tanımlamaya. Ancak, ilk önce metot imzalarının yanlış olduğunu düşünenlerin kafalarındaki soruya yanıt verelim: Hayır, imzaların parametrelerinden biri eksik değil. Tanımlanan tüm işlemler bir matris nesnesi üzerinde etki yapacağı için, bu ortak nesnenin tüm metotlara saklı argüman olarak geçirildiği varsayılıyor. Yani, tüm metotlar işlerini ileti alıcı veya hedef nesne olarak da adlandırılan özel bir matris nesnesi bağlamında yapıyor.
public class Matris {
  ...
  public Matris çarp(Matris sağMatris) { ... }
  public Matris çarp(double skalar) { ... }
  public Matris çıkar(Matris sağMatris) { ... }
  public Matris ters() { ... }
  public Matris topla(Matris sağMatris) { ... }
  // diğer işlemler
  ...
} // Matris sınıfının sonu
Dikkatinizi çekmiştir, sınıf tanımımız çarp adında iki metot içeriyor ve metot adları matematikten alışageldiğimiz +, - ve * gibi evrensel işleçler arasından seçilmemiş. Bu, Java'nın metot aşırı yüklemeyi desteklerken işleç aşırı yüklemeyi desteklemediği anlamına gelir. Metot aşırı yükleme bağlamında, farklı imzalara sahip iki veya daha fazla sayıdaki metot aynı aduzayında yer alabilir. Dolayısıyla, metot adı ve parametre listesinden birisinin farklı olması metotların birlikte tanımlanmasına olanak tanıyacaktır. Parametre sayısının veya karşılıklı parametre türlerinin farklı olması, metot imzalarının farklı olması anlamına geleceği için, örneğimizdeki aynı adlı iki metodun aynı sınıfta tanımlanmaları bir sakınca doğurmayacaktır.3

Her şey yolunda gidip de sınıfımızı tamamladıktan sonra, böylesine bir sınıfın kullanımı aşağıda verilen örnekteki gibi olacaktır. main metodunun gövdesinde new işleci kullanılarak yaratılan 3x5'lik iki matrisin toplamı, m1'e, kendisini m2 ile toplamasını sağlayan topla iletisinin gönderilmesiyle elde ediliyor. İleti alıcı rolünü oynayan m1, iletinin gönderilmesi sonrasında çağrılan Matris sınıfındaki aynı adlı metodun çağrılması sırasında saklı argüman olarak geçirilirken m2 sıradan bir argüman olarak geçiriliyor.
...

public class MatrisKullanıcı {
  public static void main(String[] ksa) {
    Matris m1 = new Matris(3, 5);
    Matris m2 = new Matris(3, 5);
    // matrisleri doldur...
    Matris m3 = m1.topla(m2);
    ...
  } // void main(String[]) sonu
} // MatrisKullanıcı sınıfının sonu
Gelelim eksiklerimizi tamamlamaya. Son adımda listelediğimiz işlemlere karşılık gelen metotlar ile başlayalım. Nasıl ilerlememiz gerektiğine topla adlı metodun gerçekleştirimini vererek açıklık getirelim. Bu amaçla sağladığımız aşağıdaki koda dikkat edecek olursanız, metot gövdesinde altalan ve matris içeriğini tutan kaba herhangi bir atıf yok. Her şey, gerçekleştirim ayrıntılarını saklayan erişici/değiştirici metotlar vasıtasıyla yapılıyor. Dolayısıyla, erişici/değiştirici metotların gövdesini sağladığımız an bu metot gerçekleştirimi de işlevsellik kazanmış olacak. Böylesine bir yaklaşımın artısı, gerçekleştirdiğimiz algoritmayı soyut bir biçimde ifade etmemizi olanaklı kılması ve kodun doğruluğu hakkında daha kolay ikna olmamızı sağlamasıdır.

Kodda yabancı gelebilecek bir diğer öğe, this anahtar sözcüğünün kullanılışı. Özel bir tutacak adı olan this, metoda saklı argüman olarak geçirilen hedef nesneye atıfta bulunmak için kullanılır. Mesela, m1.topla(m2) şeklinde ifade edilen gönderi, ileti alıcı konumundaki m1'in this ile eşleştirilmesine neden olacaktır.

Hatalı gibi gözükebilecek bir diğer nokta, ileti alıcının metot gövdesinde kullanılmasının, ad çakışması sonucu derleme hatasının ortaya çıkacağı durumlar dışında, seçimlik olmasıdır. Yani, istenecek olursa this anahtar sözcüğünün kullanımı es geçilebilir. Bundan dolayıdır ki, m adlı yerel değişkenin ilklenmesinde kullanılan ifade hatalı değildir ve this.sütunSayısı() ile eşdeğerdir.
public class Matris {
  ...
  public Matris topla(Matris sağM) {
    int n = this.sıraSayısı(), m = sütunSayısı()
    if (n != sağM.sıraSayısı() ||
        m != sağM.sütunSayısı()) return null;

    Matris sonuç = new Matris(n, m);
    for (int i = 1; i <= n; i++)
      for (int j = 1; j <= m; j++)
        sonuç.elemanGüncelle(i, j, eleman(i, j) + sağM.eleman(i, j));

    return sonuç;
  } // Matris topla(Matris) sonu
  ...
} // Matris sınıfının sonu
Her şey bitti gibi. Ama durun daha nesnemizin yapısına, yani altalanların ne olacağına karar vermedik. İş böyle olunca, nesnelerimizi yaratırken nesne içeriklerinin ilklenmesi için çağrılacak olan yapıcı metotları da yazamadık. O zaman gelin, düşündük taşındık, iki boyutlu bir dizide karar kıldık diyelim ve bu eksikliği de tamamlayalım.
public class Matris {
  public Matris(int sıraSayısı, int sütunSayısı) {
    _kap = new double[sıraSayısı][sütunSayısı];
  } // yapıcı(int, int)

  public Matris(int sıraSayısı, int sütunSayısı, double ilkDeğer) {
    this(sıraSayısı, sütunSayısı);
    for (int i = 0; i <= sıraSayısı; i++)
      java.util.Arrays.fill(_kap[i], ilkDeğer]);
  } // yapıcı(int, int, double)

  public int satırSayısı() { return _kap.length; }
  public int sütunSayısı() { return _kap[0].length; }
  public double eleman(int i, int j) {
    return _kap[i - 1][j - 1];
  } // double eleman(int, int)
  public double elemanGüncelle(int i, int j, double yeniDeğer) {
    _kap[i - 1][j - 1] = yeniDeğer;
  } // double eleman(int, int)
  ...
  private double[][] _kap;
} // Matris sınıfının sonu
Kod parçasına açıklık getirmeye, hatalı gibi gözükebilecek yapıcı metotlar ile başlayalım. Nesne yaratılması noktasında, yerin ayrılmasını takiben nesnenin tutarlı bir ilk duruma getirilmesini sağlamak görevini gören ve sınıf ile aynı ada sahip yapıcıların dönüş türünün belirtilmesine gerek yoktur.4, 5 new işlecinin kullanılması bağlamında arka planda çağrılan yapıcılar, programcı tarafından doğrudan çağrılmaz. Dikkat çekici ikinci nokta, üç argümanlı yapıcımızın ilk satırı. İleti adı içermeyen bu kullanım, bir yapıcı içinden aynı sınıfın bir diğer yapıcısını çağırmak için kullanılır ve içinde bulunduğu yapıcının birinci komutu olmak zorundadır. Aksi bir durum, derleyici tarafından hata olarak görülecektir. Değinilmesi gereken bir diğer nokta, altalanımızın erişim niteleyicisinin private olması. Bu, altalanın sınıfa özel olduğu ve sadece içinde bulunduğu sınıfın diğer öğeleri tarafından kullanılabileceği anlamını taşır. Nesne yapısını dişarıdan soyutlayan böylesine bir kısıtlamanın sebebini anlamak için altalanın public varsayıldığı aşağıdaki örneği ele alalım.
...

public class MatrisKullanıcı {
  public static void main(String[] ksa) {
    Matris m1 = new Matris(3, 5);
    // matrisi doldur...
    Matris m2 = new Matris(3, 5);
    for (int i = 0; i <= m1.length; i++)
      for (int j = 0; j <= m1[0].length; j++)
        m2[i][j] = m1[i][j] * 7;
    ...
  } // void main(String[]) sonu
} // MatrisKullanıcı sınıfının sonu
Matrisi temsil etmek için iki boyutlu bir dizinin kullanıldığını kabul eden kod parçası, m1 matrisini 7 ile çarpmak için bu ön kabulü kullanmaktadır. Ancak, belki de marjinal bir hız artışı saplantısıyla yazılan bu kod, gerçekleştirimci ile kullanıcı arasında bir bağımlılık oluşturarak aslında zarar vermektedir. Zira, gerçekleştirimcinin matris temsilini daha önceden bahsettiğimiz alternatiflerden birine değiştirmesi, kullanıcı kodunun da—tek gerçekleştirimci kodu varken binlerce kullanıcı kodu olabileceğini unutmayın—değişmesi zorunluluğunu doğuracaktır. Bir başka deyişle, işlevsellik değişmediği halde gerçekleştirimcinin yaptığı bir değişiklik pek çok kullanıcıyı etkileyecektir. Önlenmesi gereken bu felaket senaryosu, kullanıcıya ne sorusunun yanıtını veren ve nadiren değişen öğelerin sunulması, değişme ihtimali bulunan ve nasıl sorusunun yanıtını veren altalanlar ve yardımcı metotlara erişimin kısıtlanması ile mümkün olur.6
 

  1. Yazımızın sınıf kavramını tanıtmaya yönelik olduğu ve anlamayı kolaylaştırmak adına paketler, ayrıksı durumlar gibi daha sonraki yazılarda anlatılacak bazı şeyleri es geçtiği söylenmeli. ↑
  2. Örneğin, genelde küçük matrislerle uğraşacaksak, yerden maliyetli fakat hızlı işlemi olanaklı kılan bir veri yapısı seçilebilir. Ya da, 0 değerinin çok geçtiği seyrek matrislerde 0 değerlerini doğrudan tutmaktansa dolaylı yollardan belirtmeyi tercih edebiliriz.... ↑
  3. Dönüş türü bilgisi aşırı yüklemenin geçerliliğinin denetiminde kullanılmaz. Buna göre, sınıfımıza double argüman alıp double döndüren çarp adlı bir metodun eklenmesi hataya neden olacaktır. Böylesine bir kuralın var oluş sebebi, metot dönüş değerinin göz ardı edilmesi opsiyonundan kaynaklanır. Dönüş değerinin göz ardı edilmesi durumunda hangi dönüş türlü metodun kastedildiği bilinemeyeceği için, derleyici ortaya çıkan muallak durumu hata olarak bildirecektir. ↑
  4. Tüm altalanların ilklenmesine gerek yoktur. Bazılarının ilklenmemesi durumunda, Java derleyicisi söz konusu altalanların varsayılan bir ilkdeğere sahip olmasını garanti edecektir. Tamsayılar için 0, kayan noktalı sayılar için 0.0 ve mantıksal değerler için false olan varsayılan ilkdeğer, bileşke türler için tutacağın bir nesneyi göstermediğini belirten null olacaktır. ↑
  5. Bir sınıftaki yapıcı sayısı ihtiyaca göre değişebilir. Gerekli gördüğü takdirde, programcı yapıcı yazmamayı da tercih edebilir. Bu durumda, Java derleyicisi tüm altalanlara uygun ilkdeğerler sağlayan bir argümansız yapıcı sentezleyecektir. Ancak, yapıcı sentezlemenin programcı tarafından sağlanan yapıcıların varlığında söz konusu olmayacağı akılda tutulmalıdır. ↑
  6. Sınıf gerçekleştiriminin bitmesi, her şeyin bittiği anlamına gelmez. Kodun kullanıcılarla buluşması öncesinde, son bir aşama olarak, performansı düşüren noktalarda eniyilemeler (İng., optimization) yapılması düşünülebilir. ↑

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. ↑

15 Nisan 2011 Cuma

boolean Türü ve İlkel Türler Arasında Tür Uyumluluğu

İlkel türleri tanıtmaya devam ettiğimiz bu yazıda, çalışmakta olan programda bir koşulun oluşup oluşmadığını denetlemek amacıyla kullanılan boolean türüne baktıktan sonra ilkel türden değerlerin birbirleri yerine kullanılabilme özelliklerine göz atacağız.

boolean Türü

Koşullu ve yinelemeli komut işlemenin temeli olan doğruluk denetimi, boolean türlü sabitler olan true veya false değerlerinden birini üreten ilişkisel işleçlerin1 kullanılmasıyla yapılır. Denetlenmesi istenen koşulun karmaşıklaşması durumunda ilişkisel işleçli ifadeler mantıksal işleçler2 kullanılarak birleştirilebilir.3 Bir örnekle görelim.

Faktöryel.java
import java.util.Scanner;

public class Faktöryel {
  public static void main(String[] ksa) {
    Scanner grdKnl = new Scanner(System.in);
    System.out.print("Faktöryeli bulunacak sayı: ");
    int n = grdKnl.nextInt();
    System.out.println(n + "!: " + fakt(n));
  } // void main(String[]) sonu

  public static long fakt(int n) {
    if (n == 1 || n == 0)
      return 1;
      else return n * fakt(n-1);
  } // long fakt(int) sonu
} // Faktöryel sınıfının sonu

Programımız, standart girdi dosyasından—değiştirilmediği müddetçe klavye—girilen karakterleri tarayarak istenen türden veriler haline dönüştüren Scanner türündeki grdKnl kullanılarak alınan tamsayı değerin faktöryelini hesaplıyor. Bu amaçla yazılmış olan özyinelemeli fakt metodunun sonsuz döngüye girmesini engellemekte kullanılan koşul deyimi, eşitlik denetleme işleci (==) ile oluşturulan iki altdeyimin veya bağlacı (||) ile birleştirilmesiyle oluşturuluyor. Buna göre, metottan parametre değerinin 0 veya 1'e eşit olması durumunda 1, aksi takdirde parametrenin 1 küçük değerin faktöryeli ile çarpımı döndürülüyor.

Mantıksal türün sonradan eklendiği C'nin yerleştirdiği alışkanlıklar sonucu, C/C++'da bir koşulun oluşmama durumu sıfır-benzeri bir değerle (NULL, 0,0, 0, vd.) temsil edilebilirken, oluşma durumu sıfır-benzeri olmayan herhangi bir değerle temsil edilebilir. Tür güvenliğine önem veren Java'da bu alışkanlığın unutulması gerekecektir. Dolayısıyla, aşağıdaki kod parçası Java derleyicisi tarafından kabul edilmez.

if (n % 2)
  System.out.println(n + " çift değil");
  else System.out.println(n + " çift");

İlkel Türler Arasında Tür Uyumluluğu

C/C++ dünyasından aşina son satırların Java derleyicisi tarafından kabul görmemesi, tamsayıların boolean değerler ile tür uyumlu olmadığı şeklinde ifade edilir. C/C++ dilinin "Programcı hata yapmaz, hata gibi gözüken şey muhtemelen yaratıcı bir kestirmedir" anlayışına karşılık, "Programcı da bir insandır ve hata yapabilir" felsefesinden hareket eden Java, belirtiyi ciddiye alır ve olası hatanın başkalaşmasına izin vermeden programcıya en erken aşamada (derleme) durumu bildirmeyi tercih eder.

Tamsayıların mantıksal değerlerin yerine kullanılamamasına benzer bir şekilde, byte türlü bir tanımlayıcıya int türlü bir değer sağlanması da hata olarak görülecektir. Bunun sebebi, int türünün temsil aralığının byte türünün temsil aralığında bulunmayan değerler içermesidir. Mesela, yukarıda verilen Faktöryel.fakt metodunun parametre türü byte olacak biçimde değiştirilmesi hataya neden olacaktır. Çünkü, söz konusu metodun çağrıldığı satırda geçirilen argüman int türlüdür ve -231 ile 231-1 arasındaki herhangi bir değere sahip olabilir; buna karşılık, argümanın değeriyle ilklenecek parametre byte türlüdür ve -128 ile 127 arasındaki değerleri temsil edebilir. İşte bu yüzdendir ki, parametre türünün byte olarak değiştirilmesi durumunda geçirilen argüman değerinin aşağıdaki kod parçasında olduğu gibi biçimlendirilmesi gerekir.

import java.util.Scanner;

public class Faktöryel {
  public static void main(String[] ksa) {
    ...
    System.out.println(n + "!: " + fakt((byte) n));
  } // void main(String[]) sonu

  public static long fakt(byte n) { ... } // long fakt(int) sonu
} // Faktöryel sınıfının sonu

Pek çok—kesin olmak gerekirse, sekiz—ilkel türün varlığı, tür uyumluluğu konusunda karar verme işini zorlaştırabilir. Bu noktada, türlerin kayan noktalı ve tamsayı türler şeklinde dizilmesi imdadınıza yetişecektir.

double ⊃ float ⊃ long ⊃ int ⊃ short ⊃ byte
int ⊃ char
double ∩ boolean = ∅

Yukarıdaki geçişken üstküme-altküme ilişkisi ile resmedilen dizilim, en baştaki türün en kapsayıcı en sondakinin ise en az kapsayıcı olduğu şeklinde yorumlanmalıdır.4 Buna göre, double değerin beklendiği yerlerde diğer tüm sayısal türlerden değerler kullanılabilir. Buna karşılık, short değerin beklendiği yerlerde sadece byte türlü değerler kulanılabilirken, diğer tüm sayısal değerler ancak biçimlendirme yapılmak suretiyle kullanılabilecektir.

Dizilimde char türü için ayrı bir kapsama ilişkisinin tanımlanması, bu türün tamsayı türü olarak ele alınması durumunda eksi sayıları temsil edemeyecek olmasından kaynaklanır. Aynı büyüklükte bir alan kaplayan short -32768 ile 32767 aralığındaki değerleri temsil edebilirken, char 0 ile 65536 arasındaki değerleri temsil edebilir. Bir diğer deyişle, short'un char, char'ın short yerine konulması kimi zaman değer kaybına sebep olacaktır.


  1. Yararlanılabilecek ilişkisel işleçler ve varlığı denetlenen ilişki çeşidi şunlardır: == (eşitlik), != (eşitsizlik), > (büyüklük), < (küçüklük), >= (büyük veya eşitlik), < (küçük veya eşitlik). ↑
  2. Mantıksal değerleri birleştirmede kullanılabilecek bağlaçlar ve anlamları şunlardır: ! (değilleme), && (ve), || (veya). ↑
  3. Gerekli görüldüğü takdirde, denetleme işinin tümü veya bir bölümü, yüklem olarak da adlandırılan boolean dönüş türlü bir metodun çağrılması ile yapılabilir. ↑
  4. Aslına bakacak olursanız, tür uyumluluğu için üstküme-altküme ilişkisinin kullanılması yanıltıcı bir uygulama. Çünkü, long ile temsil edilen kimi değerler ne double ne de float ile temsil edilebilirken, int ile temsil edilen kimi değerler float ile temsil edilemez. Ancak, anılan türler arasında dizilimin önerdiği cinsten bir tür uyumluluğundan bahsetmek her zaman doğrudur. ↑

13 Nisan 2011 Çarşamba

Java SE 7 İle Gelen Yenilikler

Bu yazıda, pek yakında piyasaya çıkacak olan Java SE 7'nin Java programlama diline getirdiği yeniliklere göz atacağız. Yeni uyarlamanın tartışmalarını J2SE 5.0 ile eklenen yeniliklerin—soysallık🔎 (İng., generics), açımlamalar (İng., annotations), sabit listeleri (İng., enum types), vd.—getirdiği zorlu öğrenme eğrisini hatırlayıp içi ürpererek izleyenlere müjdeyi vererek başlayalım: daha önceleri eklenmesi düşünülen birim (İng. module) ve kod örtüsü (İng. closure) kavramlarının bir sonraki uyarlamaya bırakılması nedeniyle, yeni uyarlamanın J2SE 5.0'de olduğu gibi zorlayıcı olması söz konusu değil. Ama, siz yine de yerlerinizde rahat oturmayın; çünkü, bu sadece bir erteleme, her iki kavram da eninde sonunda Java dilinin üstkavram dağarcığına—yani, problem uzayındaki kavramları koda dökerken kullanılan kavramlar listesine—eklenecek.1

Yeniliklerin içeriğine girmeden önce, okumakta olduğunuz yazının günlükteki diğer yazılardan belirgin bir farklılık gösterdiğini söylemekte yarar var. Daha öncekiler tek bir konuya odaklı ve basit içerikli iken, bu yazı Java altyapınızın sağlam olduğunu varsayıyor ve farklı konulara atıfta bulunuyor. Bir diğer uyarı da yenilikleri deneyip kullanmak isteyeceklere yönelik: söz konusu yeniliklerin hiçbiri yeni bir Bytecode komutu kullanılması sonucunu doğurmadığı için, Java SE 7 uyumlu bir derleyici ile üretilen sınıf dosyaları daha önceki uyarlamaların JSM'leri tarafından çalıştırılabilecektir.2


switch-case Komutlarında String Türlü Seçici İfade

switch-case komutlarındaki seçici ifadenin türü hakkındaki kısıtlamanın String türünü kapsayacak şekilde gevşetilmesi ile başlayalım. Buna göre, eskiden seçici ifadenin tamsayı türleri ve char'a kısıtlı olması nedeniyle geçerli olmayan aşağıdaki kod parçası, Java SE 7 ile birlikte Java derleyicileri tarafından kabul görecektir.
import java.util.Locale;
import static java.lang.System;
...
devamMı:
do {
  switch (seçim.toUpperCase(new Locale("tr"))) {
    case "EVET": case "YES": ...; break devamMı;
    case "HAYIR": case "NO": ...; break devamMı;
    default: out.println("Evet veya Hayır giriniz!!!");
  }
} while(true);
...

İkili Taban Gösterimi ve Rakamların Gruplanması

Bir diğer küçük yenilik, tamsayı sabitlerin artık 8'li, 10'lu ve 16'lı tabanın yanısıra 2'li tabanda da yazılabilecek olması. Ayrıca, sayısal sabitlerin, ister tamsayı türlü olsun isterse kayan noktalı, doğru ve çabuk algılanmasını sağlamak için altçizgi karakteri ('_') kullanılarak dilendiği gibi gruplanması da mümkün.
double bütçeAçığı = 8_000_000_000_000_000;
double piSayısı = 3.1415_92_6535_897_93;
short bayrakYazmacı = 0b0000_010010_000100;
...
Bir sonraki yeniliğe geçmeden bir uyarıda bulunalım: gruplama karakteri sayıyı oluşturan bileşenlerin (tamsayı kısmı, kesir kısmı, üs kısmı) başında veya sonunda kullanılamaz. Buna göre, aşağıdaki örneklerin hiçbiri Java derleyicisinin kabulünü görmeyecektir.
double bütçeAçığı = _8000_000_000_000_000;
double piSayısı = 3_.1415_92_6535_897_93;
short bayrakYazmacı = 0b0000_010010_000100_;
double büyükSayı = 0x1.2345p_123;
double küçükSayı = 0x1.2345p-123_;
...

Kaynak Yönetimli try Bloğu

Java'yı C/C++ dilinden ayıran temel özelliklerden birisi, geliştiricinin insan olduğu ve hata yapabileceği varsayımına binaen bazı sorumlulukları geliştiriciden derleyici ve sanal makine gibi sistem yazılımlarına aktarmasıdır. Bunun en bilinen örneği, çöp toplayıcı yardımıyla yığın bellek yönetimini yazılım geliştirme aşaması etkinliği olmaktan çıkarmasıdır.3 Ancak, çöp toplama desteği kaynak yönetme kaygılarının tamamen ortadan kalktığı anlamına gelmez; geliştiriciler yığın bellek dışındaki dosya, ağ bağlantısı, veri tabanı bağlantısı gibi kaynakları yönetmek zorundadırlar. Örnek olarak, bir veri tabanı tablosunun yazma kilidini düşünün. Kilidin sahibi olan program işini bitirip tabloyu serbest bırakmadıkça bir diğer programın aynı tabloda değişiklik yapması mümkün olmayacaktır; işin bitirilip ne zaman dönüleceğini ise ancak programı yazan geliştirici(ler) bilebilir. Dolayısıyla, bu tür kaynakların mümkün olan en erken noktada geliştirici tarafından döndürülmesi gerekir. Aksi bir durum, aynı kaynağın diğer kullanıcılarının boş yere beklemesi anlamını taşır. İşte bu yüzden, yeni uyarlama ile birlikte Java, try bloğu içinde yararlanılan kaynakların otomatik olarak döndürülmesini garanti ederek olası programcı hatasının önüne geçen kaynak yönetimli try bloğunu sunmaktadır.

Ne denmek istendiğini aşağıdaki kod şablonundan izleyerek görelim. İlk satırda, try'ı takip eden ayraç çifti arasındaki ilkleme komutu, Java derleyicisine program içinde d tanımlayıcısı yoluyla temsil edilen disk dosyasının otomatik olarak yönetilmesi istendiğini belirtiyor. Bu istemin yerine getirilmesi, işlenmesi ne şekilde sona ererse ersin, try bloğu sonunda söz konusu kaynağın geri döndürüleceği anlamına gelir.
try (FileInputStream d = new FileInputStream("Veri.dat")) {
  // d'yi kullan.
} catch (IOException e) { ... }
Örneğimiz otomatik olarak yönetilmesi istenen kaynağın Java SE 7 ile birlikte eklenen java.lang.AutoCloseable arayüzünü gerçekleştirmesiyle geçerlilik kazanır. Bu, java.lang.Closeable arayüzünün bu arayüzden kalıtlayacak şekilde tanımının değiştirilmesi nedeniyle, G/Ç sınıflarınca temsil edilen tüm kaynakların kaynak yönetimli try bloğuyla otomatik olarak yönetilebileceği anlamına gelir.

finally kotarıcısını (İng., handler) bilenleriniz—sabırsızlıklarını görür gibiyim—kaynak yönetimli try komutunun aslında pek de bir şey getirmediğini düşünebilir. Ne de olsa, yukarıda anlatılan işlev kaynak döndürme komutunu finally bloğu içine yazarak da sağlanabilir. Bir diğer deyişle, aşağıdaki kod şablonu da işimizi görecektir.
try {
  // d'yi kullan.
} catch (IOException e ) { ... }
  finally { d.close(); }
Bu itirazı seslendiren arkadaşlar bir yere kadar haklı. Ne var ki, şu iki nokta onları da ikna edecektir.
  • try ve catch bloklarının işlemesi sırasında JSM sonlan(dırıl)acak olursa veya söz konusu işletilen bloğun izleği (İng., thread) durdurulur veya sonlandırılacak olursa, finally bloğunun işletileceği garantisi verilemez.
  • Kaynak yönetimli try komutunun varlığı, programcıya hatırlatıcı olmasının yanısıra belgeleme yönüyle de katkıda bulunur.

Kotarıcı Paylaşımı ve Yeniden Fırlatmanın Yeni Anlamı

Kimi zaman, birden çok ayrıksı durum kotarıcısının aynı gövdeye sahip olması durumuyla karşılaşabiliriz. Aynı kodun tüm kotarıcılara tekrar tekrar yazılması, kod şişmesinin yanında gerekebilecek bir değişikliğin birden çok yerde yapılması sonucunu doğurur. İşte bu noktada, Java'nın yeni uyarlaması programcıya kotarıcı paylaşımı şansını verir. Buna göre, aşağıda verilen try bloğu içindeki komutların icra edilmesi sırasında AyrıksıDurumA veya AyrıksıDurumB türlü bir ayrıksı durum ortaya çıkacak olursa, aynı kotarıcı soruna çözüm bulmaya çalışacaktır.
...
try {
  // AyrıksıDurumA, AyrıksıDurumB veya AyrıksıDurumC türünde
  // ayrıksı durumların ortaya çıkabileceği bir şeyler yap.
} catch (AyrıksıDurumA | AyrıksıDurumB e} { ... }
  catch (AyrıksıDurumC e) { ... }
  finally { ... }
...
Kotarıcı paylaşımında şu nokta unutulmamalıdır: ayrıksı durum nesnesi kullanılarak kotarıcı içinde yapılacak şeyler kotarıcı parametresinin türlerinin ortak paydasına sınırlı kalacaktır. Dolayısıyla, yukarıda sağlanan paylaşım örneğinde e'nin kullanımı AyrıksıDurumA ve AyrıksıDurumB türlerinin ortak atasına gönderilebilecek iletilere sınırlı olacaktır. Bunun doğal bir sonucu olarak, parametrenin türleri arasında biri diğerinin atası olan sınıfların bulunması bir hata olarak görülecek ve söz konusu kod derleyici tarafından kabul edilmeyecektir.4

Hata kotarımını ilgilendiren ikinci yenilik, yeniden atılan ayrıksı durum nesnelerinin ele alınışına yönelik. Java SE 7 öncesinde hata verecek aşağıdaki kod parçasından izleyelim.
public void birMetot() throws AyrıksıDurumA, AyrıksıDurumB {
  ...
  try {
    birŞeylerYap(...); // AyrıksıDurumA fırlatabilir.
    başkaBirŞeylerYap(...); // AyrıksıDurumB fırlatabilir.
  } catch (Exception e} {
      ... // Bir şeyler yap.
      throw e;
    }
  ...
} // void birMetot() sonu
İşlerin yolunda gitmemesi halinde, birMetot metodundaki try bloğu, AyrıksıDurumA veya AyrıksıDurumB türlü bir ayrıksı durumla sonlanabilir. Ayrıksı durum nesnesinin fırlatılması sonrasında, Exception türlü parametreye sahip kotarıcı devreye girecek ve ortalığa çeki düzen verdikten sonra parametreyi yeniden fırlatarak topu metodun çağrıcısına atacaktır. İşte bu noktada, Java SE 7'nin farkı ortaya çıkıyor: derleyicinin try bloğu içinden çağrılan metotların imzalarını incelemesi sonucunda kotarıcıda yeniden fırlatılan nesnenin Exception değil de AyrıksıDurumA veya AyrıksıDurumB türünde olabileceği bilgisi korunuyor. Bu nedenledir ki, önceki uyarlamalarda Exception fırlatabileceği ilan edilmek zorunda kalınan birMetot, artık AyrıksıDurumA ve AyrıksıDurumB fırlatabileceğini ilan eden bir imza ile tanımlanabiliyor.

Bu bölümü küçük bir uyarı ile kapatalım: Değindiğimiz her iki yeniliğin kullanımında da kotarıcı parametresinin final olduğu varsayılır. Bir başka deyişle, söz konusu parametre yeni bir ayrıksı durum nesnesini gösterecek şekilde değiştirilemez; parametrenin değiştirilmesi anlatılan yeniliklerin kullanılmasını engeller.

Sınırlı Tür Çıkarsama: Elmas İşleci

Şapkadan çıkaracağımız bir sonraki yenilik de kendini derleyicinin yaptığı ek mesainin programcıya sağladığı kolaylıkla gösteriyor: tür çıkarsama. Ancak, Haskell veya ML bilenleriniz beklentilerini yüksek tutmasın. Çünkü, Java derleyicilerinin yeni uyarlamayla birlikte—soysallığın eklenmesi sonrasında sağlananın üstüne—sağlayacağı tür çıkarsama özelliği soysal türlerin nesnelerinin yaratıldığı kullanımlara kısıtlı. Derleyicinin tür çıkarsama için işaret olarak algıladığı <> karakterlerinin görünüşünden dolayı elmas işleci olarak da adlandırılan bu yenilik aşağıdaki gibi kullanılabilir.
import java.util.*;
...
Map<String, Vector<String>> sözlük =
  new HashMap<String, Vector<String>>();

Map<String, Vector<String>> yeniSözlük = new HashMap<>();

SafeVarargs Açımlaması

Göz atacağımız son yenilik olan SafeVarargs açımlaması, sınıf kitaplıkları yazanlarınız dışındakileri doğrudan etkilemiyor. Ancak, kitaplık kullanıcıları da kimi zaman anlamakta zorlandıkları derleyici uyarılarının azalması nedeniyle epey mutlu olacaklar. Ne kastettiğimizin anlaşılması için önce bu yeni açımlamanın ne zaman gerekeceğinin bir örneğini verelim.

SVÖrneği.java
import java.util.*;

public class SVÖrneği {
  public static void main(String[] args) {
    List<Integer> tl1 = Arrays.asList(1, 2, 3);
    List<Integer> tl2 = Arrays.asList(4, 5);

    List<List<Integer>> tll = Arrays.asList(tl1, tl2);
  } // void main(String[]) sonu
} // SVÖrneği sınıfının sonu
$ javac -encoding utf-8 -Xlint:unchecked SVÖrneği.java
SVÖrneği.java:8: warning: [unchecked] unchecked generic array creation of type java.util.List<java.lang.Integer>[] for varargs parameter
    List<List<Integer>> tll = Arrays.asList(tl1, tl2);
                                           ^
1 warning
asList metodunun işini nasıl yaptığını öngörebilen birisi olarak, aynı metodun çağrıldığı 5 ve 6. satırlar onaylanırken 8. satırın uyarı üretmesi, bazılarınıza garip gelebilir. Sanırım soysallıkla ilgili kısa bir açıklama bu arkadaşları ikna edecektir.

J2SE 5.0 sürümünde eklenen soysallığı kullanan kod ile J2SE 5.0 öncesindeki kodun bir arada kullanılabilmesi için derleme sırasında tür silme adı verilen bir dönüşüme başvurulur. Bu, soysal türlere dair tür argümanlarının korunmayacağı anlamını taşır. Mesela, örneğimizin 8. satırında argüman olarak geçirilen ve [statik] türleri List<Integer> olan tl1 ve tl2 değişkenlerinin türü List'e dönüştürülecektir. Dolayısıyla, çağırılan metot, argümanları değişik ve birbirinden bağımsız türlerden değerler içerebilecek List nesneleri olarak görecektir. Bu ise, çağrılan metodun bilerek ya da bilmeyerek Integer nesneler tutması beklenen liste(ler)i herhangi bir türden nesne tutacak hale döndürmesinin mümkün olduğu anlamına gelir. Buna karşın, derleyicinin böylesine bir kodu reddetmesi de, J2SE 5.0 öncesi ve sonrasında üretilmiş kodların bir arada çalışamayacağı anlamına geleceğinden, kabul edilebilir bir seçenek değildir. Çözüm, olası hataya dikkat çekmek için programcıya verilen bir uyarı ile sağlanır.

Örneğimizdeki uyarının ortadan kaldırılması 8. satır öncesine @SuppressWarnings("unchecked") açımlamasının konulması ile sağlanabilir. Java SE 7 öncesinde geçerli olan bu çözümün doğurduğu bir sorun, çok sayıda kullanıcısı bulunan metotlarda her kullanıcının söz konusu uyarı ile uğraşmak zorunda kalmasıdır. Bunun yerine, derleyicinin istediği garantinin metot gerçekleştirimcisi tarafından verilmesi daha yerinde olacaktır. İşte, SafeVarargs açımlamasının sağladığı budur. Örneğimize uyarlayacak olursak, Arrays sınıfının gerçekleştirimcilerinin kaynak kodu aşağıdaki gibi yazıp derlemesi tüm kullanıcıların yükünü omuzlarından alacaktır.
package java.util;
...
public class Arrays {
  ...
  @SafeVarargs
  public static <T> List<T> asList(T... değerler) { ... }
  ...
} // java.util.Arrays sınıfının sonu
Arrays.asList metoduna benzer şekilde SafeVarargs açımlamasıyla bezenen diğer Java platformu metotları aşağıda verilmiştir.
  • public static <T> boolean
      java.util.Collections.addAll(
        Collection<? super T> c, T... elemanlar)
  • public static <E extends Enum<E>>
      java.util.EnumSet<E> EnumSet.of(E ilk, E... gerisi)
  • protected final void
      javax.swing.SwingWorker.publish(V... parçalar)
Listedeki metotlardan da görülebileceği gibi, SafeVarargs açımlaması ile bezenmesi düşünülen metotların bazı özellikleri olması gerekir.
  • Yapıcılara ek olarak, ancak static veya final metotlara SafeVarargs açımlaması iliştirilebilir.
  • Yapıcı veya metodun değişken sayıda argüman alıyor olması gerekir.
Bu özellikleri taşımayan metotların SafeVarargs açımlamasıyla ilan edilmesi, derlenmekte olan sınıfın reddedilmesine neden olacaktır, haberiniz ola!


  1. Adı geçen üstkavramlar hakkında bilgi edinmek için, şu videoları izlemek isteyebilirsiniz: Java birimleri, kod örtüleri. Bu videolar, bir sonraki uyarlama ile Java'ya giriş yapacak bu programlama kavramları ile ilgili gerekli bilgileri sağlayacaktır. ↑
  2. Aslına bakarsanız, Java platformunun JRuby ve Jython gibi dinamik türlemeli dilleri kullanılarak yazılan programların daha hızlı çalışmasını sağlayacak bir yenilik, Java SE 7 ile birlikte gelen JSM'nin yeni bir Bytecode komutunu (invokedynamic) desteklemesi durumunu ortaya çıkardı. Dolayısıyla, bu diller kullanılarak yazılacak yeni programların derlenmesi sonucu oluşturulacak sınıf dosyalarının önceki uyarlamaların JSM'leri tarafından çalıştırılmaları mümkün olmayacaktır. Ancak, statik türlemeli olan Java kullanılarak yazılan programlarda böylesine bir durumun söz konusu olmaması nedeniyle bu bizi ilgilendirmiyor. ↑
  3. Bu noktada, olası bir yanılgıyı düzeltip Sezar'ın hakkını Sezar'a vermemiz gerekli. Her ne kadar Java yaygın kullanımlı diller içinde çöp toplayıcı desteğini getirerek öncü rolünü oynamış olsa da, bu aslında Lisp-temelli dillerde 1960 yılından bu yana bulunan bir özellik. ↑
  4. Biraz düşünüldüğünde bunun sebebi daha iyi anlaşılacaktır. Böylesine iki sınıfın ortak atası sınıflardan daha genel olanıdır ve kotarıcı parametresine gönderilecek iletiler bu sınıfın desteklediği iletilere sınırlı olacaktır. Yani, kalıtlayan sınıfın gönderilebilecek iletilerin belirlenmesinde—akılları karıştırmak dışında—hiçbir etkisi olmayacaktır. ↑