27 Ekim 2011 Perşembe

Hatasız Programcı Olmaz: Java'da Hata Kotarımı

Yazılım gerçek dünyada var olan somut/soyut süreçlerin bilgisayar donanımı üzerinde çalıştırılan benzetimleridir. Amaç, sürecin, etkileşimde bulunulan diğer süreçler ile birlikte daha verimli ve sağlıklı bir şekilde işletilmesini sağlamaktır. Mesela, bir otomasyon yazılımı zayiatı azaltmak ve bakımı hızlandırmak suretiyle parçası olduğu üretim sürecinin daha verimli olmasını sağlayacaktır. Doğal olarak, bir yazılımın başarılı kabul edilmesindeki en önemli ölçüt, bilgisayarda oluşturulan hesaplama sürecinin benzetimi yapılmakta olan sürecin algılanışına1 sadık kalıp kalmadığı şeklinde ifade edilebilecek doğruluk (İng., correctness) özelliğidir. Peşinden koşulması gereken bir diğer özellik, yazılımın öngörülmeyen koşullar altında da kabul edilebilir bir tepki göstermesi olarak da tanımlayabileceğimiz dayanıklılıktır (İng., robustness). Bu beklentiler yazılım sektörünün rekabete dayalı pragmatik bir dünya olması gerçeği ile birlikte ele alındığında, yazılım üreticisinin birincil görevinin düzgün bir peformansa sahip, doğru ve dayanıklı—yani, güvenilir (İng., reliable)—programların makul bir hızda geliştirilmesi olduğu söylenebilir. İşte bu yazıda, ifade edilen görev tanımındaki güvenilirlik özelliğine yönelik olarak Java programlama dili tarafından sağlanan desteğe değineceğiz.

Kötü bir haber vererek başlayalım: Java, programlarınızın doğruluğunu sağlamak bağlamında kaynak kodunuzun dilin yazım kurallarına uygunluğunu denetlemek dışında pek bir destek sağlamaz; sözdizimsel hataların ötesinde Java derleyicisinin yapacağı çok şey yoktur. Aşağıdaki örnek üzerinden ne demek istediğimizi açalım.

Faktöryel.java
import static java.lang.System.out;
...

public class Faktöryel {
  public static void main(String[] ksa) {
    byte b = Byte.parseByte(ksa[0]);
    out.println(b + "!: " + fakt(b));
    ...
  } // void main(String[]) sonu
 
  public static long fakt(byte n) {
    long çarpım = 1;
    for (byte i = 2; i <= n; i++) çarpım += i;
  
    return çarpım;
  } // long fakt(byte) sonu
  ...
} // Faktöryel sınıfının sonu
$ javac -encoding UTF-8 Faktöryel.java
$ java Faktöryel 5
5!: 15 ✘
Görüldüğü gibi, sözdizimsel bir hata bulunmamakla birlikte, faktöryel kavramının hatalı gerçekleştiriminden kaynaklanan bir mantıksal hata ortaya çıkmıştır. Derleyicinin müdahele ederek işaretli satırdaki + yerine * yazılması gerektiği yorumunu yapması olanaksızdır. Bu gibi hataların, kodun sınanması sırasında keşfedilerek programcı tarafından ortadan kaldırılması gerekir. Bir an için bunun yapıldığını ve hatanın düzeltilerek doğru bir gerçekleştirimin sağlandığını düşünelim. Bu durumda, programımızın çalıştırılması aşağıdaki sonuçları verecektir.
$ java Faktöryel 5
5!: 120 ✔
$ java Faktöryel 20
20!: 2432902008176640000 ✔
$ java Faktöryel 21
21!: -4249290049419214848 ✘
Bir ihtimal Java'daki tamsayı türlerinin taşma davranışını bilmeyenleriniz, herhangi bir sayının eksi değerli bir faktöryele sahip olduğuna şaşırabilir. Bu gruptaki arkadaşlara önerim, daha fazla ilerlemeden şu yazıya🔎 bir göz atmaları. Diğer arkadaşlar ise, bunun sebebinin çarpım sonucunun long değerler için söz konusu olan aralığa düşmemesi nedeniyle kırpılması olduğunu bileceklerdir. Peki bu durumda, faktöryel metodumuzun doğru olmadığını söyleyebilir miyiz? Faktöryelin tanımını bilen herkesin bu soruya hayır yanıtı vermesi gerekir; ortada olan şey bir hatadan çok yetersizliktir ve muhtemelen metodun kullanıcısı ile iletişim eksikliğinden kaynaklanmaktadır. Soruna değişik şekillerde çözüm getirilebilir.
  1. Gerçekleştirime dokunulmaz ve kullanıcıya sağlanan belgelerde metodun 0 ile 20 arasındaki tamsayılar için doğru çözümler ürettiği ve dolayısıyla sağlanması beklenen argüman değerinin bu aralıkta olması gerektiği söylenir.
  2. Duruma has bir başka çözüm sağlanarak eksiklik giderilir. Örneğimizde, dönüş türü olarak long yerine java.math.BigInteger türünün kullanılması ve kodda buna göre değişikliklerin yapılması işimizi görecektir.
  3. Geçirilen argüman değerinin beklenen aralıkta olup olmadığı denetlenir. Bu, basit bir if komutuyla yapılabileceği gibi doğruluk savı (İng.; assertion) denetimiyle de yapılabilir. İlk yolun seçilmesi durumunda, görülen olumsuzluk dönüşte [0 gibi] özel olarak yorumlanan bir değerle belirtilebileceği gibi söz konusu durumu özetleyen bir ayrıksı durum (İng., exception(al condition)) nesnesi ile de bildirilebilir.
İkinci ve üçüncü maddelerdeki yaklaşımların nasıl gerçekleştirilebileceğini aşağıdaki alternatif çözümü izleyerek görelim. Özyinelemeli olan yeni metodumuzda, argümanın eksi olması hali, ki bu beklenilmeyen bir kullanıma işaret eder, çağırıcıya IllegalArgumentException türündeki bir ayrıksı durum nesnesi ile bildirilirken, argümanın 0 veya 1 olması halinde 1, geri kalan durumlarda ise matematikten tanıdık n * (n-1)! değeri döndürülmektedir. java.lang paketinde tanımlanan IllegalArgumentException, sağlanan argüman değerinin, -5'in faktöryelinin bulunmaya çalışılmasında olduğu gibi, uygunsuz olduğuna işaret eder.
...
import java.math.BigInteger;

