İstisna Yönetimi

Hatalar programlarımızın akışını kesen durumlardır. Bu durum bazen bilinçli, bazen de bilinçsiz şekilde karşılaşılabilen bir olgudur. Haliyle her ihtimal düşünülerek bunları yönetmek isteriz. Javada da bu yönetim için geliştirilmiş bir yaklaşım bulunmaktadır.

17 – Exceptions isimli projemizde bu konuyla ilgili örnekleri barındıracağız. Hatalar genel olarak olması beklenen bir durumun, farklı veya istenmeyen bir şekilde vuku bulması ile oluşur. Örneğin dolu olmasını beklediğiniz bir dizinin, bir şekilde boş oluşması sebebi ile, kodunuzda o dizi ile işlem yapan kısım hata alabilir. Bağlanmaya çalıştığınız veri tabanı çökmüş, okumaya çalıştığınız dosya silinmiş, istek attığınız web servis cevap formatını değiştirmiş olabilir. Bunların hepsi bir hata sebebine dönüşebilir. Bu yaklaşımla sizin de programınızı kontrollü bir şekilde hataya düşürmeniz, uygulama sağlığı, hizmet devamlılığı gibi konularda faydalı olacaktır.

Öncelikle kavramlara değinmek istiyorum. Burada bahsi geçen aksiliklerin tamamına hata ismini vermek, teknik olarak yanlış bir yaklaşımdır. Esasında bu durumlar, hata (Error) ve istisna (Exception) olarak ikiye ayrılmaktadır. Genel olarak “hata” gerçekleştiğinde kontrol edemeyeceğiniz durumları temsil eder. Buna en sık verilen örnek, bir şekilde uygulamanın memory üzerinden yük yaratıp, onu doldurması ve nihayetinde onu kullanılamaz hale getirmesi yani tamamen durdurmasıdır. İstisnalar ise kontrolü daha mümkün durumlardır. Örneğin, kullanıcıdan aldığı iki sayıyı birbirine bölen bir uygulamanın, sıfıra bölme sonucunda karşılaştığı matematiksel durumun yönetimi mümkündür.

İstisnalar iki ayrı şekilde sınıflandırılabilir

  • Checked Exceptions: Çalışma anı hataları dışında kalan istisnalara verilen isimdir. Bu istisnalar, derleyici tarafından derleme anında kontrol edilmektedir. Eğer bunları kontrol altına almaz veya programınızda deklare etmezseniz uygulamanız derlenmez.
  • Unchecked Exceptions: Çalışma anı hatalarıdır. Bu tarz hatalar için derleyici sizi uyarmaz. Bu durumun oluşup, programın düzgün şekilde yoluna devam edebilmesi, yazılımcının dikkatine bağlıdır.

İlk olarak klasik bir sıfıra bölme hatasını inceleyelim. Bir hesaplama sınıfı yazacağız ve bunun içine karşılaştırma methodu ekleyeceğiz. Methodumuz iki parametre alacak ve ilk sayının ikinci sayıya oranını ekrana yazdıracak.

public class Calculation {

	public static void compare(int a, int b) {
		
		double comparison = a/b;
		System.out.println("a is greater than b " + comparison +" times.");
	}
	
}

Bu koddaki comparison değişkeni oldukça tehlikeli bir biçimde hesaplanmıştır. Bu tehlikenin sebebi b değişkeninin 0 gelme olasılığıdır. Bu methodu hem çalışırken, hem de istisna fırlatırken (Exception fırlatmak) gözlemleyelim.

Görüleceği üzere TestClass içerisinde 14. satırdaki kod doğru çıktıyı vermektedir. 15.satır çalıştığında ise 2/0 ifadesi hataya sebep olur. Bunun hemen ardından 4 ve 2 sayılarını karşılaştıran kodun da çalışmadığını gözlemliyoruz. Zira istisna yönetilmemiş durumdadır. Yazılım jargonu ile buradaki exception handle edilmemiştir.

“Bu methoda gelen parametre sıfır mı?” kontrolünü kendimiz ekleyebilir ve istisnanın önüne geçebilirdik. Ancak bunun yerine hatayı yaşar halde tutup, bu durumları nasıl yöneteceğimiz konusuna odaklanalım. Javada istisna yönetimi için “Şu işlemi dene ve olur da sorun olursa şu yolu izle” şeklinde cümleleştirebileceğimiz bir yaklaşım mevcuttur. Bu yaklaşım için try-catch (dene-yakala) ifadesi ile kullanılır.

public static void compare(int a, int b) {

	try {
		double comparison = a / b;
		System.out.println("a is greater than b " + comparison + " times.");
	} catch (Exception e) {
		System.out.println("Comparison failed!");
	}

}

