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

10 Kasım 2011 Perşembe

Soysallık

Kalıtlamanın getirilerinden biri, sınıflar arası ortak yönlerin bir üstsınıfta toplanmasına olanak tanıyarak kodun yeniden kullanımını sağlamasıdır.1 Bu sayede, değişikliğin gerektiği durumlarda birçok sınıfta değişiklik yapmaktansa üstsınıftaki kodun değiştirilmesi ile işin daha kısa sürede ve daha düşük hata oranlı bir biçimde yapılması mümkün olacaktır. Ne var ki, aynı yöntemin değişik türden verileri tutmada yararlanılan veri kaplarının gerçekleştiriminde kullanılması derleme zamanında denetlenebilecek kimi hataların çalışma zamanına kaymasına neden olmaktadır ki, bu piyasaya çıkmış bir yazılımın müşterinin "hatalı" kullanımı sonrasında göçebileceği anlamına gelir. Ne kastettiğimizi son giren ilk çıkar mantığıyla çalışan yığıt veri yapısının aşağıdaki gerçekleştirimi üzerinden görelim.

YığıtBoşDurumu.java
package vy.ayrıksıdurumlar;

public class YığıtBoşDurumu extends RuntimeException { ... }
IYığıt.java
package vy.arayüzler;

import vy.ayrıksıdurumlar.YığıtBoşDurumu;

public interface IYığıt {
  Object çıkar() throws YığıtBoşDurumu;
  void ekle(Object yeniElm);
  Object gözAt() throws YığıtBoşDurumu;
  boolean boşMu();
} // IYığıt arayüzünün sonu
Yığıt.java
package vy;

import java.util.Vector;
import vy.ayrıksıdurumlar.YığıtBoşDurumu;
import vy.arayüzler.IYığıt;

public class Yığıt implements IYığıt {
  public Yığıt() { _kap = new Vector(); }
  ...
  public Object çıkar() throws YığıtBoşDurumu {
    if (boşMu()) throw new YığıtBoşDurumu();

    Object üstteki = _kap.get(_kap.size() - 1);
    _kap.remove(_kap.size() - 1);

    return üstteki;
  } // Object çıkar() sonu 
  ...
  private Vector _kap;
} // Yığıt sınıfının sonu
Ekleme ve çıkarmanın elemanları tutan kabın aynı ucuna—mesela, Vector sonu veya LinkedList başı—yapılarak gerçekleştirilebilecek yığıt, söz konusu işlemlerin ilişkin arayüz tanımında da belirtildiği üzre Object tutacakları ile işlerini görmeleri nedeniyle herhangi türden bir nesneyi tutabilecektir. Buna göre, Yığıt sınıfının nesneleri yeri geldiğinde Öğrenci nesneleri tutarken, yeri geldiğinde Öğretmen nesneleri de tutabilecektir. Ancak, dikkat etmediğimiz takdirde aşağıda olduğu gibi bir durumun ortaya çıkması da olanaklıdır.
public static void yığıtıKullan() {
  IYığıt sınıf = new Yığıt();
  sınıf.ekle(new Öğrenci(...));
  Öğrenci ilkÖğrenci = (Öğrenci) sınıf.gözAt();
  if (Math.random() > Math.random())
    sınıf.ekle(new Öğrenci(...));
    else sınıf.ekle(new Öğretmen(...));
  ...
  Öğrenci sonÖğrenci = (Öğrenci) sınıf.çıkar();
  ...
} // void yığıtıKullan() sonu
Yığıt tanımımız Object tutacağı ile gösterilebilen türden—yani, Java'daki bileşke türlerin tümü—nesne tutabileceği için elemanların türdeş olma garantisi derleyici tarafından denetlenemez. Her ikisi de eninde sonunda Object'ten kalıtladığı için, aynı yığıta Öğrenci nesnesi de Öğretmen nesnesi de eklenebilmektedir. Bu ise, yukarıdaki kod parçasının son satırında olduğu gibi türdeşlik garantisinden hareketle yazılan satırların programın çalıştırılması sırasında ClassCastException ayrıksı durumuna neden olması demektir. Yani, yığıtımız ekleme sırasında itiraz etmediği nesnenin geri döndürülmesi sırasında kullanıcının kodunu göçertmektedir. Bunun önüne geçmek ancak öngörülen türlerin tümü için ayrı yığıt gerçekleştirimlerinin sağlanması ile olanaklıdır. Bu ise aşağıdaki gibi gereğinden kalabalık ve bakımı zor bir sınıf sıradüzeni anlamına gelir.

  • Object
    • Kişi
      • Çalışan
        • Öğretmen
        • Müstahdem
      • Öğrenci
    • Yığıt_Object, Yığıt_Kişi, Yığıt_Çalışan, Yığıt_Öğrenci, Yığıt_Öğretmen, Yığıt_Müstahdem, ...