public class Faktöryel {
  public static void main(String[] ksa) {
    ...
    long l = Long.parseLong(ksa[1]);
    try { out.println(l + "!: " + fakt2(l)); }
      catch(IllegalArgumentException a) {
        out.println(l + "!: " + fakt2(Math.abs(l)) + "!!!");
      }
  } // void main(String[]) sonu
  ...
  public static BigInteger fakt2(long n) {
    if (n < 0)
      throw new IllegalArgumentException(String.valueOf(n));
    if (n < 2)
      return BigInteger.ONE;
      else return BigInteger.valueOf(n)
                            .multiply(fakt2(n - 1));
  } // BigInteger fakt2(long) sonu
} // Faktöryel sınıfının sonu
$ java Faktöryel 21 21
21!: -4249290049419214848 ✘
21!: 51090942171709440000 ✔
$ java Faktöryel 5 -5
5!: 120 ✔
-5!: 120!!!
Beklenmedik koşulların oluştuğu noktada, özet bilgi içeren bir ayrıksı durum nesnesinin yaratılıp throw komutu ile fırlatılması gerekir. Bunun sonrasında, ortaya çıkan durumun çözümünü bilen birisinin duruma el atıp bir şeyler yapması beklenir. Biraz düşünüldüğünde, çözümün içine düşülen duruma sebep olanlar tarafından sağlanabileceği görülecektir. Bu ise, ayrıksı durumun ortaya çıktığı noktaya gelinene kadar izlenen çağrı zinciri üzerindeki noktalar demektir. Bir diğer deyişle, çare ya sorunun ortaya çıktığı noktada ya da o noktaya gelinmesi ile son bulan çağrı yığıtındaki diğer metotlarda bulunabilir. Çözüme dair bir şeyler yapabileceğini düşünenlerin bu iddialarını try-catch yapısı içinde bildirmeleri beklenir. try, takip eden kıvrımlı ayraç çifti arasındaki komutların sorun çıkarabileceğini bildirirken, kotarıcı olarak adlandırılan catch bloğu/blokları iliştirildikleri korumalı bölgede ortaya çıkabilecek sorunların tümü veya bazıları için çözümler sunar. Çağrı zinciri üzerindeki noktaların hiçbirinde çözüm önerilmemesi halinde ise, top denetim akışı geriye sarılarak programı başlatan JSM'ye atılacak ve JSM de ortaya çıkan durumu oluştuğu yerden başlayarak nerelere uğrayarak çözmeye çalıştığını söyleyen bir raporu standart hata ortamına2 yazarak programı sonlandıracaktır. Bunun nasıl olduğuna aşağıdaki çıktıya göz atarak bir bakalım.
...
public class Faktöryel {
  public static void main(String[] ksa) {
    ...
    long l = Long.parseLong(ksa[1]);
    out.println(l + "!: " + fakt2(l));
  } // void main(String[]) sonu
  ...
  public static BigInteger fakt2(long n) {
    if (n < 0)
      throw new IllegalArgumentException(String.valueOf(n));
    ...
  } // BigInteger fakt2(long) sonu
} // Faktöryel sınıfının sonu
$ java Faktöryel 5 -5
5!: 120 ✔
Exception in thread "main" java.lang.IllegalArgumentException: -5
 at Faktöryel.fakt2(Faktöryel.java:20)
 at Faktöryel.main(Faktöryel.java:9)
JSM'ye bakılacak olursa, programın [main adına sahip] ana izleğinde Faktöryel sınıfının main metodu içinden 9. satırda aynı sınıftaki fakt2 metodu çağrılmış ve denetim akışı bu metottta iken 20. satırda IllegalArgumentException ayrıksı durumu ortaya çıkmış. Anlamadıysanız ikinci bir örnekle açıklık getirmeye çalışalım; anlayanlar da bu sayede pekiştirmiş olurlar.
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.Scanner;

public class Örnek {
  public static void main(String[] ksa) {
    // ...
    m1();
    // ...
  } // void main(String[]) sonu

  private static void m1() {
    // ...
    m2();
    // ...
  } // void m1() sonu

  private static void m2() {
    String dosyaAdı;
    Scanner grdKanalı = new Scanner(System.in);
    System.out.print("Dosya adını giriniz: ");
    dosyaAdı = grdKanalı.nextLine();
    // ...
    m3(dosyaAdı);
    // ...
  } // void m2() sonu

  private static void m3(String dosyaAdı) {
    BufferedReader dosya =
      new BufferedReader(new FileReader(dosyaAdı));
    // ...
  } // void m3(String) sonu
} // Örnek sınıfının sonu
İşaretli satırlarda, argümanda geçirilen ada sahip ve o anki çalışma dizini içinde bulunan bir dosyanın tamponlanarak ve karakter yönelimli bir biçimde okunması için uygun türden bir akak nesnesi yaratılıyor. Bu işlem, sağlanan ada sahip bir dosyanın bulunmaması halinde java.io.FileNotFoundException ayrıksı durumu ile sonlanacaktır. Dolayısıyla, programın derlenip var olmayan bir dosyanın adı verilerek çalıştırılması, yukardakine benzer bir çağrı yığıtı özetinin üretilmesine neden olacaktır diye düşünebiliriz. Ancak, gelin görün ki, program derlendiğinde durumun hiç de öyle olmadığı görülür.
$ javac -encoding UTF-8 Örnek.java 
Örnek.java:29: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
      new BufferedReader(new FileReader(dosyaAdı));
                         ^
1 error
Yukarıdaki çıktıdan da görülebileceği gibi, IllegalArgumentException için sesini bile çıkarmayan derleyici, iş FileNotFoundException'a geldiğinde yaygarayı basmaktadır. Çifte standardın sebebi, ayrıksı durumların iki kategoriye ayrılmasıdır:
  • RuntimeException köklü sınıf sıradüzeninde olanlar: Bu kategoriye giren ayrıksı durumlar, ya ortaya çıktıkları noktada yakalanıp kotarılmalı ya da çağrı yığıtındaki diğer metotlardan birinde kotarılması gerektiğini hatırlatmak için metot imzasında ilan edilmelidirler.
  • Diğerleri: Bu ayrıksı durumların ne yakalanması ne de, yakalanmadıkları takdirde, metot başlığında ilan edilmeleri zorunludur.
Dolayısıyla, ilk kategoriye giren FileNotFoundException ayrıksı durumunun ortaya çıkması olasılığının belirmesiyle derleyici bizim kotarmak ya da bu görevi ihale etmek yönünde bir karar verdiğimizi görmek ister. Bu istemin karşılanmaması halinde ise söz konusu eksikliğe dikkat çekerek derlemenin başarısızlıkla sonlandığını bildirir. Burada bir noktanın özellikle vurgulanması yerinde olacaktır: derleyici, kodu okuyup var olmayan bir dosyanın girileceğinden emin olduğu için değil, böyle bir olasılığın var olduğu için itiraz etmektedir. Derleyicinin gözünde, FileReader sınıfının kullanılan yapıcısı FileNotFoundException türlü bir ayrıksı durum nesnesi fırlatabilir ve programcının da buna karşı hazırlıklı olduğunu kanıtlaması gerekir.

