- Konu sahibi
- #1
Java konusunda sık yapılan hataları önlemenin en etkili yolu, kodu yalnızca çalışıp çalışmadığına göre değil; hata durumlarını, kaynak kullanımını, veri türlerini ve bakım kolaylığını da kontrol ederek geliştirmektir. Bunun için özellikle değişkenleri doğru başlatmalı,
Java derleyicisi sözdizimi ve tür uyumsuzluğu gibi sorunları yakalar; ancak yanlış sonuç üreten bir hesaplamayı, hatalı bir iş kuralını veya kapanmayan bir dosyayı her zaman tespit edemez. Bu nedenle bir programın derlenmesi, programın doğru olduğu anlamına gelmez.
Kodunuzu kontrol ederken üç ayrı soruyu birlikte sorun:
Bir değişkeni kullanılmadan önce anlamlı bir değerle başlatmak, hem derleme hatalarını hem de yanlış varsayımları azaltır. Özellikle sınıf alanlarını varsayılan değerlerine bırakmak, alanın henüz hazırlanmadığını gizleyebilir.
Aşağıdaki örnekte
Metoda boş bir dizi verilirse sonuç
Burada boş dizi ile
Değişkenleri mümkün olan en dar kapsamda tanımlayın. Bir değeri yalnızca döngü içinde kullanıyorsanız sınıf alanı yapmayın. Geniş kapsamlı ve sık değişen alanlar, hangi metodun veriyi değiştirdiğini izlemeyi zorlaştırır.
Nesnelerde
İki metnin içeriği aynı olduğu hâlde
Bir arama metodunun sonuç bulamadığında
Buradaki
Bir metodun geçerli sonuç vermediği durumlarda
Dosya, ağ bağlantısı veya veritabanı bağlantısı gibi kaynaklar bellekteki sıradan nesnelerden farklıdır. Bu kaynaklar işletim sistemi veya uzak servis üzerinde yer tuttuğu için yalnızca çöp toplayıcıya güvenmek doğru değildir.
Kaynak yönetiminde
Blok sona erdiğinde veya okuma sırasında istisna oluştuğunda okuyucu kapatılır. Bu,
Bir bağlantının kapatılması, işlemin başarıyla tamamlandığını kanıtlamaz. Örneğin dosyanın ilk satırını okuyup döndürmek, dosyanın bütün içeriğinin işlendiği anlamına gelmez. Uygulamanızın hedefi tüm kayıtları okumaksa döngünün tamamını ve okunan kayıt sayısını ayrıca kontrol etmelisiniz.
Aşağıdaki yaklaşım hatayı gizler:
Bu durumda kullanıcı işlem başarısız olduğu hâlde uygulama başarılıymış gibi devam edebilir. İstisnayı yakaladığınızda en azından şu kararlardan birini açıkça verin:
Bir koleksiyon üzerinde
Bu örnekte işlem sonucunda yalnızca
Bu nedenle anahtar olarak kullanılan sınıflarda:
Java'da iki tamsayının bölümü tamsayıdır. Bu nedenle
Burada dönüşüm bölmeden önce yapılır.
Ayrıca büyük sayılarda taşma ihtimalini değerlendirin. Bir
Birden fazla metodun aynı sınıf alanını değiştirmesi, hatanın hangi işlemden kaynaklandığını bulmayı zorlaştırır. Mümkün olduğunda nesneleri oluşturulduktan sonra değiştirilemeyecek şekilde tasarlayın, alanları
Bir sınıfa dışarıdan doğrudan değiştirilebilir liste vermek de benzer bir sorundur:
Birden fazla iş parçacığı aynı değişkeni okuyup yazıyorsa, işlemler beklediğiniz sırada gerçekleşmeyebilir.
Sorunu çözmek için önce paylaşımın gerçekten gerekli olup olmadığını belirleyin. Paylaşılan durum gerekmiyorsa her iş parçacığına kendi verisini verin. Gerekiyorsa erişim için uygun bir eşzamanlılık aracı seçin; yalnızca
Bir eşzamanlılık hatasını düzeltirken yalnızca hatanın görünmemesine bakmayın. Aynı işlemi çok sayıda iş parçacığı altında çalıştırıp beklenen toplam, kayıt sayısı ve işlem sırasını kontrol edin. Hata ara sıra oluşuyorsa bu, sorunun ortadan kalktığını değil, yeniden üretilmesinin zor olduğunu gösterebilir.
Kod incelemesinden veya göndermeden önce aşağıdaki sorulara cevap verin:
Bu tablo bir hata bulma aracıdır; tek başına testin yerini tutmaz. Her önemli davranış için beklenen sonucu belirleyen test yazın. Testte yalnızca başarılı senaryoyu değil, boş girdi, sınır değer, bulunamayan kayıt ve beklenmeyen hata durumunu da ayrı ayrı kontrol edin.
Önce hatayı mümkün olan en küçük örnekte yeniden üretin. Hatanın oluşması için gereken girdileri, beklenen sonucu ve gerçek sonucu kaydedin. Ardından tek bir değişiklik yapın ve aynı girdilerle tekrar çalıştırın. Sonuç düzelmişse bu, yalnızca o örneğin düzeldiğini gösterir; farklı sınır değerleri de test etmeden genel çözüm kabul edilmemelidir.
Örneğin
Üretimde görülen bir hata için günlüğe yalnızca istisna türünü değil, işlemi ayırt etmeye yarayan güvenli bilgileri de ekleyin. Ardından aynı hatayı geliştirme ortamında yeniden üretmek için kullanılan girdi, çağrı sırası ve ilgili kod sürümü belirlenmelidir. Bu bilgiler yoksa düzeltmenin gerçekten aynı soruna yönelik olup olmadığını doğrulamak zorlaşır.
Her
equals() ile == farkını gözetmeli, null durumlarını açıkça yönetmeli, kaynakları try-with-resources ile kapatmalı, koleksiyonları değiştirirken yineleme kurallarına uymalı ve istisnaları gelişigüzel yakalamamalısınız. Küçük bir örnek üzerinden birim test yazmak ve derleyici uyarılarını düzeltmek, hataların üretim ortamına taşınmasını büyük ölçüde azaltır.Hataları yalnızca derleme aşamasında aramayın
Java derleyicisi sözdizimi ve tür uyumsuzluğu gibi sorunları yakalar; ancak yanlış sonuç üreten bir hesaplamayı, hatalı bir iş kuralını veya kapanmayan bir dosyayı her zaman tespit edemez. Bu nedenle bir programın derlenmesi, programın doğru olduğu anlamına gelmez.
Kodunuzu kontrol ederken üç ayrı soruyu birlikte sorun:
- Kod derleniyor mu?
- Beklenmeyen veya boş girdilerde nasıl davranıyor?
- Uzun süre çalıştığında kaynakları serbest bırakıyor ve beklenen sonucu koruyor mu?
18 girdisinin doğru işlenmesi yeterli değildir. Boş metin, null, negatif sayı ve sayıya dönüştürülemeyen bir değer için de davranış belirlenmelidir. Hata önleme yaklaşımı, geçerli girdiyi işlemek kadar geçersiz girdiyi kontrollü biçimde reddetmeyi de kapsar.Değişkenleri açıkça başlatın ve kapsamını dar tutun
Bir değişkeni kullanılmadan önce anlamlı bir değerle başlatmak, hem derleme hatalarını hem de yanlış varsayımları azaltır. Özellikle sınıf alanlarını varsayılan değerlerine bırakmak, alanın henüz hazırlanmadığını gizleyebilir.
int alanı otomatik olarak 0, boolean alanı false, nesne referansı ise null değerini alır; ancak bu değerlerin iş uygulaması açısından geçerli olup olmadığını siz belirlemelisiniz.Aşağıdaki örnekte
toplam değişkeni başlangıçta açıkça tanımlanır:
Java:
public int toplam(int[] sayilar) {
int toplam = 0;
for (int sayi : sayilar) {
toplam += sayi;
}
return toplam;
}
Metoda boş bir dizi verilirse sonuç
0 olur. Bu, toplam işlemi için anlamlı olabilir; ancak null dizi gönderilmesi farklı bir durumdur. Metodun null kabul etmemesi gerekiyorsa bunu baştan belirleyip açık bir hata üretmek daha doğrudur:
Java:
import java.util.Objects;
public int toplam(int[] sayilar) {
Objects.requireNonNull(sayilar, "sayilar null olamaz");
int toplam = 0;
for (int sayi : sayilar) {
toplam += sayi;
}
return toplam;
}
Burada boş dizi ile
null birbirinden ayrılır. Boş dizi geçerli bir girdi olarak 0 döndürür; null ise programcının sözleşmeye aykırı çağrı yaptığını gösteren açık bir hata oluşturur. Bu davranış, daha sonra oluşabilecek belirsiz bir NullPointerException yerine sorunun kaynağını daha erken gösterir.Değişkenleri mümkün olan en dar kapsamda tanımlayın. Bir değeri yalnızca döngü içinde kullanıyorsanız sınıf alanı yapmayın. Geniş kapsamlı ve sık değişen alanlar, hangi metodun veriyi değiştirdiğini izlemeyi zorlaştırır.
== ile equals() kullanımını karıştırmayın
Nesnelerde
==, iki referansın aynı nesneyi gösterip göstermediğini karşılaştırır. equals() ise sınıfın tanımladığı mantıksal eşitliği kontrol etmek için kullanılır. Metin karşılaştırırken == kullanmak, bazı durumlarda çalışıyor gibi görünen ancak başka girdilerde bozulan hatalara yol açar.
Java:
String beklenen = "admin";
String girilen = new String("admin");
System.out.println(beklenen == girilen); // false
System.out.println(beklenen.equals(girilen)); // true
İki metnin içeriği aynı olduğu hâlde
== sonucu false olabilir; çünkü iki ayrı String nesnesi oluşturulmuştur. Metin içeriğini karşılaştırmak için equals() kullanın. Değişkenlerden biri null olabilecekse karşılaştırmayı sabit metin üzerinden yapmak daha güvenlidir:
Java:
if ("admin".equals(kullaniciRolu)) {
// Kullanıcı rolü admin ise
}
kullaniciRolu null olsa bile bu koşul hata üretmez ve false olur. Bu yaklaşım, yalnızca kullaniciRolu.equals("admin") yazmaktan daha güvenlidir.null değerleri varsaymak yerine sözleşmeyi belirleyin
null, bir referansın herhangi bir nesneyi göstermediğini belirtir. Hata çoğu zaman null değerinden değil, kodun bu değerin gelemeyeceğini varsaymasından kaynaklanır. Her metodun hangi parametreleri kabul ettiğini ve hangi durumlarda null döndürebileceğini belirlemek gerekir.Bir arama metodunun sonuç bulamadığında
null döndürmesi mümkünse çağıran kod bunu kontrol etmelidir:
Java:
Kullanici kullanici = kullaniciBul(id);
if (kullanici == null) {
System.out.println("Kullanıcı bulunamadı.");
return;
}
System.out.println(kullanici.getAd());
Buradaki
null sonucu, veritabanı bağlantısının kesin olarak bozuk olduğunu kanıtlamaz; yalnızca bu çağrıda kullanıcı nesnesi bulunamadığını gösterir. Kayıt bulunamaması ile bağlantı hatası farklı durumlar olduğundan, veri erişim katmanı bu durumları mümkünse farklı istisnalarla veya açık bir sonuç türüyle ayırmalıdır.Bir metodun geçerli sonuç vermediği durumlarda
Optional kullanmak bazı API'lerde okunabilirliği artırabilir; ancak her alanı ve her parametreyi Optional yapmak zorunlu veya uygun değildir. Asıl amaç, null sözleşmesini gizlememektir.Kaynakları her durumda kapatın
Dosya, ağ bağlantısı veya veritabanı bağlantısı gibi kaynaklar bellekteki sıradan nesnelerden farklıdır. Bu kaynaklar işletim sistemi veya uzak servis üzerinde yer tuttuğu için yalnızca çöp toplayıcıya güvenmek doğru değildir.
Kaynak yönetiminde
try-with-resources kullanın:
Java:
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public String ilkSatiriOku(Path dosya) throws IOException {
try (BufferedReader okuyucu = Files.newBufferedReader(dosya)) {
return okuyucu.readLine();
}
}
Blok sona erdiğinde veya okuma sırasında istisna oluştuğunda okuyucu kapatılır. Bu,
finally içinde elle kapatma işlemine göre daha az tekrar içerir ve kapatma sırasındaki hataların daha doğru yönetilmesini sağlar.Bir bağlantının kapatılması, işlemin başarıyla tamamlandığını kanıtlamaz. Örneğin dosyanın ilk satırını okuyup döndürmek, dosyanın bütün içeriğinin işlendiği anlamına gelmez. Uygulamanızın hedefi tüm kayıtları okumaksa döngünün tamamını ve okunan kayıt sayısını ayrıca kontrol etmelisiniz.
İstisnaları sessizce yutmayın
Aşağıdaki yaklaşım hatayı gizler:
Java:
try {
dosyaOku();
} catch (IOException e) {
// Hiçbir işlem yapılmıyor
}
Bu durumda kullanıcı işlem başarısız olduğu hâlde uygulama başarılıymış gibi devam edebilir. İstisnayı yakaladığınızda en azından şu kararlardan birini açıkça verin:
- İstisnayı anlamlı bir mesajla üst katmana iletmek.
- Güvenli bir varsayılan değer döndürmek.
- İşlemi yeniden denemek; ancak sonsuz deneme döngüsü oluşturmamak.
- İşlemi iptal edip kullanıcıya veya uygulama günlüğüne yeterli bilgi vermek.
Exception veya Throwable gibi geniş türleri her yerde yakalamak, programlama hatalarını ve sistem düzeyi sorunları maskeleyebilir. Hata mesajına işlem adı, ilgili kimlik ve güvenli bağlam bilgisi ekleyin; parola, erişim anahtarı ve kişisel verileri günlüğe yazmayın.Koleksiyonları yineleme sırasında değiştirmeyin
Bir koleksiyon üzerinde
for-each ile dolaşırken aynı koleksiyondan eleman silmek, çoğu durumda ConcurrentModificationException oluşturabilir veya kodun davranışını belirsiz hâle getirebilir. Silme ihtiyacı varsa yineleyicinin kendi remove() metodunu kullanın ya da koşula göre removeIf tercih edin.
Java:
import java.util.ArrayList;
import java.util.List;
List<String> roller = new ArrayList<>(List.of("admin", "misafir", "editor"));
roller.removeIf(rol -> rol.equals("misafir"));
System.out.println(roller); // [admin, editor]
Bu örnekte işlem sonucunda yalnızca
misafir rolü çıkarılır. Koleksiyonu başka bir iş parçacığı aynı anda değiştiriyorsa konu yalnızca removeIf kullanımıyla çözülmez; eşzamanlı erişim için uygun veri yapısı veya kilitleme stratejisi ayrıca seçilmelidir. Tek iş parçacıklı bir döngü hatası ile çok iş parçacıklı erişim sorunu birbirine karıştırılmamalıdır.Değişebilir nesneleri anahtar olarak kullanırken dikkatli olun
HashMap ve HashSet gibi yapılar, hashCode() ve equals() sonuçlarına göre çalışır. Bir nesne haritaya anahtar olarak eklendikten sonra, eşitlik veya karma değerini etkileyen alanını değiştirirseniz nesne haritada bulunamayabilir.Bu nedenle anahtar olarak kullanılan sınıflarda:
equals()vehashCode()kurallarını birlikte ve tutarlı uygulayın.- Anahtar alanlarını mümkünse
finalyapın. - Anahtarın haritaya eklenmesinden sonra eşitliği etkileyen alanları değiştirmeyin.
equals() metodunu geçersiz kılıyor ancak hashCode() metodunu geçersiz kılmıyorsa, eşit kabul edilen iki nesne farklı karma değerlerine sahip olabilir. Bu durumda HashMap veya HashSet beklenmedik sonuç verir. Bir nesne eşitlik açısından aynı kabul ediliyorsa hashCode() sonucu da aynı olmalıdır.Tamsayı bölmesi ve tür dönüşümlerini kontrol edin
Java'da iki tamsayının bölümü tamsayıdır. Bu nedenle
5 / 2 sonucu 2 olur; 2.5 bekliyorsanız işleme en az bir kayan noktalı değer katmalısınız.
Java:
int tamamlanan = 5;
int toplam = 2;
double oran = (double) tamamlanan / toplam;
System.out.println(oran); // 2.5
Burada dönüşüm bölmeden önce yapılır.
double oran = tamamlanan / toplam; yazılırsa bölme önce tamsayı olarak gerçekleşir ve sonuç 2.0 olur. Değişkenin türünü double yapmak tek başına yeterli değildir; dönüşümün işlemin doğru noktasında yapılması gerekir.Ayrıca büyük sayılarda taşma ihtimalini değerlendirin. Bir
int değeri sınırını aşarsa sonuç hata vermeden beklenmeyen bir değere dönebilir. Para, kimlik numarası veya yüksek hacimli sayaçlarda alanın gerektirdiği türü seçin; para hesaplarında kayan noktalı türlerin yuvarlama davranışını ayrıca değerlendirin.Paylaşılan değişebilir durumu azaltın
Birden fazla metodun aynı sınıf alanını değiştirmesi, hatanın hangi işlemden kaynaklandığını bulmayı zorlaştırır. Mümkün olduğunda nesneleri oluşturulduktan sonra değiştirilemeyecek şekilde tasarlayın, alanları
private tutun ve doğrudan değişiklik yerine kontrollü metodlar kullanın.Bir sınıfa dışarıdan doğrudan değiştirilebilir liste vermek de benzer bir sorundur:
Java:
import java.util.ArrayList;
import java.util.List;
public final class Siparis {
private final List<String> urunler;
public Siparis(List<String> urunler) {
this.urunler = List.copyOf(urunler);
}
public List<String> getUrunler() {
return urunler;
}
}
List.copyOf, sınıfın içindeki listenin dışarıdan değiştirilmesini önlemeye yardımcı olur. Ancak listedeki elemanların kendisi değişebilir nesnelerse yalnızca listenin kopyalanması elemanları otomatik olarak değişmez yapmaz. Bu nedenle sınıfın bütün veri modelini, yalnızca dış kabını değil, değişebilirlik açısından değerlendirin.İş parçacığı hatalarını sıradan değişken hatası gibi ele almayın
Birden fazla iş parçacığı aynı değişkeni okuyup yazıyorsa, işlemler beklediğiniz sırada gerçekleşmeyebilir.
sayac++ gibi görünen tek bir ifade bile okuma, artırma ve yazma adımlarından oluşur; bu adımlar eşzamanlı erişimde birbirinin üzerine yazılabilir.Sorunu çözmek için önce paylaşımın gerçekten gerekli olup olmadığını belirleyin. Paylaşılan durum gerekmiyorsa her iş parçacığına kendi verisini verin. Gerekiyorsa erişim için uygun bir eşzamanlılık aracı seçin; yalnızca
volatile kullanmak, bileşik işlemleri otomatik olarak atomik hâle getirmez.Bir eşzamanlılık hatasını düzeltirken yalnızca hatanın görünmemesine bakmayın. Aynı işlemi çok sayıda iş parçacığı altında çalıştırıp beklenen toplam, kayıt sayısı ve işlem sırasını kontrol edin. Hata ara sıra oluşuyorsa bu, sorunun ortadan kalktığını değil, yeniden üretilmesinin zor olduğunu gösterebilir.
Hataları önlemek için kısa bir kontrol listesi kullanın
Kod incelemesinden veya göndermeden önce aşağıdaki sorulara cevap verin:
| Kontrol | Sorulacak soru | Sorun varsa ilk adım |
|---|---|---|
| Veri türü | Bölme, taşma veya dönüşüm sonucu beklenen türde mi? | İşlemi ve dönüşümün yapıldığı satırı kontrol edin. |
| Nesne karşılaştırması | İçerik mi, aynı nesne mi karşılaştırılıyor? | Metin ve değer karşılaştırmalarında equals() kullanımını inceleyin. |
| Null yönetimi | Parametre veya dönüş değeri null olabilir mi? | Sözleşmeyi belirleyip erken kontrol veya uygun sonuç türü ekleyin. |
| Kaynak yönetimi | Dosya, bağlantı veya akış her durumda kapanıyor mu? | Kaynağı try-with-resources içine alın. |
| İstisna yönetimi | Hata kayboluyor mu, yanlış katmanda mı yönetiliyor? | İstisnayı anlamlı biçimde iletin veya kontrollü olarak yönetin. |
| Koleksiyon işlemi | Yineleme sürerken koleksiyon değişiyor mu? | removeIf, yineleyici veya uygun eşzamanlı yapı kullanın. |
| Paylaşılan durum | Birden fazla iş parçacığı aynı veriyi değiştiriyor mu? | Paylaşımı kaldırın veya erişimi uygun biçimde senkronize edin. |
Bir hatayı düzeltirken sonucu nasıl doğrulayabilirsiniz?
Önce hatayı mümkün olan en küçük örnekte yeniden üretin. Hatanın oluşması için gereken girdileri, beklenen sonucu ve gerçek sonucu kaydedin. Ardından tek bir değişiklik yapın ve aynı girdilerle tekrar çalıştırın. Sonuç düzelmişse bu, yalnızca o örneğin düzeldiğini gösterir; farklı sınır değerleri de test etmeden genel çözüm kabul edilmemelidir.
Örneğin
5 / 2 sorunu için yalnızca 5 ve 2 değerlerini değil, 0, negatif sayılar, eşit değerler ve büyük değerleri de test edin. Sıfıra bölme gibi bir durumda sonuç matematiksel olarak tanımsızdır; bunu yakalayıp kullanıcıya anlamlı bir hata vermek, rastgele bir değer döndürmekten daha doğrudur.Üretimde görülen bir hata için günlüğe yalnızca istisna türünü değil, işlemi ayırt etmeye yarayan güvenli bilgileri de ekleyin. Ardından aynı hatayı geliştirme ortamında yeniden üretmek için kullanılan girdi, çağrı sırası ve ilgili kod sürümü belirlenmelidir. Bu bilgiler yoksa düzeltmenin gerçekten aynı soruna yönelik olup olmadığını doğrulamak zorlaşır.
Sık sorulan sorular
Her null değeri için istisna fırlatmak gerekir mi?
Hayır.
null uygulamanın geçerli bir sonucuysa bunu açıkça temsil edip çağıran kodun işlemesini sağlayabilirsiniz. Ancak null kabul edilmeyen bir parametre geldiyse erken ve anlamlı bir hata üretmek, daha sonra belirsiz bir noktada oluşacak hatadan daha yararlıdır.== hiç mi kullanılmaz?
Kullanılır; ancak nesnelerin içeriğini karşılaştırmak için değil, aynı nesne referansını veya özel durumlarda aynı referansı kontrol etmek için kullanılır. İlkel türlerde değer karşılaştırması yapar.
String, Integer ve kendi sınıflarınız gibi nesnelerde hangi karşılaştırmanın gerektiğini açıkça belirleyin.Bir istisnayı yakalayıp yeniden fırlatmak sorunu çözmüş olur mu?
Tek başına olmaz. Yeniden fırlatma, hatayı uygun katmana iletmek için kullanılabilir; ancak işlem bağlamı, kullanıcıya verilecek yanıt ve günlük kaydı da düşünülmelidir. İstisnayı yakalayıp aynı anlamı taşımayan yeni bir hata mesajıyla sarmalamak, asıl nedeni gizlememelidir.
Birim test yazmak bütün Java hatalarını önler mi?
Hayır. Birim testler, yazılmış senaryolardaki beklenen davranışı doğrular; test edilmeyen eşzamanlılık, kaynak tüketimi, yapılandırma veya entegrasyon sorunlarını garanti etmez. Bu nedenle birim testleri kod incelemesi, sınır değer kontrolleri ve gerektiğinde entegrasyon testleriyle birlikte kullanın.