Kodda da görüleceği gibi try ve catch ifadeleri birer bloktur. Bu şekilde yazılan bir kodun amacı try bloğu içerisinde olur da bir istisna oluşursa, kodun akışının catch bloğu içerisine düşerek ilerlemesini sağlamaktır. Bu değişiklik ardından main methodumuzu tekrar tetiklediğimizde daha farklı bir çıktı alırız.

Çıktı:
a is greater than b 2.0 times.
Comparison failed!
a is greater than b 2.0 times.

Görüldüğü üzere program yoluna devam ederek 3. hesaplamayı da yaptı. İkinci değişkenin sıfır geldiği durumda ise catch bloğuna düşerek akışın devamlılığını sağladı.

Bu kod parçasında görürüz ki catch idafesinin yanında bir parantez vardır ve içerisinde Exception e yazmaktadır. Bunun anlamı try bloğundan fırlayan ve Exception tipinde olan (instanceof) istisna, catch bloğu içerisinde (scope) e ismi ile anılan bir nesne olacaktır. Bu nesnenin üzerindeki getMessage() methodunu kullanarak çıktımıza tekrar göz atalım ve exception üzerindeki mesajı da konsola yazdırmış olalım.

 catch (Exception e) {
	System.out.println("Comparison failed! -> " + e.getMessage());
}
Çıktı:
a is greater than b 2.0 times.
Comparison failed! -> / by zero
a is greater than b 2.0 times.

Bu yazıdaki ilk ekran görüntüsüne dikkatli baktığınızda, “java.lang.ArithmeticException” ifadesine denk gelirsiniz. Bu aslında javanın içerisinden (core) gelen bir sınıfın ismidir. Bu sınıf RuntimeException sınıfından türemiştir. RuntimeException ise Exception sınıfından türemiştir. Nihayetinde ise Exception sınıfı Throwable sınıfından türemiştir. Javadaki tüm hata ve istisna sınıflarının en üst atası Throwable sınıfıdır. İstisna yönetimi yaparken bu zincirden yararlanmak mümkündür. Örneğin yukarıdaki catch bloğunda yakalanmak istenen istisnanın Exception sınıfından olması beklenmekte. Ancak biz ArithmeticException istisnası geldiğinde başka, farklı bir istisna geldiğinde başka davranmak isteyebiliriz. Java bu noktada catch bloğunu çoklamamıza izin vermektedir.

public static void compareArrayIndexes(int[] a, int b) {

	try {
		
		for (int i = 0; i < a.length; i++) {
			double comparison = a[i] / b;
			System.out.println("a ["+ i +"] is greater than b " + comparison + " times.");
		}
		
	} catch (NullPointerException e) {
		System.out.println("NullPointerException block");
	} catch (ArithmeticException e) {
		System.out.println("ArithmeticException block");
	}

}

Yukarıdaki kod bloğunda çoklu catch kullanımının bir örneğini görmekteyiz. Bu durumda try içerisinden fırlayacak istisnanın tipine göre uygun blok çalışır. Bu method Calculation sınıfına eklenmiştir. Method, bir önceki methoda benzemektedir. Bu defa kendisine gönderilen dizinin tüm indekslerindeki değerler ile ikinci parametreyi kıyaslamaktadır. Buradaki akışın çalışmasını görmek için 3 farklı dizi ile işlem yapacağız.

int[] array1 = { 2, 4, 6, 8 };
Calculation.compareArrayIndexes(array1, 1);

int[] array2 = { 2, 4, 6 };
Calculation.compareArrayIndexes(array2, 0);

int[] array3 = null;
Calculation.compareArrayIndexes(array3, 0);

Bu kod için konuşmak gerekirse; ilk dizi hatasız şekilde çalışacaktır. İkinci dizi ise 0 ile karşılaştırıldığından ilk örneğimizdeki hatayı (0’a bölme istisnası) verecektir. Son olarak herhangi bir değer yüklemesi yapılmayan değişken ile işlem yaparken alınabilecek olan, null pointer exception hatasını (istisna) gözlemleyeceğiz.

Çıktı:
a [0] is greater than b 2.0 times.
a [1] is greater than b 4.0 times.
a [2] is greater than b 6.0 times.
a [3] is greater than b 8.0 times.
ArithmeticException block
NullPointerException block

Görüldüğü gibi fırlatılan istisna, ilgili catch bloğu tarafından yakalanmaktadır. Catch bloklarının sırası önem taşımaktadır. Zira yukarıdan aşağıya bir akış söz konusudur. Eğer en genel istisna türünüzü en yukarıda yakalasaydınız, aşağıdaki catch blokları ulaşılamaz durumda olurdu. Hatta ve hatta derleyici hata verir ve programınız derlenmezdi. Ekran görüntüsünde bu durumu gözlemleyebilirsiniz.