Kotarımın sorunun çıktığı nokta yerine çağrı yığıtındaki diğer metotlara bırakıldığı metot başlığına eklenen throws bildirimi ile ilan edilir. Buna bir örnek aşağıda verilmiştir: m3'te ortaya çıkabilecek sorun öngörülmüş fakat metot içinde çözülmektense metot başlığında belirtilmek suretiyle çağrı yığıtındaki diğer metotlara bırakılmıştır. Daha kesin ifade edecek olursak, m3'te ortaya çıkabilecek sorun için top önce m2'ye ve oradan da m1'e atılmış ve hal çaresi bu metot içinde sağlanmıştır.
...
import java.io.FileNotFoundException;

public class Örnek {
  public static void main(String[] ksa) {
    // ...
    m1();
    // ...
  } // void main(String[]) sonu

  private static void m1() {
    // ...
    try { m2(); } catch(FileNotFoundException a) { ... }
    // ...
  } // void m1() sonu

  private static void m2() throws FileNotFoundException {
    ...
    // ...
    m3(dosyaAdı);
    // ...
  } // void m2() sonu

  private static void m3(String dosyaAdı)
    throws FileNotFoundException {
    BufferedReader dosya =
      new BufferedReader(new FileReader(dosyaAdı));
    // ...
  } // void m3(String) sonu
} // Örnek sınıfının sonu
Kotarıcıların iliştirildikleri korumalı bölgeye özel oldukları unutulmamalıdır. Bundan dolayı, aşağıdaki kod parçası derleyici tarafından kabul görmeyecektir; kotarıcı m2 metodunun sadece birinci kullanımına çözüm sağlamaktadır.
private static void m1() {
    // ...
    try { m2(); } catch(FileNotFoundException a) { ... }
    // ...
    m2(); // ⇒ Derleme hatası!
    // ...
  } // void m1() sonu
Bu noktada, neden iki kategori olduğunu soruyor olabilirsiniz. Aşağıdaki örnekle anlamaya çalışalım. Belli ki, bu basit programı yazan arkadaş Java'da dizilerin 0-temelli indislere sahip olduğunu unutmuş ve üç elemanlı bir int dizisi yarattıktan sonra üçüncü eleman yerine 3 indisiyle gösterilen elemanın değerini 4 olarak güncellemek istemiş. Yani, Java programlama bilgisi eksik olan arkadaş derleme zamanında yakalanamayan bir hata yapmış. Takdir edersiniz ki, böylesine bir hatanın kotarıcıda düzeltilmesi ve programa devam edilmesi olanaklı değildir. Yapılması gereken, programcı arkadaşımızın dizileri daha iyi öğrenmesi ve kullanıcıdan 0..2 aralığında değer istemesi gerektiğini farketmesidir. Çünkü, ayrıksı durum düzeneği programın beklenmedik durumlarda daha sağlıklı bir tepki vermesini sağlamak için kullanılır, kötü programcıların programlama hatalarını düzeltmesi gibi erişilmesi olanaksız bir amaç için değil.
import java.util.Scanner;

public class DiziKullanımı {
  public static void main(String[] ksa) {
    int[] iDz = {1, 2, 3};
    Scanner grdKanalı =  new Scanner(System.in);
    System.out.print("1..3 aralığında bir sayı giriniz: ");
    iDz[grdKanalı.nextInt()] = 4;
  } // void main(String[]) sonu
} // DiziKullanımı sınıfının sonu
$ javac -encoding UTF-8 DiziKullanımı.java
$ java DiziKullanımı
1..3 aralığında bir sayı giriniz: 3
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
 at DiziKullanımı.main(DiziKullanımı.java:8)
Benzer bir gözlem null değerli bir tutacak yoluyla ileti gönderilmeye çalışılması sonrasında ortaya çıkan NullPointerException için de yapılabilir. Sıkça kendini gösteren bu ayrıksı durumun da sebebi programlama hatasıdır ve kotarılması olanaksızdır. Dolayısıyla, yakalanması veya metot başlığında ilan edilmesi pek anlamlı olmayacaktır.3 Buna karşılık, bir dosyanın olmaması kullanıcının dosya adını giren kullanıcının dikkatsizliğinden kaynaklanabilir. Doğal olarak, bu beklenmedik durumun öngörülerek kotarıcıda ikinci bir hak verilerek programın boş yere sona ermesinin önüne geçilmelidir.

Peki, ya sizin ve kullanıcının iradesi dışında gelişen sebepler nedeniyle programınız göçecek olursa. Mesela, arkadaşınız ortaklaşa kullandığınız bilgisayardan programınızın çalışması için gerekli olan bir sınıf dosyasını silecek olursa, ne olur? Ya da, dosyanın diskte kaydedildiği sektörlerden birinin bozulması nedeniyle sınıf dosyasının okunarak içselleştirilmesi söz konusu olamazsa, ne olur? Ya da ya da, programınızın işlemesi için gereksinilen kaynaklar JSM tarafından sağlanamayacak kadar büyük olmaya başlarsa, ne olur? Gelin bu soruların yanıtını Faktöryel sınıfını ikinci komut satırı argümanı çok büyük bir sayı ile deneyerek birlikte görelim.
$ java Faktöryel 5 5827 2> StackOverflowError.txt
5!: 120 ✔       
Standart hata ortamını yönlendirerek hata mesajlarını StackOverflowError.txt dosyasına gönderen yukarıdaki komutun ürettiği bilgi incelendiğinde, fakt2'nin sürekli kendini çağırdığı ve programın StackOverflowError ile sonlandığı görülecektir. Yani, JSM özyinelemeli çağrılar sonucu genişleyen çağrı yığıtını doldurmuş ve taşmanın vuku bulduğu noktada imdat çağrısını yapmıştır. Bu hatanın ortaya çıkışı, JSM'nin yararlandığı yığıt büyüklüğünü -Xss512k veya -Xss1m gibi bir opsiyonla büyüterek ertelenebilir. Ancak; işin özünde çaresiz olan JSM'dir ve belleğin bittiği yerde o da bir şey yapamayacaktır. Bir diğer deyişle, beklenmedik durum dış kaynaklıdır ve programcının yapacağı fazla bir şey yoktur. Örneğimizde, özyinelemeli çözümü for döngülüsüyle değiştirerek belki bir şeyler yapabiliriz ama bu her zaman mümkün olmayacaktır. İş böyle olunca, kotarımdan söz etmek de mantıksızdır.

Bu ana kadarki bilgilerimizi maddeleyerek özetleyecek olursak aşağıdaki listeyi elde ederiz. Java programları, iç ve dış koşullara göre beklenenden farklı bir şekilde davranabilir. Davranış sapması programcının müdahele edemeyeceği dış kaynaklardan ötürüyse, programcı Error köklü sıradüzenindeki sınıflardan birisinin nesnesi ile haberdar edilir. Yok eğer oluşan durum içselse ve programcı tarafından çözüm bulunabilecek gibi gözüküyorsa, programcının ihtiyacının duyduğu bilgi RuntimeException köklü sıradüzenindeki sınıflardan birisinin nesnesi ile sağlanır. Aksi takdirde, programcının yaptığı mantıksal bir hata dönüşerek ayrıksı durum haline gelmiştir ve bu, programcının dersini tamamlaması için, Exception köklü sıradüzenindeki diğer sınıfların nesneleri ile haber verilmelidir.

  • Throwable: Hata ve ayrıksı durumlar
    • Error: JSM'nin göçmesine neden olan hatalar
    • Exception: Ayrıksı durumlar
      • RuntimeException ve altsınıfları: Kotarılması zorunlu olmayanlar
      • Kotarılması zorunlu olanlar