Dilimizde iki ucu gaytalı değnek şeklinde nitelenen bu durum, J2SE 5.0 sürümüne kadar geçerli olmuş ve anılan sürüm ile birlikte Java diline eklenen soysallık yoluyla ortadan kaldırılabilmiştir. Bu özellik sayesinde, olası biçimlendirme hatalarının derleme zamanında önüne geçilerek tür güvenliği sağlanmış ve tek bir tür tanımının kullanılması ile kod bakımı kolaylaştırılmıştir. Soysallıktan yararlanarak oluşturulan sıradüzeni aşağıdaki gibi olacaktir.

  • Object
    • Kişi
      • Çalışan
        • Öğretmen
        • Müstahdem
      • Öğrenci
    • Yığıt<E>

Soysal Türlerin Tanımı ve Kullanımı


Soysal türlerin tanımı ve kullanımı metotları andırır. Nasıl ki, metotların hangi türden değerler geçirilerek işletilebileceğini belirtmek için ayraç çifti arasında belirtilen parametre listesi kullanılır, soysal türlerin hangi türler için parametrize edildiğini belirtmek için üçgen ayraç çifti arasında belirtilen tür parametre listesinden yararlanılır. Buna göre, yukarıdaki arayüz ve sınıfın soysal uyarlaması aşağıdaki gibi olacaktır.

IYığıt.java
package vy.arayüzler;

import vy.ayrıksıdurumlar.YığıtBoşDurumu;

public interface IYığıt<E> {
  E çıkar() throws YığıtBoşDurumu;
  void ekle(E yeniElm);
  E gözAt() throws YığıtBoşDurumu;
  boolean boşMu();
} // IYığıt<E> arayüzünün sonu
Yığıt.java
package vy;

import java.util.Vector;
import vy.ayrıksıdurumlar.YığıtBoşDurumu;
import vy.arayüzler.IYığıt;