Bunlarla beraber eğer iki ayrı exception türünü aynı şekilde yönetmek isterseniz, sizi kod tekrarından kurtaracak bir çözüm javada bulunmaktadır. Çok basit şekilde iki ayrı exception sınıfını pipe yani | işareti ile ayırarak catch parantezi içerisinde yazabilirsiniz.

catch (NullPointerException | ArithmeticException e) {
	System.out.println("NullPointerException | ArithmeticException block");
}

Catch blokları zaman zaman bir methoddan çıkış noktalarıdır. Yani o noktada methodunuz return ifadesi ile sonlanır. Ancak bir istisna yaşansın ya da yaşanmasın, bazı eylemleri gerçekleştirmek isteyebilirsiniz. Bu konuda en sık verilen örnek veri tabanı bağlantılarıdır.Veri tabanlarını belirli sayıda bağlantıyı açık tutabileceğiniz yapılar olarak düşünebilirsiniz. Buraya kurduğunuz bağlantı ile yaptığınız işlem başarılı veya başarısız olsun, bağlantıyı mutlaka kapatmak istersiniz. Böyle bir durum sizi hem try hem de catch bloğu içerisinde aynı kodu yazmaya zorlar. Bu tekrardan kurtulmak için try-catch bloğu ile birlikte kullanılan bir de finally bloğu vardır. Bu blok try veya catch bloklarının çalışmasının ardından çalışır. Üstelik bu bloklarda return yapsanız bile, finally bloğuna yazdığınız kod çalışacaktır. Hatta ve hatta finally bloğu ile return ifadenizi değişikliğe uğratabilirsiniz. Bunu bir örnek vasıtasıyla gözlemleyelim. Aşağıdaki methodu Calculation sınıfımıza ekliyoruz.

public static void getArrayIndex(int index, String[] array) {
	
	try {
		System.out.println("try block");
		System.out.println("Success -> " + array[index]);
	} catch (Exception e) {
		System.out.println("catch block");
	} finally {
		System.out.println("finally block");
	}
	
}

Görüldüğü gibi en alta bir finally bloğu geldi. Sıralamanın bu şekilde olması gerekmektedir. Bu method bir dizi ve indeks almaktadır. Aldığı indeksi dizinin içinde aratarak try bloğunda ekrana yazdırmaya çalışır. Eğer bir istisna oluşursa (Olmayan bir indeksin ekrana yazdırılmaya çalışılması), başarılı mesajı görülmeden catch bloğuna düşeriz. Hata olsun veya olmasın, finally bloğu çalışacaktır. Aşağıdaki kod ile testimizi yapalım ve çıktıya göz atalım.

String[] array = { "British", "Racing", "Green", "Aston", "Martin" };

Calculation.getArrayIndex(1, array);

System.out.println("--------------");

Calculation.getArrayIndex(5, array);
Çıktı:
try block
Racing
finally block
--------------
try block
catch block
finally block

İstisna yönetimi ile ilgili son değineceğimiz başlık throw ve throws sözcükleri olacaktır. Javada istisnalar, sorunlardan dolayı belirli bir kod parçasından fırlatılır. Bu ifade istisnanın oluşup bir üst çerçeveye (frame, method, sınıf) hata alındığının bildirilmesi anlamına gelmektedir. Bunları görebilmek adına, Calculation sınıfımıza, istisna fırlatmaya çok uygun bir method yazalım.

public static void sum(Object o1, Object o2) {
	
	int sum = (int) o1 + (int) o2;
	System.out.println("Sum is " + sum);
	
}

Görüldüğü üzere bu method içerisinde bir casting, yani dönüşüm işlemi yapılmaktadır. Ancak sınıfın parametreleri Object tipinde olduğundan kendisine gönderilen her türlü nesneyi kabul eder. Tahmin edileceği üzere her nesne tipi int tipine dönüştürülemez. Örneğin bu methodu Calculation.sum(“a”, “a”); şeklinde çağırmaya çalışırsak, ClassCastException alırız. O halde bu methodun istisna fırlatabileceğini belirterek, methodu çağıran kişilere dikkatli olması gerektiğini söyleyelim. Öncelikle ilk durumu görelim.

Çıktı:
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
     at Calculation.sum(Calculation.java:49)
     at TestClass.case4(TestClass.java:47)
     at TestClass.main(TestClass.java:9)

Şimdi ise düzenlenmiş durum üstünden konuya göz atalım. Öncelikle sum() methodumuzun imzasını güncelleyeceğiz. Güncellememiz yeni bir ifade ekleyerek gerçekleşecek. Bu ifade throws Exception ifadesidir. Bu ifadeyi eklediğimizde method çağrısının da derleyici tarafından işaretlendiğini görürüz.