Hangi kategoriye giriyor olursa olsun, programcı tüm koşullar altında elinden gelenin en iyisini sergilemelidir. Mümkünse, kotarıcıdaki kod ile programın sağlıklı bir şekilde devamı sağlanmalı, bu mümkün olmuyorsa, nasıl olsa bir şey yapamıyorum, denilerek oturmaktansa programın zarif bir şekilde bitmesi için elden gelen yapılmalıdır. Mesela, kullanılmakta olan dış kaynaklar kotarıcıda döndürülmeli, programın bakıcısının muhtemel bir programlama hatasını bulmasını kolaylaştırmak adına bir seyir defterine neden ile ilgili bilgiler not düşülmelidir. Bu noktada karşımıza, ayrıksı durum oluşsa da oluşmasa da çalıştırılacak kodu içeren bir kotarıcı olarak finally çıkıyor. Bir örnek ile görelim.
public void m() {
  ...
  try {
    FileReader dosya = new FileReader(...);
    ... 
  } catch (java.io.IOException a) { ... }
    catch (ADurumu a) { return; }
    ...
    catch (Exception a) { ... }
    finally { dosya.close(); ... }
  ...
} // void m() sonu
Birden çok kotarıcıya sahip yukarıdaki kod parçasında, try bloğunun işlenmesi sırasında oluşacak duruma göre farklı kotarıcılar devreye girebilir ya da işler yolunda giderse denetim akışı finally sonrasındaki komutla devam eder. Ancak, ne olursa olsun, finally kotarıcısı herhalükâda işlenecektir. Bu, başka hiçbir şey yapmadan çağırıcıya dönen ADurumu ayrıksı durumu için de geçerlidir; çünkü metottan dönüş öncesinde işlenmemiş finally kotarıcıları sırayla işlenir. Dolayısıyla, ortaya çıkan ayrıksı durum nedeniyle işlenen kotarıcıda metottan dönülse bile kapatılamamış olan dosya muhakkak kapatılacaktır.

Kod parçasında dikkat çekilmesi gereken bir diğer nokta, kotarıcıların yerleştiriliş sırasının önemli olduğudur. Bir kotarıcı sadece argümanındaki sınıfa ait nesneleri değil, o sınıfın kökü olduğu sıradüzeni içindeki tüm sınıfların nesnelerini yakalar. Örneğin, yukarıdaki ilk kotarıcı IOException nesnelerini yakaladığı gibi, FileNotFoundException'ın da içinde bulunduğu pek çok sayıda sınıfın nesnelerini de yakalayacaktır. Benzer şekilde, Exception argümanlı kotarıcı, kendisinden önce geçen kotarıcılarda yakalanmayan tüm ayrıksı durum nesnelerini yakalayarak amansız—bir o kadar da anlamsız—bir gümrük memuru görevini görecektir. Böylesine bir kotarıcının başa konulması—genelde, bir üstsınıf türlü argümana sahip kotarıcının altsınıf türlü argümana sahip kotarıcıdan önce gelmesi—her şeyi yakaladığı için takip eden kotarıcıları anlamsız kılacaktır. Kotarıcıların, özelden genele olacak şekilde sıralanması gerekir.

Evet, geldik kendi ayrıksı durumunu kendin pişirmeye. Ama, yazımızın vardığı uzunluğu ve muhtemel konsantrasyon azalmasını düşünerek bunu bir başka yazıya bırakalım. Onun için, şimdilik bu kadar. Unutmayın, hatasız program(cı) olmaz; birazcık defansif programlama ortaya çıkabilecek zararın faturasını azaltır. Onun için sadece doğruluk değil, dayanıklılığa da önem verin. Yani, güvenilir yazılım üretin.


  1. Yazılımın benzetimi yapılan sürecin geliştiriciler (alan uzmanı, gereksinim toplayıcı, mimar, tasarımcı ve gerçekleştirimci) tarafından yorumlanması ile ortaya çıktığı unutulumamalıdır. Dolayısıyla, doğruluk özelliğini sürecin algılanışına göre yapmak daha doğru olur.
  2. Standart hata ortamı, bir programın işleyişi sırasında üretilen hata mesajlarının gönderildiği aygıttır ve yeniden yönlendirme yapılmadığı veya System.setErr metodu kullanılarak değiştirilmediği müddetçe standart çıktının da gönderildiği varsayılan aygıt olan ekrandır.
  3. Yapılan ayrımın bir diğer olumlu yanı, programların fazladan try-catch yapıları ve throws bildirimleri ile doldurulmamasıdır. Örnek olarak verdiğimiz iki ayrıksı durum düşünüldüğünde—tüm dizi erişimi ve ileti gönderimlerinin try-catch yapısı içine alındığını veya bu işlemleri içeren metotların imzasına ayrıksı durum ilanının eklendiğini düşünün—kotarımın zorunlu olmamasının kaynak kodun okunabilirliğine yaptığı katkı takdir edilecektir.

14 Ekim 2011 Cuma

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


Scala üzerine ikinci yazımızda bu JSM dilinin sağladığı sınıf tanımlama imkânına daha yakından bakacağız ve bunu yaparken de Java ile olan farklılıklara değinmeye çalışacağız.1 Matematikteki kesirli sayı kavramını soyutlayan aşağıdaki sınıf iskeletiyle başlayalım.2

Kesir.scala
package com.ta.matematik
...
class Kesir(pay: Long = 1, payda: Long = 1) {
  require(payda != 0)
  private var (_pay, _payda) = (pay, payda)
  sadeleştir()
  ...
  private def sadeleştir() = { ... }
} // Kesir sınıfının sonu
Java'dan gelenlerin gözüne çarpacak farklılıklardan belki de ilki, public niteleyicisinin sırra kadem basmış olması. Bu durumun sebebi, paket çapında erişimin varsayıldığı Java'dan farklı olarak, Scala'da programlama öğelerinin nitelenmemeleri halinde public erişime sahip kılınmasıdır. Dolayısıyla, Kesir ve erişim niteleyicisi bulunmayan tüm sınıf öğeleri herkes tarafından kullanılabilecektir. Bunun uygun olmaması halinde, programcının niyetini protected veya private niteleyicilerinden birini kullanarak bildirmesi gerekir. Bu noktada, protected niteleyicisinin paket içindeki türleri kapsamayıp, söz konusu öğeyi sadece o anki türden doğrudan veya dolaylı bir biçimde kalıtlayan türlere erişilebilir kıldığını söylemekte yarar vardır.3 Ayrıca, paket çapında erişimden de bahsedilmediği dikkatinizi çekmiştir. Ancak, telaşa düşmeyin; nitelenmekte olan öğeyi içeren paket, sınıf veya tek-örnekli sınıf adlarından birinin protected veya private ile birlikte belirtilerek daha rafine erişim politikalarının ifade edilmesi mümkündür. Örneğin, private yerine private[com.ta.matematik] nitelemesinin yapılması, sadeleştir adlı metodu sınıfa özel olmaktan çıkaracak ve com.ta.matematik paketindeki tüm öğelere görünür kılacaktır.