public class Yığıt<E> implements IYığıt<E> {
  public Yığıt() { _kap = new Vector<E>(); }
  ...
  public E çıkar() throws YığıtBoşDurumu {
    if (boşMu()) throw new YığıtBoşDurumu();

    E üstteki = _kap.get(_kap.size() - 1);
    _kap.remove(_kap.size() - 1);

    return üstteki;
  } // E çıkar() sonu
  ...
  private Vector<E> _kap;
} // Yığıt<E> sınıfının sonu
Bir soysal türün nesnesinin yaratılması, tür parametrelerine karşılık gelen tür argümanlarının soysal türün adının ardından sağlanmasıyla mümkün olur. Bunun sonucunda soysal türün örneği olarak kullanılacak türe parametreli tür denir. Örneğin, aşağıdaki kod parçasında IYığıt<Öğrenci> parametreli türüne sahip sınıf değişkeni Yığıt<Öğrenci> parametreli sınıfına ait bir kabı göstermektedir ve bu kap Öğrenci tutacakları—dolayısıyla, Öğrenci köklü sıradüzenindaki sınıfların türünden nesneler—içerecektir.2 Ayrıca, dikkat ederseniz, tür parametresi ile belirtilen dönüş türlerine sahip iletiler/metotlar (gözAt, çıkar) biçimlendirme olmaksızın kullanılmaktadır; zira, biçimlendirme derleyici tarafından eklenen kod sayesinde otomatikman yapılmaktadır.
public static void yığıtıKullan() {
  IYığıt<Öğrenci> sınıf = new Yığıt<>();
  sınıf.ekle(new Öğrenci(...));
  // Biçimlendirmeye gerek yok
  Öğrenci ilkÖğrenci = sınıf.gözAt();
  if (Math.random() > Math.random())
    sınıf.ekle(new Öğrenci(...));
    else sınıf.ekle(new Öğretmen(...)); // Derleme hatası!!!
  ...
  Öğrenci öğr = sınıf.çıkar();
  ...
} // void yığıtıKullan() sonu
Bu noktada, tür argümanlarının bileşke türlü olmak zorunluluğu hatırlatılmalıdır. Dolayısıyla, IYığıt<int> tamsayılar = new Yığıt<>(); şeklinde bir tanımın yapılması derleyici tarafından kabul görmeyecektir. Ancak bu, parametreli sınıflara ait kaplara ilkel türlü değerlerin konulamayacağı anlamına gelmez. Yapılması gereken, kabı söz konusu ilkel türün karşılığındaki sarmalayıcı tür ile ilan edip işin gerisini derleyiciye bırakmaktır. Derleyici, eklenmek istenen ilkel türlü değeri usulca sarmalarken (İng., boxing, wrapping) döndürülen tutacağın gösterdiği nesneyi açarak (İng., unboxing) içeriği ilkel değere dönüştürecektir.
IYığıt<Integer> tamsayılar = new Yığıt<>();
tamsayılar.ekle(3); // Aşağıdaki ile aynı
tamsayılar.ekle(new Integer(3));
...
int üstteki = tamsayılar.çıkar();
// ≡ int üstteki = tamsayılar.çıkar.intValue();
Gerektiği takdirde, tür parametresine sınır getirerek kullanım noktasında geçirilmesi beklenen türlerin kısıtlanması sağlanabilir. Örnek olarak, sadece Number köklü sıradüzenindeki sınıfların nesnelerini içerebilecek bir kap düşünün. Bu istem, aşağıdaki sınıf başlığında olduğu gibi, Number sınıfının soysal türün parametresine üst sınır olarak getirilmesiyle ifade edilir. Böylece, SayıKabı soysal sınıfına üye parametreli sınıfların tür argümanı Number ya da Number'dan kalıtlayan sınıflara sınırlı olacaktır; derleyici diğer kullanımları hatalı kabul edecektir. Buna göre, SayıKabı<Byte> ve SayıKabı<Float> kabul görürken, SayıKabı<Object> ve SayıKabı<Character> reddedilecektir.
public class SayıKabı<E extends Number> { ... }
İstendiği takdirde, tür parametreleri gerçekleştirilmesi beklenen bir arayüz ile de sınırlandırılabilir. Hatta, sınıf adı ve birden çok arayüz adı birlikte verilerek de sınır konulabilir. Mesela, aşağıdaki soysal türün kullanımı noktasındaki kabul edilebilir tür argümanlarının Snf'den kalıtlaması ve Aryz1 ile Aryz2 arayüzlerini gerçekleştirmesi zorunludur.
public class SoysalTür<E extends Snf & Aryz1 & Aryz2> { ... }

J2SE 5.0 Öncesi Kod İle Birliktelik: Ham Türler


Java'daki soysal türler, J2SE 5.0 sürümüne dek geçen dokuz yıla yakın sürede üretilen kodun kullanılmasını sağlamak adına ham türleri destekler. Bu desteğin amacı, hali hazırda var olan milyonlarca satırın çöpe gitmesini önlemek ve söz konusu soysallık öncesi kodun zaman içinde dönüştürülmesini sağlamaktır.

Ham türlü tanımlayıcılar, soysal bir türün üçgen ayraç çifti ve tür argümanları olmaksızın, yani J2SE 5.0 öncesindeki gibi, kullanılması ile tanımlanır. Soysal türü tür parametrelerine Object geçiriliyormuş gibi kullanmaya denk olan bu kullanım, her şey eninde sonunda Object tutacağı yoluyla görülebileceği için, derleyicinin kullanım hatalarını denetleme yeteneğini ortadan kaldırır. Dolayısıyla, soysallık sonrası yazılan kod içinde ham türlerin kullanımından kaçınılmalıdır.

