23 Şubat 2011 Çarşamba

Bir Java Programının Anatomisi

Önceki yazımda Java programlarının nasıl çalıştırıldığına değinmiştim. Bu yazımda ise, Java programlarını oluşturan dosyaların nasıl yapılandırılabileceğini anlatmaya çalışacağım. Bunu yaparken de aşağıdaki küçük programdan yararlanacağım.

BasitProgram.java
import java.util.Date;
import java.util.Scanner;
import static java.lang.System.*;

public class BasitProgram {
  public static void main(String[] ksa) {
    out.println("Tarih: " + new Date());
    Scanner tarayici = new Scanner(in);
    double x = tarayici.nextDouble();
    out.println("e^" + x + " = " + Math.exp(x));

    return;
  } // void main(String[]) sonu
} // BasitProgram sınıfının sonu

C++ dilindeki küresel fonksiyonlara alışmış olanları uyararak ilk kuralımızı söyleyelim: Java'da her şey bir sınıf içinde bulunmak zorundadır. Dosya içinde tek bir sınıfın var olması durumunda bu sınıf ile sınıf tanımını içeren dosyanın aynı ada sahip olması gerekir.

Sınıf tanımı, veriler ve bu verileri işleyecek metotlardan oluşur. Sınıfı oluşturan tüm öğeler, tıpkı sınıfın kendisi gibi, bir erişim niteleyicisine sahiptir. Bu niteleyici, söz konusu öğenin Java platformundaki hangi sınıflar içinden kullanılabileceğini belirler. Örneğin, public olarak nitelenmiş BasitProgram sınıfındaki main metodu public—yani, kamuya açık—tanımlandığı için platformdaki tüm sınıflar tarafından kullanılabilecektir.

Çalıştırılabilir sınıfların—yani, JSM'ne geçirilerek doğrudan işletilen sınıfların—tanımında yer alan main metodu özel bir şekilde ele alınır. JSM kendisine geçirilen bir sınıfı belleğe yükledikten sonra main metodu içindeki ilk edimli satırı işleyerek programı çalıştırmaya başlar. Bir diğer deyişle, main metodu programların giriş noktasıdır.

main metodunun gövdesi dışındaki özellikleri değişmez ve şu şekilde yorumlanabilir.
  • public: main metodunun JSM tarafından çağrılabilmesi için public erişime sahip olması zorunludur.
  • static: static öğeler içinde bulundukları sınıfın nesnesi yaratılmadan  da kullanılabilir. Nesne yaratmanın programın çalışması sırasında olduğu ve programın main metodunun ilk edimli komutunun işlenmesi ile çalışmaya başladığı düşünülürse, main metodunun nesne yaratılması sonrasında çağrılmasının olanaksızlığı görülecektir.
  • void: main metodu çağırıcısı olan JSM'ne değer döndürmez. İşlerin yolunda gitmeyip bunun döndürülecek bir değer ile bildirilmesinin gerektiği durumlarda, System.exit metodunun kullanılması yerinde olacaktır.
  • String[] ksa: main metodu String'lerden oluşan bir dizi değer—0 veya daha fazla sayıda—bekler. main metodunun giriş noktası olduğu ve dolayısıyla değerleri içeren argümanın program içindeki bir diğer metottan geçirilmesinin söz konusu olmadığı düşünüldüğünde, argüman geçirmenin değer sağlamanın özel bir yöntemi olan komut satırı argümanları ile yapılması gerektiği görülecektir. [Aslında, main metodu program içindeki, kendisi de dahil, herhangi bir metottan çağrılabilir. Ancak, ilk çağrı her zaman program dışından olacaktır.]
İzin verildiği durumlarda, sınıf içindeki programlama öğeleri değişik şekillerde kullanılabilir. İlk yöntem, kullanılmak istenen öğeye dair tanımı içeren sınıfı programımıza görünür hale getirmek ve bunun ardından bu sınıfı kısa adından yararlanarak kullanmaktan geçer. Yukarıdaki programın ilk satırındaki import bildirimi ve ilişkin sınıfın 8. satırdaki kullanımı buna örnektir; ilk satırda programımıza görünür hale getirilen Scanner sınıfı, 8.satırda nesne yaratmak amacıyla kısa adıyla kullanılabilmaktedir. import bildiriminin bulunmaması durumunda, ilişkin sınıfların uzun adlarıyla kullanılmaları gerekir. Dolayısıyla, ilk satırdaki import bildiriminin iptal edilmesi sonrasında, 8.satırın aşağıdaki biçimde yazılması gerekecektir.