Java'dan gelenleri başlarda şaşırtabilecek bir diğer nokta, nesnenin yaratılması esnasında yapıcıya geçirilmesi beklenen argümanların özelliklerini belirten parametre listesinin sınıf adı sonrasında bulunmasıdır. Buna göre, Kesir sınıfının örneği ks1 = new Kesir(6, 10) şeklinde yaratılabilecektir. İlkleme ise Kesir sınıfının tanımındaki metotların dışında kalan tüm öğelerin koddaki geçiş sırasında işlenmesiyle yapılacaktır. Yani, sınıf gövdesindeki söz konusu öğeler nesne ilkleme bloğu görevini görecektir; Scala terminolojisiyle konuşacak olursak, sınıf gövdesindeki öğeler birincil yapıcı olarak ele alınacaktır. Örneğimiz üzerinden gidecek olursak; Predef adlı tek örnekli sınıftan ithal edilen require metodu ile geçirilen paydanın uygunluk denetiminin yapılmasını takiben, yaratılmakta olan nesnenin altalanları olan _pay ve _payda koşut bir biçimde pay ve payda ile ilklendikten sonra yaratılmakta olan kesir sadeleştirilecektir. Bu noktada, koşut atamanın tüm kollarının aynı anda işleniyormuş gibi düşünülmesi gerektiği unutulmamalıdır.

Haklı olarak, Predef sınıfının nereden çıktığını sorabilirsiniz. Yanıtı, Scala derleyicisinin, Java derleyicisinin java.lang paketini otomatikman ithal edilmiş kabul etmesindeki gibi, java.lang ve scala paketleriyle scala.Predef tek örneklisini otomatikman tüm programlara ithal etmesinde yatar. Bunun sonucunda, Predef tanımı da evrensel olarak görünür hale gelecek ve bazı işlemler öncesinde önkoşul denetimine yarayan require metodu da yukarıdaki gibi kullanılabilecektir.

Bir diğer farklılık, Java'da aynı adlı metotların ezilmesi ile öykünülen varsayılan argümanların kullanımı. Scala 2.8'den başlayarak geçerli olan bu özellik sayesinde, varsayılan değerin uygun olması durumunda, argümanın es geçilmesi de olanaklıdır. Örneğin, yukarıdaki sınıf tanımının geçerli olduğu bir ortamda, tamsayı = new Kesir(7) ve bir = new Kesir(), sırasıyla, 7/1 ve 1/1 değerlerini temsil eden kesirli sayıları yaratacaktır. Bu noktada, varsayılan argümanların metot imzasının sonunda yer alması zorunluluğu unutulmamalıdır. Dolayısıyla, 1/7 kesrini temsil eden nesnenin yaratılması için her iki argümanın da geçirilmesi gerekecektir.

Scala 2.8 ile birlikte, ileti gönderileri ve metot çağrıları sırasında argümanların parametre adları kullanılarak konumlarından farklı bir sırada geçirilmesi de olanaklı kılınmıştır. Buna göre, önceki paragraflardaki ks ve tamsayı tanımlayıcılarının, sırasıyla, new Kesir(payda = 5, pay= 3) ve new Kesir(pay= 7) şeklinde tanımlanması mümkün olacaktır. Bu sayede, birBölüYedi = new Kesir(payda = 7) tanımlamasının da geçerli olması sağlanarak 1/7 kesrine karşılık gelen nesnenin tek argüman geçirilerek yaratılması da mümkün olmaktadır.

Yukarıdaki örnekte varsayılan argümanlar yardımıyla sağlanan değişik sayıda argümanlarla kullanılabilen yapıcı metotlar görüntüsü, yardımcı yapıcı metotlar yoluyla da sağlanabilir. Varsayılan argümanlarla birlikte de yararlanılabilecek bu yaklaşımda, programcı ilk iş olarak uygun argümanlarla birincil yapıcıyı veya diğer yardımcı yapıcılardan birini çağıran this adına sahip metotlar yazar. Örneğin, aşağıdaki kod parçasında iki argümanın da sağlanması durumunda birincil yapıcı çağrılırken, bir veya sıfır argümanın sağlanması halinde, sırasıyla, 7. ve 8. satırlardaki yardımcı yapıcılar çağrılacaktır. Dikkat ederseniz, her iki yapıcı da işini bir diğer yapıcıya havale ederek görmekte.
...
class Kesir(private var _pay: Long,
            private var _payda: Long) {
  require(_payda != 0)
  sadeleştir()
  ...
  def this(_pay: Long) = this(_pay, 1)
  def this() = this(1)

  def pay = _pay
  def payda = _payda
  ...
  override def toString() = _pay + "/" + _payda
  ...
} // Kesir sınıfının sonu
Birincil yapıcının parametre listesindeki değişiklik de gözünüze çarpmıştır, Tanımının önüne var niteleyicisinin konulmasıyla ilişkin parametrenin değişken içeriğe sahip kılınması nedeniyle, daha önceki kod parçasında olduğu gibi yapıcıya geçirilen argümanların değişken içerikli altalanları ilklemesi ve değişikliklerin bu altalanlarda yapılması artık gerekli değildir. Ancak, birincil yapıcıya özel bu durum, Scala'nın atıf geçirme (İng., pass by reference) yöntemini desteklediği şeklinde yorumlanmamalıdır; Java'da olduğu gibi, Scala'da da argümanlar ilişkin metoda değer geçirme (İng., pass by value) yöntemiyle sağlanır.

Java'dan tanıdık gelecek bir nokta, üstsınıflardan kalıtlanan bir metodun ezilmekte olduğunu bildiren override niteleyicisidir. Ancak, Java dengi @Override açımlamasının kullanımı seçimli olup sadece tavsiye edilirken, Scala'daki bu niteleyicinin kullanımı aynı imzaya sahip metotların varlığında zorunludur. Bu kurala uyulmaması, derleyicinin hata mesajıyla karşılanacaktır.