Ham türlerin kullanımı iki durumda zorunludur: i) Yeni kod içinden soysallık öncesi kod kullanıldığında, ii) eski kod içinden soysal kod kullanıldığında. Örneğimiz ile devam ederek iki duruma da bir bakalım. Varsayalım ki, soysallık öncesinde IYığıt ve Yığıt türlerini tanımladık ve sınadıktan sonra kullanıma sunduk. Bu türler, soysallığa dair tür parametreleri olmadığı için, tür argümanları geçirilerek kullanılamaz; kullanım, ister soysallık öncesi kod içinden olsun isterse soysallık sonrası kod içinden, aşağıdaki gibi olacaktır.
public static yığıtıKullan() {
  IYığıt sınıf = new Yığıt();
  sınıf.ekle(new Öğrenci(...));
  Öğrenci ilkÖğrenci = (Öğrenci) sınıf.gözAt();
  if (Math.random() > Math.random())
    sınıf.ekle(new Öğrenci(...));
    else sınıf.ekle(new Öğretmen(...)); // Derleme hatası vermez!
  ...
  Öğrenci sonÖğrenci = (Öğrenci) sınıf.çıkar();
  ...
} // void yığıtıKullan() sonu
Soysallık sonrası yazılmış olan kullanıcı kodun derlenmesi hataya sebep olmamakla birlikte, derleyicinin, bu çeşit bir kullanımı potansiyel çalışma zamanı hatalarına davet çıkarmak olarak görmesi nedeniyle, denetlenemeyen olası hata uyarısını (İng., unchecked warning) vermesine yol açacaktır. Yani, soysallık öncesindeki gibi sessiz kalmaktansa, derleyici olası hataya işaret etmekte ve bizden ya kullanıcı kodunu değiştirerek duruma açıklık getirmemizi ya da elimizin altındaysa hem kullanılan kodu hem de kullanıcı kodunu soysallık sonrası standartlarına getirmemizi istemektedir. Kullanılan kodun elimizin altında olmaması halinde, iki şey yapılabilir: i) Kullanılan kod soysal değilse, kodumuz içinde kullanımımızın doğru olduğuna dair derleyiciye garanti veririz, ii) kullanılan kod soysalsa soysal türü kullanma niyetimizi belirten tür argümanlarını kullanırız. İlk şık, aşağıdaki şekilde SuppressWarnings açımlamasıyla yerine getirilebileceği gibi derleyiciye geçirilecek -Xlint:-unchecked opsiyonu ile de yerine getirilebilir. Her iki durumda da yaptığımız, derleyiciye "unchecked" etiketli uyarıları göz ardı etmesini söylemektir. Ancak; ilk yöntemde uyarılar açımlamanın öncesine yerleştirildiği metot boyunca göz ardı edilirken, derleme opsiyonu yeğlendiğinde derleyicinin hoşgörüsü derlenen tüm koda yaygınlaştırılmaktadır.
@SuppressWarnings({"unchecked"})
public static void yığıtıKullan() {
  IYığıt sınıf = new Yığıt();
  sınıf.ekle(new Öğrenci(...));
  Öğrenci ilkÖğrenci = (Öğrenci) sınıf.gözAt();
  if (Math.random() > Math.random())
    sınıf.ekle(new Öğrenci(...));
    else sınıf.ekle(new Öğretmen(...));
  ...
  Öğrenci sonÖğrenci = (Öğrenci) sınıf.çıkar();
  ...
} // void yığıtıKullan() sonu
Kullanılan kodun elimizin altında olması halinde, verilen uyarı dikkate alınmalı ve kaynak kod soysallık sonrası standartlara getirilerek yeniden derlenmelidir. Yeniden derlenen kodun eski kullanıcıları bu değişiklikten etkilenmeyeceklerdir. Çünkü, soysallık öncesinde yazılmış kullanıcılar değiştirilmiş kodun gözünde ham türlerden yararlanan yeni koddan farklı değildir.