java.util.Scanner tarayici = new java.utility.Scanner(in);

Bu noktada, iki özel durumu vurgulamakta yarar var. Öncelikle, içinde bulunulan sınıfın öğelerinin kısa adlarıyla kullanılması için herhangi bir şeyin yapılması gerekmez. Ayrıca, java.lang paketinde bulunan sınıflar, kullanıcının import bildirimini kullanmasına gerek bırakmadan otomatikman ithal edilmiş varsayılır.

import bildiriminin diğer bir kullanımı, aynı paket içinde bulunan sınıfların tümünü görünür hale getirir. Örneğin, aynı paket içinde bulunan Date ve Scanner sınıflarını ayrı ayrı ithal etmektense, aşağıdaki jokerli import bildiriminden yararlanarak iki satırda yaratılan etkiyi tek bir satırda yaratmak mümkündür.

import java.util.*;

import bildiriminin programımızda da geçen son kullanımı, herhangi bir sınıf—paket değil!—içindeki static öğelerin sınıf adı olmaksızın kullanılmasını olanaklı kılar. Buna göre, System sınıfı içinde tanımlanmış ve değiştirilmedikçe ekran ile klavyeye bağlanmış olan standart çıktı ve girdi dosyalarını temsil eden kamuya açık static out ve in, System.out ve System.in yerine, sırasıyla, out ve in şeklinde kullanılabilmektedir.

Son olarak; bir programın nasıl sona ereceğine değinerek yazımızı bitirelim. Yaptığı iş gereği sonsuz bir döngü içinde göçene kadar çalışması beklenen programlar dışında tüm programlar eninde sonunda bitecektir. Bu üç şekilde olabilir.
  • return: Bu komut işlemekte olan metottan çağırıcısına dönülmesini sağlar. main metodundaki gibi bir dönüş değerinin beklenmediği durumlarda, metot gövdesinin son komutu sonrasında bir return komutu olduğu varsayılır. Buna göre, örneğimizin 12.satırındaki komut gereksizdir. Çalışması sonucunda değer döndüreceği ilan edilen metotlarda ise, return komutu mecburidir ve dönüş türüne tür uyumlu bir değer ile birlikte kullanılmalıdır.
  • System.exit: Bu komut hangi metot içinde olursa olsun, çağrı zincirindeki metotların tümünü atlayarak, işlemekte olan metottan JSM'ne döner. Dönüş değeri olarak System.exit'e geçirilen argümanın değeri kullanılır.
  • Ayrıksı durum🔎: Oluşması düşük olasılıklı bir durumun ortaya çıkması sonrasında, metottan durumu özetleyen bilgiyi içeren bir ayrıksı durum nesnesi ile de dönülebilir.

18 Şubat 2011 Cuma

İlk Java Programı

Adet olduğu üzre, benim de Java günlüğümün ilk yazısı Java programlarının çalıştırılma biçimiyle ilgili. Kaynak kodun makine koduna derlendikten sonra—olası bir bağlama aşamasının ardından—fiziksel makine üzerinde doğrudan işlenerek çalıştırıldığı C/C++, Pascal gibi derlemeli dillerin aksine, Java kaynak kodu derlemeli yorumlama (İng., interpretive compilation) ile çalıştırılır. Buna göre, Java dilinde yazılmış olan bir program önce Bytecode adındaki bir ara dilin komutlarını içeren ve formatı standardize edilmiş sınıf dosyasına çevrilerek sanal bir makine üzerinde yorumlanarak çalıştırılır. Her iki yöntemin de artıları ve eksileri vardır. Ancak, bu konuya girmeden önce derlemeli yorumlamanın nasıl işlediğini aşağıdaki minik örnek üzerinden görelim.