toString'i bir yerlerden gözünüzün ısırdığını düşünüyorsanız, belleğinizin sizi aldatmadığını söyleyebilirim; Java'dan bildiğimiz bu ileti, Scala'da da hoş yazım amacıyla kullanılıyor. Tıpkı Java'da olduğu gibi, programcıların belli bir türe ait değerlerin hoş yazımı için kök sınıf tarafından sağlanan ilişkin metot gerçekleştirimini ezmesi gerekiyor. Ancak, toString'in Scala tür sıradüzeni içinde nereden nasıl kalıtlandığını daha iyi anlamak için, Java'da olmayan bir üstkavramın tanıtılmasında yarar var: çeşni (İng., mixin). Kaynak kodda trait anahtar sözcüğüyle karşılık bulan bu programlama kavramının anlaşılması, Java ve Scala türleri arasındaki etkileşimi daha iyi kavramak için de yardımcı olacaktır. O zaman, karşılaştırılabilirlik kategorisini tanımlayan scala.math.Ordered çeşnisinin Scala'nın resmi sitesindeki gerçekleştirimine ve Kesir sınıfına söz konusu çeşninin nasıl katıldığına göz atarak bu üstkavramı anlamaya çalışalım.

Soysal olan Ordered çeşnisi, başlığındaki kalıtlama ilişkisinden de görülebileceği gibi, Java platformundaki karşılaştırılabilir nesnelerin kategorisini tanımlayan Comparable arayüzünden kalıtlar. Scala türlerinin Java'dakileri geliştirebileceğine örnek oluşturan bu başlığın anlamı, işini compare metoduna havale ederek gören compareTo metodu gerçekleştiriminden de gözlemlenebilir. Böylece, Scala'da yazılan bir sınıfın nesneleri Java programları içinden de kullanılabilecektir.

Ordered.scala
package scala.math

trait Ordered[A] extends java.lang.Comparable[A] {
  def compare(sağ: A): Int
  def <(sağ: A): Boolean = (this compare sağ) <  0
  def >(sağ: A): Boolean = (this compare sağ) >  0
  def <=(sağ: A): Boolean = (this compare sağ) <= 0
  def >=(sağ: A): Boolean = (this compare sağ) >= 0
  def compareTo(sağ: A): Int = compare(sağ)
} // Ordered[A] çeşnisinin sonu

object Ordered { 
  implicit def orderingToOrdered[T](x: T)
    (implicit ord: Ordering[T]): Ordered[T] = 
    new Ordered[T] {
      def compare(sağ: T): Int = ord.compare(x, sağ)
    }
} // Ordered tek-örneklisinin sonu
Ordered çeşnisinin gövdesine baktığımızda, tanımlanan kategorideki iki Scala nesnesinin—ileti alıcı (this) ve sağ—karşılaştırılma sonucunu döndüren compare metodunun gövdesi verilmeden sağlandığını görüyoruz. Bu durum, Scala derleyicisi tarafından söz konusu metodun soyut olarak ele alınacağı anlamını taşır; derleyicinin yaptığı bu varsayım yüzünden programcının ayrıca çeşniyi veya metodu soyut olarak nitelemesine gerek yoktur.

Yukarıdaki kod parçasının ortaya koyduğu bir diğer önemli nokta, Scala'nın, Java'nın aksine, işleçlerin aşırı yüklenmesini—ya da, işin doğrusunu söylemek gerekirse, bu tür bir yanılsamayı yaratabilecek özellikleri—desteklediğidir. Öncelikle, tanımlayıcı adlarının oluşturulmasında kullanılan karakterler kullanageldiğimiz işleçlerin simgelerini de kapsayan daha geniş bir yelpazeden seçilebilir.4 Ayrıca, tek argüman alan iletiler işleç ortada sözdizimiyle de kullanılabilir. Örneğin +, topla veya add kadar geçerli bir tanımlayıcı adıdır. İsteyecek olursak, değişken/sabit veya ileti/metot adlarını topla veya add yerine + olarak da seçebiliriz. Aynı zamanda, adı ne şekilde verilmiş olursa olsun, tek argüman alan iletileri, alıcı.ileti(arg) yerine alıcı ileti arg şeklinde de gönderebiliriz. Dolayısıyla, yukarıdaki this compare sağ ifadesi this.compare(sağ) ile eşdeğerdir.

Kod parçamızda dikkat çeken bir diğer bölüm, tek-örneklimizin tanımında geçen implicit anahtar sözcüğüdür. Bu sözcük, söz konusu metodun, programcının açıkça kullanması dışında kimi zaman arka planda derleyicinin sentezlediği kod tarafından da çağrılabileceğini belirtir. Örneğimizde olduğu gibi dönüşüm amacıyla kullanılan bu tür metotlar, nesne tutacağını argüman türünden (Ordering) eşlik eden türe (Ordered) çevirir. Mesela, Ordering çeşnisi tutacağıyla bir nesneye ileti gönderilmesi ve bu iletinin geçerli olmadığının anlaşılması durumunda, derleyici yukarıdaki ve benzeri metotlardan birini usulca kullanarak nesneyi başka bir açıdan görecek ve program hata vermeden devam edecektir.

Tanımlanmış bir çeşni, bir diğer çeşni tarafından kalıtlanmak suretiyle geliştirilebileceği gibi, içerdiği soyut öğelerin sağlanması ve/veya bazı öğelerinin ezilmesi yoluyla bir sınıfa katılabilir. Her iki durum da extends seçilmiş sözcüğü ile ifade edilir. Ancak; sınıfın bir başka sınıftan kalıtlaması halinde, extends üstsınıfı belirtmek için kullanılırken, sınıfa katılan çeşniler with anahtar sözcüğü ile belirtilir. Çeşniler arası kalıtlama ilişkisinin çoklu olmasının yanısıra, bir sınıf birden çok çeşniyi gerçekleştirebilir.

Ordered çeşnisinin Kesir sınıfı tarafından gerçekleştirilmesi aşağıda verilmiştir. Bu tanıma göre, yaratılacak Kesir ve Kesir'den kalıtlayan tüm sınıfların nesneleri karşılaştırılabilir nesneler kategorisine gireceklerdir. Bu özellik, Ordered çeşnisinin Comparable'dan kalıtlaması nedeniyle, söz konusu nesnelerin sadece Scala programlarında kullanılmaları halinde değil, Java ve diğer Java platformu dillerinde yazılmış programlar içinden kullanılmalarında da geçerli olacaktır. Mesela, Kesir nesneleri ile doldurulmuş bir liste java.util paketindeki Collections.sort metodu ile sıralanabildiği gibi, scala.collection.immutable.List sınıfının sort metoduyla da sıralanabilir.
...
import scala.math

class Kesir extends Ordered[Kesir] {
  ...
  def equals(sağ: Kesir) = compare(sağ) == 0
  