Daha Esnek Metot İmzaları İçin Joker Türler


Pek çok ölümlü Java programcısı için soysallık desteğini ustaların erişilmez alemine sınırlı kılan en başlıca etken, joker tür argümanlarının varlığıdır. Değişik biçimlerde kendini gösteren bu korkunç yaratığı, iki kümenin sahip olduğu ortak elemanların sayısını döndüren metodu gerçekleştirerek tanımaya başlayalım. Veri Kapları Çerçevesi'nin sağladığı Set arayüzünde karşılanmayan bu işlem, birinci argümandaki kümenin her bir elemanının diğer kümede var olup olmamasına göre sayaç değişkenini güncelleyen aşağıdaki metotla gerçekleştirilebilir.
import java.util.Set;
...
public static int ortakElmSayısı(Set km1, Set Km2) {
  int elmSayısı = 0;
  for (Object elm : km1)
    if (km2.contains(elm)) elmSayısı++;

  return elmSayısı;
} // int ortakElmSayısı(Set, Set) sonu
Bir önceki altbölümden dersini almış olanlarınız, metot imzasındaki ham türlere bakıp bildiklerinden kuşkuya düşerek, neden Set<Object> kullanılmamış, diye sorabilirler. Öncelikle, bu arkadaşları doğru yolda olduklarını söyleyerek yatıştıralım; gerçekten de, yukarıdaki imzada bulunan ham türler derleyicinin tür denetim desteğini engellemek suretiyle tehlikeye davet çıkarıyor. Ancak; ham tür yerine Set<Object> kullanmak da sorunu halletmiyor. Şöyle ki; T'nin S'den kalıtladığı ve G'nin soysal tür olduğu bir ortamda, G<T> G<S>'den kalıtlamaz. Bir diğer deyişle, G<T> nesneleri G<S> nesneleri olarak ele alınamaz. Somutlaştıracak olursak, Integer'ın Object'ten kalıtlıyor olmasına karşın, Set<Integer> Set<Object>'ten kalıtlamaz. Bu ise, iki türün uyumsuz olduğu ve birbirlerini ilklemekte veya birbirlerine atanmakta kullanılamayacağı anlamına gelir. Gelin, bu sonucu aşağıdaki kod parçasının üzerinden giderek pekiştirelim. Öncelikle, 3. satırda kümeyi yaratırken elemanlarımızın Integer (ve otomatikman sarmalanarak Integer'a dönüştürülen int) ile tür uyumlu olabileceğini ilan ediyoruz. Daha sonraki satırlarda, verdiğimiz bu söze uyarak kümemize eleman ekliyoruz. Ancak; intKüme ile Set<Object> türlü objKüme'yi ilkleyen son satır, derleyicinin sıkı denetimini aşamıyor ve hataya neden oluyor. Bunun sebebi, iki tutacak tarafından paylaşılan ve başta Integer ile tür uyumlu nesneler ile doldurulacağı ilan edilen küme nesnesinin artık objKüme aracılığıyla Object ile tür uyumlu nesneler—yani, Java nesne alemindeki bütün nesneler—ile doldurulması ihtimalidir.
import java.util.*;
...
Set<Integer> intKüme = new TreeSet<>();
intKüme.add(new Integer(0));
...
Set<Object> objKüme = intKüme; // Derleme hatası!!!
Derdimizin çaresi tür argümanı olarak joker kullanımından geçer. ? ile belirtilen joker, adını bilmediğimiz veya umursamadığımız türler için kullanılır. Aşağıdaki metot imzasını buna uygun okuyacak olursak, ortakElmSayısı'nın herhangi bir türden elemanlara sahip iki Set beklediğini söyleyebiliriz.
public static int ortakElmSayısı(Set<?> km1, Set<?> Km2) {
  ...
} // int ortakElmSayısı(Set<?>, Set<?>) sonu
Şu iki noktanın akılda tutulmasında yarar olacaktır: i) farklı parametrelerde kullanılan jokerler birbirinden bağımsızdır ve farklı türlerle eşleştirilebilirler, ii) joker parametreli türün nesnesine eleman olarak sadece null eklenebilir.3