SelamMillet.java
public class SelamMillet {
  public static void main(String[] ksa) {
    System.out.println("Selam millet!");
  } // void main(String[]) sonu
} // SelamMillet sınıfının sonu

Aşağıda olduğu gibi derleyiciye kaynak dosya adının geçirilmesi ile icra edilen derleme aşaması, Java dili kurallarına uygun kodu class uzantılı Bytecode dili komutları içeren sınıf dosyası haline çevirir. [Kaynak kod içinde birden çok sınıfın tanımının yapılması durumunda, her sınıfa karşılık bir sınıf dosyası üretilecektir.]

# Derleme aşaması
$ javac SelamMillet.java
$ ls SelamMillet.*
SelamMillet.class   SelamMillet.java

Sınıf dosyasının oluşturulmasıyla birlikte Java Sanal Makinesi'ne (JSM) geçirilip çalıştırılmasının bir edimli kütüğün çalıştırılmasından aslında çok farkı yoktur. Her iki durumda da derleme sonucunda oluşturulan çalıştırılabilir dosya bir makine üzerinde yorumlanır. Ancak, edimli kütük için yorumlama bir fiziksel makine üzerinde yapılırken, sınıf dosyası için soyut bir makine üzerinde yapılır.

# Yorumlama aşaması
$ java SelamMillet
$ Selam millet!

Çalıştırılabilir sınıf dosyalarının soyut makine tarafından yorumlanması, dosya içeriğini oluşturan Bytecode komutlarının fiziksel makine komutları karşılıklarına dönüştürülmesini takiben bu makine komutlarının fiziksel makine üzerinde işlenmesiyle tamamlanır. Derlemeli dillerde programın çalışması öncesinde yapılan bu dönüşümün varlığı, Java programlarının daha yavaş çalışacağı anlamına gelir. Ancak, bu yavaşlama yorumlanan dilin (Bytecode) bir ara dil olması ve makine diline yüksek düzey dillerden daha yakın olması nedeniyle safkan yorumlamadaki kadar fazla olmayacaktır. Ayrıca, sanal makinenin yazılımda gerçekleştirilmesi ve Bytecode dili ile sınıf dosyası formatının standardize edilmiş olması, sınıf dosyalarının JSM'ne sahip tüm donanımlarda çalıştırılabileceği anlamını taşır. Yani, program çalışma hızındaki yavaşlamaya karşılık, nesne kodu düzeyinde taşınabilirlik elde edilmiştir.

Çalışma hızındaki yavaşlama, Java dilinde yazılmış programları doğrudan makine diline çeviren bir derleyici—mesela, gcj—ile aşılabilir. Platforma özel yazılımların üretiminde kabul edilebilirse de, Java'nın taşınılmazlığın vazgeçilmez olduğu İnternet ortamının dili olarak lanse edildiği düşünüldüğünde, çoğu kullanım senaryosu için taşınabilirliği olanaksız kılan bu seçenek uygulanabilir olmayacaktır. Dolayısıyla, taşınabilirlikten vazgeçmeyen bir çözümün tercih edilmesi daha yerinde olacaktır. Aranan çözüm, hemen hemen tüm JSM'lerinin desteklediği anında derlemeden geçer. Programcı/kullanıcı açısından  derlemeli yorumlamadan farkı bulunmayan bu çalıştırma yönteminde  JSM, Bytecode komutlarını yorumlarken bazı eniyilemeler yapar. Örneğin, ilk kez çağrılan bir metodun ilk çağrısı sırasında sonraki çağrıların daha hızlı olması için söz konusu metot veya metodun bulunduğu sınıf o noktada makine koduna çevrilerek bir önbelleğe yerleştirilir. Sonraki çağrılar, silinmediyse  önbellekteki derlenmiş kodu kullanacak ve programın çalışma hızı makine koduna derlenen programlarınkine benzer  bir düzeye çıkacaktır.

Özetlemek gerekirse; Bytecode komutları içeren sınıf dosyalarına derlenen Java kaynak kodu, bu sınıf dosyalarının anında derleme desteğiyle yorumlanması sayesinde nesne kodu düzeyindeki taşınabilirlik özelliğini yitirmeden hızlı bir şekilde çalıştırılır.