  def compare(sağ:Kesir) = {
    val fark = this - sağ
    if (fark._pay < 0) -1
    else if (fark._pay > 0) 1
      else 0
  } // compare(Kesir): Int sonu
  ...
  def -(sağ: Kesir): Kesir = {
    val pay = _pay * sağ._payda - _payda * sağ._pay

    new Kesir(pay, _payda * sağ._payda).sadeleştir()
  } // -(Kesir): Kesir sonu
  ...
} // Kesir sınıfının sonu
Kesir sınıfındaki equals metodunun Ordered çeşnisindeki compare ile uyumlu olacak şekilde ezilerek gerçekleştirildiği gözünüze çarpmıştır. Sakın ola ki, Java'yı iyi bilen biri olarak, bunun eşitlik denetimi işlecini (==) etkilemeyeceğini düşünmeyin. Çünkü, Scala'da equals ile == her zaman aynı şekilde çalışır: kök sınıftaki equals gerçekleştiriminin bir sınıf tarafından ezilmesi == işlecinin de anlamını değiştirir. Eşitlik denetimine ek olarak aynılık denetimi isteyenlerin, eq ve onun değillemesi olan ne iletilerini kullanması tavsiye edilir.

Bir sınıfa çeşni katılması nesnenin yaratıldığı noktada, dinamik olarak da gerçekleştirilebilir. Örnek olarak, KesirEksik sınıfının Ordered çeşnisi katılmadan tanımlanmış olduğunu varsayalım. Bu takdirde, aşağıdaki kod parçasından da görebileceğiniz gibi, bu sınıfa ait [1/11 değerine sahip] bir nesne Java'daki adsız sınıflara benzer bir tanımla söz konusu çeşniye sahip kılınabilir.
val ks =
  new KesirEksik(3, 33) with Ordered[KesirEksik] {
    def compare(sağ: KesirEksik) = {
      val fark = this - sağ
      if (fark.pay < 0) -1
      else if (fark.pay > 0) 1
        else 0
    } // compare(KesirEksik): Int sonu
  
    def equals(sağ: KesirEksik) = compare(sağ) == 0
  } // Kesir'i geliştiren adsız sınıfın sonu
toString'in soy ağacını öğrenmek için yola çıkmıştık, şimdi equals ve arkadaşlarının da katılımı ile iş daha da karıştı, değil mi? Üzülmeyin, aşağıdaki kısmi tür sıradüzeninin açıklanması her şeyi yoluna koyacaktır. [Yatık yazılı türler çeşnileri, diğerleri ise sınıfları temsil ediyor.]

  • Any
    • AnyRef ≡ java.lang.Object
      • ... // Java'dan ithal ediilen bileşke türler
      • ScalaObject
        • ... // Scala'da tanımlanan bileşke türler
    • AnyVal
      • Boolean + scala.runtime.RichBoolean
      • Byte + scala.runtime.RichByte
      • ...
      • Unit

Java'daki bileşke tür ve ilkel tür ayrımı Scala'da bire bir karşılık bulmaz; ilkel türlerin ele alınışını C# diline benzeterek anlamak daha kolay olacaktır. Çünkü, arka planda işlemcinin desteklediği türlerden birine eşleştirilerek işlenen bu çeşit değerler, kaynak kod düzeyinde kalıtlanarak geliştirilemeyen—yani final—özel sınıfların nesneleri gibi ele alınır. Dolayısıyla, ilkel türden değerler de ileti alıcı konumunda kullanılabilir. Bunu akılda tutarak yukarıda verilen sıradüzenini açalım. Öncelikle, ister ilkel olsun isterse bileşke, tüm türlerin kökü ==, !=, equals, toString ve diğer temel iletileri içeren Any sınıfına gider. Scala nesnesi olduğunun anlaşılması için ScalaObject adlı bir gösterge çeşninin katıldığı bileşke türlü değerler, java.lang.Object'tekilere ek olarak eq ve ne gibi Scala nesnelerine özel iletiler içeren AnyRef sınıfında belirlenen sözleşmeye göre davranırlar. İlkel türlü değerler ise, ilişkin sınıfları işaretlemek için kullanılan AnyVal çeşnisi katılmış sınıflara aittir. Buna ek olarak, Predef tek-örneklisinde yapılan dönüşümler sayesinde, tüm ilkel tür değerler daha zengin bir arayüze sahipmiş gibi görünebilirler. Örnek olarak, 1'den 10'a tüm tamsayıları standart çıktıya yazan for (i <- 1 to 10) System.out.println(i) komutunun perde arkasına bir göz atalım. Unuttuysanız hatırlatalım, tek argümanlı ileti gönderileri işleç ortada sözdizimiyle de yazılabilir. Dolayısıyla, ileti alıcıdan başlayarak argümanındaki değere kadar olan tamsayıları içeren bir dilim döndüren to iletisini dönüştürerek döngümüzü for (i <- 1.to(10)) System.out.println(i) şeklinde yazmak da aynı işi görecektir. Yani, Int türlü 1'e to iletisi gönderilecek ve döndürülen dilim nesnesi gezilerek döngü işlenecektir. Ne var ki, Int sınıfının arayüzüne bakıldığında, to adında bir iletinin olmadığı görülecektir. O zaman, nasıl oluyor da Scala derleyicisi böyle bir durumda itiraz etmeden yukarıda anlatılan şeyi yapıyor? Bu sorunun yanıtı, Predef tek-örneklisinde sağlanan dönüştürücü metodun örtük çağrısında (İng., implicit call) yatıyor: derleyici, iletinin desteklenmediğini görmesinin ardından Int türlü hedef nesneyi scala.runtime.RichInt nesnesine çeviriyor ve iletiyi bu nesneye gönderiyor. Bir diğer deyişle, Int sınıfındaki işlevsellik RichInt sınıfında sağlanan işlevsellikle zenginleştiriliyor.

Bu haliyle çeşnilerin soyut sınıflardan çok da farklı olmadığını düşünebilirsiniz: tıpkı soyut sınıflarda olduğu gibi, çeşniler altalan tanımlarının yanısıra iletiler ve bu iletilerin bazıları veya tümü için gerçekleştirimler içerebilir. Buna karşılık, çeşniler yardımcı yapıcılara sahip olamaz; ek olarak, birincil yapıcılar argüman alamadığı gibi kalıtladıkları türlerin yapıcılarına argüman geçiremez. Ayrıca, (soyut) sınıflar için tekli kalıtlama geçerliyken çeşniler birden çok çeşniden kalıtlayabilir. Bundan dolayı bazılarınız, önceki paragraflardaki kategori sözcüğünün kullanımının da katkısıyla, çeşnilerin biraz da arayüzlere benzediğini düşünebilir. İki grubun da bir yere kadar haklı olduğu söylenebilir. Kimin haklı olduğunu ilan etmeden önce, Scala derleyicisinin çoklu sınıf kalıtlamanın desteklenmediği JSM üzerinde nasıl olur da soyut sınıf özellikleri sergileyen çeşnilerin çoklu kalıtımını olduruyor, ona bir bakalım. Bunun için sınıf dosyalarını tanımlanan türün üyelerini listeleyerek tersine bütünleştiren javap komutunu ve Scala derleyicisinin bir opsiyonunu kullanacağız.