Yığıt arayüzüne iki yeni ileti ekleyerek devam edelim. Bunlardan yükle, argümanındaki kabın elemanlarını teker teker hedef nesneye eklerken, temizle hedef nesneyi boşaltırken silinen elemenları daha sonraki kullanımlar için argümanda geçirilen kaba kaydediyor.
import java.util.Collection;

public class IYığıt<E> {
  ...
  public void temizle(Collection<E> kopya); // Kapsayıcı değil!
  public void yükle(Collection<E> kaynak); // Kapsayıcı değil!
  ...
} // IYığıt<E> arayüzünün sonu
İlk denememiz, soysal türlerin daha önceden bahsettiğimiz kalıtlama ile birlikte değişmeme özelliğinden dolayı tüm kullanımları kapsamıyor ve kimi zaman kullanıcı tarafında derleme hatasına neden olabiliyor. Anılan özelliği yinelemektense, yükle iletisinin bir kullanım örneğine bakarak durumu anlamaya çalışalım.
IYığıt<Kişi> güruh = new Yığıt<>();
// güruh'a bir şeyler koy
Vector<Öğrenci> sınıf = new Vector<>();
// sınıf'a bir şeyler koy
güruh.yükle(sınıf); // Derleme hatası!!!
Belli ki, yukarıdaki kodu yazan arkadaş Öğrenci sınıfının Kişi'den kalıtladığını düşünerek Vector<Öğrenci> adlı parametreli türün de Collection<Kişi>'yi gerçekleştiren Vector<Kişi>'den kalıtladığı sonucuna varmış. Ne var ki, soysallığın kalıtlama ile birlikte değişme özelliği olmaması nedeniyle, öncülü doğru olan bu tümcenin sonuç kısmı hatalı. Yani, Vector<Öğrenci> Vector<Kişi>'den kalıtlamaz. İş böyle olunca, kodun son satırı parametre (Collection<Kişi>) ile argüman (Vector<Öğrenci>) arasındaki tür uyumsuzluğu nedeniyle derleme hatasına yol açıyor.

İçine düştüğümüz sıkıntı, sınırlı joker kullanımıyla çözülebilir. Yukarıdaki örneği sürdürerek ifade edecek olursak; derleyiciye söylememiz gereken, Yığıt<Kişi> türlü bir yığıta Kişi veya Kişi'den kalıtlayan herhangi bir sınıfa ait elemanlar içeren bir kap ile yükleme yapılabileceğidir. Bu ise, yükle'nin parametresinin Collection<? extends E> türüne sahip ilan edilmesi ile olanaklıdır. Yani, hedef nesneye girdi sağlama görevi gören kap, üst sınırlı joker türüyle tanımlanmalıdır.

temizle iletisinin imzasında ortaya çıkan sorun da sınırlı jokerlerin koşut bir kullanım biçimiyle sağlanabilir. Önce derleyicinin kabul etmeyeceği bir kullanıcı kodu görelim.
IYığıt<Öğrenci> sınıf = new Yığıt<>();
// sınıf'a bir şeyler koy
Vector<Kişi> güruh = new Vector<>();
sınıf.temizle(güruh); // Derleme hatası!!!
Bu örnekte de esnek olmayan bir imzanın cezasını çekiyoruz: Öğrenci tür argümanıyla yaratılan sınıf ancak ve ancak Öğrenci tutacakları içeren bir kaba kaydedilebiliyor. Kullanıcının yapmak istediği gibi kap Kişi eleman türüne sahip olduğunda, derleyici karşımıza dikiliveriyor. Halbuki, Öğrenci tutacağı ile görülebilen nesneler Öğrenci'nın atası olan tüm sınıfların (Kişi ve Object) tutacakları ile de görülebilir. Yani, imzanın kullanıcımızın yapmak istediğine izin verecek şekilde gevşetilmesi gerekir. Bir diğer deyişle, yığıt içeriğinin kaydedildiği kabın eleman türünün Öğrenci ve Öğrenci'nin atası olan herhangi bir sınıf olabileceğini derleyiciye bildirmemiz gerekir. Bu ise, alt sınırlı bir joker türünün kullanımı ile olanaklıdır ve örneğimizde imzadaki parametre türünün Collection<? super E> şeklinde değiştirilmesi işimizi görecektir.