Buradaki durumun yorumu, derleyicinin bize “Burada bir hata oluşma ihtimali var ve bunu kontrol altında tutmalısın” mesajı verdiği yönündedir. İşte bu tarz durumlar yine try-catch blokları ile yönetilmektedir. Buradaki uyarının düzeltilmesi için derleyici bize iki seçenek sunmaktadır. İlk seçenek çağrıyı yapan methodumuzun da imzasına throws ifadesi ekleyerek, bu methodu çağıran bir üst methodun istisnayı yönetmesini sağlamak yönündedir. Yani “exception handle edilmez” ve “üst methoda throw edilir”. İkinci seçenek ise istisna yaşamaya yatkın satırın (veya satırların) try-catch bloğu ile sarmalanması yönündedir. Biz de bu ikinci seçenek üzerinden ilerleyeceğiz. Editörden de faydalanarak “Surround with try/catch” seçeneğine tıklıyoruz.

Ardından methodumuz aşağıdaki hale gelecektir. Bu durumu editörsüz yönetmeniz de mümkündür. Editör sadece işleri kolaylaştırır. Dolayısı ile aşağıdaki dönüşümü elle yapmanız ile editör üzerinden yapmanız arasında bir fark bulunmamaktadır.

private static void case4() {
	try {
		Calculation.sum("", "");
	} catch (Exception e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}

Bu kodu çalıştırdığımızda konsolda hata almamıza rağmen program tamamlanır. Bu bir catch bloğunun varlığı ile mümkündür. Eğer bu bloğu eklemeseydik programımız derlenmeyeceği için hiç çalışmayacaktı. Peki ya ikinci opsiyonumuzu değerlendirmek isteseydik? Yani case4() methodumuza throws ibaresi ekleseydik? Bu durumda aşağıdaki gibi bir methodumuz olurdu.

private static void case4() throws Exception {
	Calculation.sum("", "");
}

Bu seçenekte ise case4() methodunun çağırıldığı yerde bir kurguya gitmek gerekirdi. Yine elimizdeki 2 opsiyon geçerli olurdu. Haliyle istisna yönetimlerinde nasıl bir strateji izleneceği konusunda esneklik söz konusudur. Yapılar büyüdükçe ve karmaşıklaştıkça hata yönetimi, loglama vb. stratejileri de duruma göre şekil değiştirebilmektedir.

Son olarak TestClass sınıfımızın tam halini paylaşmak ve kapanışı yapmak isterim. Bu yazı dizisini özellikle javaya ilk başladığım dönemde yaşadığım zorlukları düşünerek oluşturmaya gayret ettim. Özellikle kullanılan genel deyimlere ve jargona mümkün olduğunca yakın durmaya çalıştım. Dili aşırı yerelleştirerek, yazı serisini tamamen anlaması imkansız bir hale getirmek istemedim. Zira bu fazlasıyla yerelleşmiş ifadeler iş hayatında kullanılmadığı gibi, herkesin bilmediği ifadeler olduğundan karşınızdaki kişilerle anlaşmayı da zorlaştırmakta. Ayrıca bu yazılar, bir yandan her şeye çok hızlı ulaşabildiğimiz, bilginin parmak uçlarımızı çalıştırma seviyesine indiği bu dönemde üretim yapmanın ne kadar meşakkatli olduğunu anladığım bir seri oldu. Olur da bu seriyi okuyan birisi olursa lütfen yorumlar kısmı üzerinden daha iyi olabilecek noktaları iletsin isterim. Hatamız olduysa affola diyerek sözümü bitirmek istiyor ve bu seriyi okuyan herkese sevgilerimi iletiyorum.

public class TestClass {

	public static void main(String[] args) {

		case1();
		case2();
		case3();
		case4();

		System.out.println("Forever and ever");

	}

	private static void case1() {

		Calculation.compare(2, 1);
		Calculation.compare(2, 0);
		Calculation.compare(4, 2);

	}

	private static void case2() {

		int[] array1 = { 2, 4, 6, 8 };
		Calculation.compareArrayIndexes(array1, 1);

		int[] array2 = { 2, 4, 6 };
		Calculation.compareArrayIndexes(array2, 0);

		int[] array3 = null;
		Calculation.compareArrayIndexes(array3, 0);

	}
	
	private static void case3() {
		
		String[] array = { "British", "Racing", "Green", "Aston", "Martin" };

		Calculation.getArrayIndex(1, array);

		System.out.println("--------------");

		Calculation.getArrayIndex(5, array);
		
	}

	
	private static void case4() {
		try {
			Calculation.sum("", "");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
}

Leave a Reply

Your email address will not be published. Required fields are marked *