Çeşni.scala
trait Çeşni { def ileti() { println("iletide ...") } }
$ scalac Çeşni.scala
$ ls Çeşni*.class
Çeşni.class Çeşni$class.class
$ javap Çeşni
Compiled from Çeşni.scala
public interface Çeşni extends scala.ScalaObject {
  public abstract void ileti();
}
Çeşni.class dosyasının incelenme sonucu, tercihini arayüzden yana kullananları haklı çıkarmış gibi gözüküyor; javap komutunun çıktısına göre, tanımlamakta olduğumuz iletinin imzası arayüze aynen taşınmış. Ancak, doğal olarak, arayüzlerin gerçekleştirim ayrıntısı içerememesi nedeniyle, metot gövdesi uçup gidivermiş. Bu sihirbazlığa açıklık getirilmesi gerekli. İşte bu noktada, Scala derleyicisine geçirilecek print opsiyonu yardım çağrımıza yanıt verecektir.
$ scalac -print Çeşni.scala
[[syntax trees at end of cleanup]]// Scala source: Çeşni.scala
package <empty> {
  abstract trait Çeşni extends java.lang.Object with ScalaObject {
    def ileti(): Unit
  };
  abstract trait Çeşni$class extends  {
    def ileti($this: Çeşni): Unit = scala.this.Predef.println("ileti içinde ...");
    def /*Çeşni$class*/$init$($this: Çeşni): Unit = {
      ()
    }
  }
}
Görünen o ki, metot gövdesi yukarıda yaptığımız sorgu sonrasında listelenen ikinci dosyaya, Çeşni$class.class, taşınmış. Yani, ortada kaybolup giden bir şey yok. Ama,çeşnimizin dönüşümünü Scala üstkavramları cinsinden veren bu çıktıda açıklama getirilmesi gereken bir nokta var: Çeşni$class çeşnisi, Java'ya nasıl çevrilecek? Çok bekletmeden yanıtını verelim: soyut sınıf olarak. Her şeyi bir arada gösteren aşağıdaki kod üzerinden anlamaya çalışalım.
trait Çeşni { def ileti() = println("ileti içinde...") }
⇓ Scala → Java
public interface Çeşni { public abstract void ileti(); }
+
public abstract class Çeşni$class {
  static void ileti1(Çeşni $this) = { ... }
  ...
}
class ÇeşniciBaşı extends Çeşni { ... }
⇓ Scala → Java
public class ÇeşniciBaşı implements Çeşni {
  ...
  void ileti() { Çeşni$class.ileti(this) }
  ...
}
Özetleyecek olursak; çeşni tanımının sözleşmesi bir arayüze, gerçekleştirim ayrıntıları ise bir soyut sınıfa konulur; çeşninin katıldığı sınıfta ise çeşninin arayüzündeki iletilere karşılık gelen metotlar, işlerini soyut sınıftaki gerçekleştirime havale ederek görürler.

Dikkatinizi çekeceğimiz son nokta, çeşni katılmış bir sınıfın nesnesine gönderilen iletilerin işlemesi sırasında super anahtar sözcüğünün anlamını ilgilendiriyor. Tekli sınıf kalıtlamanın geçerli olduğu ve arayüzlerin gerçekleştirim ayrıntısı içermediği Java'da üstsınıftaki yapıcı veya diğer metotlara atıfta bulunmak için kullanılan bu sözcük, çoklu çeşni kalıtlama, bir sınıfa birden çok çeşni katılabilmesi ve çeşnilerin gerçekleştirim ayrıntısı içermesi nedeniyle Scala'da anlaşılması daha zor bir anlama sahiptir. Programming in Scala kitabındaki örnek ile anlamaya çalışalım.
class Hayvan
trait Tüylü extends Hayvan
trait Bacaklı extends Hayvan
trait DörtBacaklı extends Bacaklı
class Kedi extends Hayvan with Tüylü with DörtBacaklı
super çağrılarının etkisini anlamak için, Scala'nın sıradüzenindeki türleri nasıl sıraya dizdiğini anlamak gerekir. Doğrusallaştırma adı verilen bu işlemin üç temel kuralı vardır:
  1. Bir sınıf üstsınıfları ve katılan çeşnilerinin öncesinde doğrusallaştırılır.
  2. Doğrusallaştırılmada daha önceden geçmiş bir tür bir daha sıraya konmaz.
  3. Bir sınıfın üstsınıfı ve birden çok çeşnisi olması durumunda, ilk olarak en son katılan çeşni doğrusallaştırılır.
Anlamanızı kolaylaştırmak için yalın bir tanımı olan Hayvan sınıfından başlayalım. Birinci madde gereği, doğrusallaştırma sonucu türler Hayvan, AnyRef, Any şeklinde sıralanacaktır. Yani, Hayvan sınıfının içinde geçen bir super çağrısı, AnyRef sınıfı içindeki ilişkin metoda atıfta bulunacaktır. Hayvan'ı geliştiren Tüylü çeşnisinin doğrusallaştırılması ise, kendisi ve Hayvan'ın doğrusallaştırılması ile elde edilen Tüylü, Hayvan, AnyRef, Any sırasını verecektir. Bacaklı ve DörtBacaklı için de benzer bir şekilde oluşturulan sıralama, sırasıyla, Bacaklı, Hayvan, AnyRef, Any ve DörtBacaklı, Bacaklı, Hayvan, AnyRef, Any olarak bulunacaktır. Hayvan sınıfına doğrudan ve Tüylü ve DörtBacaklı çeşnileri üzerinden olmak üzere üç değişik yoldan ulaşan Kedi sınıfı, ikinci ve üçüncü maddeler akılda bulundurularak doğrusallaştırılmalıdır. Yani, istediğimiz sıra Kedi sınıfının DörtBacaklı, Tüylü ve Hayvan'ın doğrusallaştırılması ile birleştirilmesi sonucunda bulunacaktır. DörtBacaklı'nın doğrusallaştırılması ile elde edilen sıranın öteki türlerin sıralamasındaki tüm türleri içermesi nedeniyle, sonuç Kedi, DörtBacaklı, Bacaklı, Tüylü, Hayvan, AnyRef, Any olarak ortaya çıkacaktır. Bu sıra, zincirleme super çağrılarının bulunduğu bir kodda bize denetim akışının izleyeceği yolu verir.


  1. Değinilecek farklılıklardan çoğunun kaynak kodu kısaltıp okumayı kolaylaştırdığına ve alana özel diller geliştirirken gerekli olacak akıcı arayüz yazımını olanaklı kıldığına dikkatinizi çekerim.
  2. Yazıyı okurken Scala'nın kimi özelliklerini gösterebilmek adına, kodumuzu sınıf yazma reçetesinin anlatıldığı yazıda🔎 tavsiye edilenden farklı bir biçimde oluşturduğumuzu aklınızdan çıkarmayınız.
  3. Hatırlayacak olursanız, protected niteleyicisi Java'da altsınıfların yanısıra, aynı paketteki türlere de erişim hakkı sağlar.
  4. Tanımlayıcı adının oluşturulmasında işleçler ve diğer karakterlerin birbirine karıştırılarak kullanılması mümkün değildir.