Buna göre, arayüze eklenmek istenen iletilerin imzası şu şekilde oluşur. Ortaya çıkan imzalar, parametre listesindeki kaplar için genelde izlenmesinde yarar olacak bir kuralı da ele vermektedir: Hedef nesneye girdi sağlama görevi gören kaplar üst sınırlı joker tür, hedef nesnenin ürettiği çıktının kaydedildiği çıktı amaçlı kaplar ise alt sınırlı joker tür ile tanımlanmalıdır.
import java.util.Collection;

public class Collections {
  ...
  public void temizle(Collection<? super E> kopya);
  public void yükle(Collection<? extends E> kaynak);
  ...
} // IYığıt<E> arayüzünün sonu

Soysal Metotlar


Veri kapları dışında soysallıktan yararlanılan bir diğer programlama öğesi metotlardır. Genelde soysal kaplar üzerinde çalışan bu metotların soysallığı, niteleyicilerinin sonrasında kullanılan tür parametreleri yoluyla ifade edilir. Standart Veri Kapları Çerçevesi'ndeki değişik türden kaplar üzerinde uygulanabilecek metotları içeren java.util.Collections sınıfında bulunan sıralama metotlarına bakarak görelim.
package java.util;

public class Collections {
  ...
  public static <E extends Comparable<? super E>> void
    sort(List<E> liste) { ... }
  public static <E> void
    sort(List<E> liste, Comparator<? super E> karşılaştırıcı) { ... }
  ...
} // Collections sınıfının sonu
Sıralama, çok özel koşullarda kullanılabilecek bazı algoritmalar dışında, elemanların karşılaştırılması yardımıyla icra edilen bir yeniden düzenleme işlemidir. Dolayısıyla, sıralanması istenen kabın elemanlarının karşılaştırılabilir bir türe ait olması gerekir. Bu beklenti Java'da iki şekilde karşılanabilir:
  1. Elemanların ait olduğu sınıf Comparable arayüzünü gerçekleştirir. Bu noktada, gerçekleştirme ilişkisinin doğrudan olması gerekmediği unutulmamalıdır. Genel olarak, bir nesne üyesi bulunduğu sınıfın atası olan herhangi bir sınıftaki compareTo metodu ile karşılaştırılabilir. Bu sebepten ötürü, List<E>'nin sıralanabilmesi için E'nin Comparable arayüzünü gerçekleştirmesi veya gerçekleştiren bir üstsınıfa sahip olması gerekir. Bu ise yukarıdaki imzada olduğu gibi alt sınırlı bir joker tür ile belirtilebilir.
  2. Elemanları karşılaştırmayı bilen bir başka sınıf bu talebi karşılar. Bunun için, söz konusu sınıfın java.util paketindeki Comparator arayüzünü eleman türüne uyumlu bir şekilde gerçekleştirmesi gerekir. Tür uyumluluğu, bir önceki maddede olduğu gibi alt sınırlı joker tür ile belirtilmelidir.

Soysallık ve Diziler


İlk öğrendikleri programlama dilinin komutsal olması nedeniyle Java'da da gerekli gereksiz dizi kullanmaya alışanları soysal türler kötü bir sürprizle karşılar: dizilerin bileşen türü tür parametresi kullanılarak ifade edilemez. Örneğin, Yığıt sınıfının aşağıdaki şekilde dizi kullanacak biçimde gerçekleştirilmesi derleme hatasına neden olacaktır.
...
public class Yığıt<E> implements IYığıt<E> {
  public Yığıt() { 
    _kap = new E[]; // Derleme hatası!!!
    ...
  } // varsayılan yapıcı sonu
  ...
  private E[] _kap;
} // Yığıt<E> sınıfının sonu
Bunun sebebi, dizi ve soysal türler arasındaki temel bir farktan kaynaklanır: dizilerin tür bilgileri derleme sonrasına da taşınırken, soysal türlerin tür bilgileri derleyicinin kullanımı sonrasında silinir. Mesela, aşağıdaki kod parçasında intDz değişkeninin [ve tüm diğer Integer elemanlı dizilerin] üstnesnesi Integer[].class iken dblDz değişkeninin [ve tüm diğer Double elemanlı dizilerin] üstnesnesi Double[].class'dır. İstenecek olursa, ilişkin üstnesne (ve eleman türünün üstnesi [Integer.class ve Double.class]) kullanılarak içgörü (İng., reflection, introspection) yardımıyla Integer veya Double elemanlı yeni diziler yaratılabilir. Buna karşılık, intVec ve dblVec değişkenlerinin her ikisi de aynı üstnesneye sahip olacaktır: Vector.class. Çünkü, derleme sırasında kullanılan tür bilgileri tür silme (İng., type erasure) sonunda yok olmuş ve tüm parametreli türler aynı üstnesneyle temsil edilmek zorunda kalmıştır.4
Integer[] intDz = new Integer[10]();
Double[] dblDz = new Double[5]();
...
Vector<Integer> intVec = new Vector<>();
Vector<Double> dblVec = new Vector<>();
Bu nedenden ötürü, dizi kullandığımız yukarıdaki gibi durumlarda, ya dizi kullanmaktan vazgeçerek soysal türlerden birine yönelmemiz ya da derleme hatası vermemekle birlikte derleyici uyarısına neden olan şu kodu tercih etmemiz gerekir. Tavsiye edilen birinci yolun seçimidir.
...
public class Yığıt<E> implements IYığıt<E> {
  public Yığıt() { 
    _kap = (E[]) new Object[]; // Derleyici uyarısı!
    ...
  } // varsayılan yapıcı sonu
  ...
  private E[] _kap;
} // Yığıt<E> sınıfının sonu


  1. Anlatım, haklı olarak, kalıtlamanın sadece sınıflar arası geçerli bir ilişki olduğu izlenimini uyandırabilir. Fakat bu kesinlikle doğru değil; kalıtlama sınıflar arasında olduğu gibi arayüzler arasında da geçerli olan bir ilişkidir.
  2. Kod parçasında Java SE 7 ile eklenen elmas işlecinin kullanımına dikkat ediniz. Dolayısıyla, çalışma ortamını henüz güncellememiş olanlar bu kodu denediklerinde derleyici hatası ile karşılaşacaktır. Bu hatanın giderilmesi için tanımın şu şekilde tür çıkarsama olmaksızın yapılması gerekir.

    IYığıt<Öğrenci> sınıf = new Yığıt<Öğrenci>();

    Elmas işleci ve diğer Java SE 7 yenilikleri için buraya🔎 bakınız.
  3. Bazılarınızın bunun çalışma zamanı hatasına gebe bir durum olduğunu söylediklerini duyar gibiyim. Doğru ya, ilk argüman olarak Öğrenci nesneleri tutan, ikinci argüman olaraksa Öğrenci ile alakasız Koyun sınıfının nesnelerini tutan bir kullanım düşünebiliriz. Bu durumda, metodumuzdaki contains iletisinin gerçekleştirimindeki equals çağrısı, Öğrenci nesneleri ile Koyun nesneleri karşılaştırılamayacağı için, çuvallayacak ... mıdır acaba? equals iletisinin Object sınıfında tanımlanan genel sözleşmesine baktığınızda, true döndürme koşulunun hedef nesne ile uyumlu bir türe ait argümandaki null olmayan nesnenin eşit addedilmesi olduğu, geri kalan durumlarda ise false döndürülmesi gerektiğini görürsünüz. Dolayısıyla, equals iletisinin Öğrenci sınıfındaki gerçekleştirimi hedef nesne ile uyumsuz olan bir nesne gördüğünde, kodun devamında bir hatanın oluşmasına sebebiyet vermeden false döndürmelidir.
  4. Bunun bir sonucu olarak, üstnesneden yararlanarak işini gören instanceof işleci de soysal türün ham tür halini bekler. Yani, kullanım if (nesne instanceof Yığıt) ... şeklinde olmalıdır.

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.