← Kursa Dön
📄 Text · 15 min

try-catch-finally

Programlar her zaman planladığın gibi çalışmaz. Kullanıcı beklenmedik bir şey girer, dosya bulunamaz, ağ bağlantısı kopar. Java'da bu tarz "istisnai durumları" yönetmenin yolu exception handling mekanizmasıdır. Ve bu mekanizmanın temel yapı taşı: try-catch-finally.

Neden Exception Handling?

Şöyle düşün: Bir restoranda yemek sipariş ediyorsun. Normalde garson siparişi alır, mutfak hazırlar, yemeğin gelir. Ama ya mutfakta malzeme bittiyse? Ya da şef hastalandıysa? Restoran bu durumlarda "kapıyı kapatıp kaçmak" yerine bir B planı devreye sokar — belki alternatif bir yemek önerir, belki özür dileyip indirim yapar.

Exception handling de tam olarak bu. Program bir sorunla karşılaştığında çökmek yerine kontrollü bir şekilde durumu yönetmesini sağlarsın.

Exception handling olmadan kod şöyle görünür:

int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsException!
System.out.println("Bu satır asla çalışmaz");

Program patlar, kullanıcı kötü bir hata mesajı görür, geri kalan kod çalışmaz. Hiç hoş değil.

try-catch Temel Yapısı

En basit haliyle: "Şu kodu dene (try), hata olursa yakala (catch)."

try {
    int[] numbers = {1, 2, 3};
    System.out.println(numbers[5]);
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Hata: Geçersiz index! " + e.getMessage());
}
System.out.println("Program çalışmaya devam ediyor");

Burada olan şey basit: try bloğundaki kod çalışır. Eğer ArrayIndexOutOfBoundsException fırlarsa, program catch bloğuna atlar. En önemlisi — program çökmez, son satır da çalışır.

catch bloğundaki e parametresi, fırlayan exception'ın kendisi. Ondan bilgi alabilirsin:

try {
    String text = null;
    text.length();
} catch (NullPointerException e) {
    System.out.println("Mesaj: " + e.getMessage());
    System.out.println("Tür: " + e.getClass().getName());
    e.printStackTrace(); // Detaylı hata izi — debug için altın değerinde
}

e.getMessage() kısa bir açıklama verir. e.printStackTrace() ise hatanın tam izini — hangi satırda, hangi method'da olduğunu — konsola basar. Debug yaparken en çok kullanacağın method budur.

Birden Fazla catch Bloğu

Bir try bloğunda farklı türlerde hatalar olabilir. Her birini ayrı yakalayabilirsin:

try {
    String input = "abc";
    int number = Integer.parseInt(input);
    int[] arr = new int[number];
    arr[10] = 5;
} catch (NumberFormatException e) {
    System.out.println("Sayıya çevrilemedi: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Geçersiz index: " + e.getMessage());
} catch (NegativeArraySizeException e) {
    System.out.println("Negatif boyut olmaz!");
}

Java, catch bloklarını yukarıdan aşağıya kontrol eder. İlk eşleşen catch çalışır, diğerleri atlanır.

⚠️ Dikkat: Catch bloklarını özelden genele sırala. Eğer üst sınıf exception'ı önce yazarsan, alt sınıflar asla yakalanmaz ve compiler hata verir.

// YANLIŞ — compile hatası!
try {
    // ...
} catch (Exception e) {         // Çok genel — her şeyi yakalar
    // ...
} catch (IOException e) {       // Buraya asla ulaşılamaz!
    // ...
}

// DOĞRU
try {
    // ...
} catch (IOException e) {       // Önce spesifik
    // ...
} catch (Exception e) {         // Sonra genel
    // ...
}

Multi-catch (Java 7+)

Bazen farklı exception türleri için aynı şeyi yapmak istersin. Java 7 öncesi her biri için ayrı catch yazman gerekirdi. Artık | (pipe) operatörüyle birleştirebilirsin:

try {
    // Dosya oku, parse et...
} catch (IOException | ParseException e) {
    System.out.println("Dosya işleme hatası: " + e.getMessage());
    logger.error("Hata oluştu", e);
}

Bu, aynı kodu iki kere yazmaktan kurtarır. Temiz, okunabilir, DRY.

Birkaç kural var:

// Birden fazla exception türünü birleştirebilirsin
catch (IOException | SQLException | ParseException e) {
    // e'nin türü bu üçünün ortak üst sınıfı olur
    handleError(e);
}

// AMMA — biri diğerinin alt sınıfıysa OLMAZ
catch (FileNotFoundException | IOException e) { // HATA!
    // FileNotFoundException zaten IOException'ın alt sınıfı
}

💡 İpucu: Multi-catch'te e değişkeni effectively final'dır. Yani catch bloğu içinde e = new IOException() gibi yeniden atayamazsın.

finally Bloğu

finally bloğu ne olursa olsun çalışır. Exception fırlasın ya da fırlamasın, catch'e düşsün ya da düşmesin — finally her zaman çalışır.

Bu neden önemli? Kaynakları temizlemek için. Dosya açtıysan kapat, veritabanı bağlantısı aldıysan geri ver, kilit aldıysan bırak.

FileReader reader = null;
try {
    reader = new FileReader("data.txt");
    // Dosyayı oku...
    int data = reader.read();
    System.out.println("Okunan: " + (char) data);
} catch (IOException e) {
    System.out.println("Dosya okunamadı: " + e.getMessage());
} finally {
    // Ne olursa olsun dosyayı kapat
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            System.out.println("Kapatma hatası: " + e.getMessage());
        }
    }
    System.out.println("finally bloğu çalıştı");
}

Gördüğün gibi, finally içinde bile try-catch yazmak gerekebiliyor. Bu biraz çirkin, değil mi? Birazdan göreceğimiz try-with-resources tam da bu sorunu çözüyor.

finally bloğunun davranışı:

// Senaryo 1: Hata yok
try {
    System.out.println("try çalıştı");
} finally {
    System.out.println("finally çalıştı"); // Çalışır
}

// Senaryo 2: Hata var, catch var
try {
    throw new RuntimeException("Boom!");
} catch (RuntimeException e) {
    System.out.println("catch çalıştı");
} finally {
    System.out.println("finally çalıştı"); // Çalışır
}

// Senaryo 3: Hata var, catch YOK
try {
    throw new RuntimeException("Boom!");
} finally {
    System.out.println("finally çalıştı"); // YİNE çalışır!
    // Exception yukarı fırlatılmaya devam eder
}

⚠️ finally ve return: Eğer try ve finally bloklarının ikisi de return içeriyorsa, finally'deki return kazanır. Bu çok kafa karıştırıcı bir davranıştır — finally içinde return yazmaktan kaçın.

public static int confusing() {
    try {
        return 1;
    } finally {
        return 2; // Bu kazanır! Method 2 döner.
    }
}

try-with-resources (Java 7+)

Bu özellik hayat kurtarıcı. AutoCloseable interface'ini implement eden kaynakları otomatik kapatır. O çirkin finally-içinde-try-catch dansından kurtulursun.

// ESKİ YÖNTEM (Java 7 öncesi)
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("data.txt"));
    String line = br.readLine();
    System.out.println(line);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (br != null) {
        try { br.close(); } catch (IOException e) { e.printStackTrace(); }
    }
}

// YENİ YÖNTEM (try-with-resources)
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    String line = br.readLine();
    System.out.println(line);
} catch (IOException e) {
    e.printStackTrace();
}
// br otomatik kapatılır — finally yazmana gerek yok!

Farkı görüyor musun? Kod yarı yarıya kısaldı ve daha okunabilir oldu. try parantezinin içinde tanımlanan kaynak, blok bittiğinde otomatik olarak close() method'u çağrılarak kapatılır.

Birden fazla kaynak da açabilirsin:

try (
    FileInputStream fis = new FileInputStream("input.txt");
    FileOutputStream fos = new FileOutputStream("output.txt");
    BufferedReader reader = new BufferedReader(new InputStreamReader(fis))
) {
    String line;
    while ((line = reader.readLine()) != null) {
        fos.write(line.getBytes());
        fos.write('\n');
    }
} catch (IOException e) {
    System.out.println("IO Hatası: " + e.getMessage());
}
// Tüm kaynaklar otomatik kapatılır — ters sırada (reader, fos, fis)

💡 İpucu: Kaynaklar tanımlanma sırasının tersinde kapatılır. Son açılan ilk kapatılır (LIFO). Bu, bağımlılık sorunlarını önler.

Java 9'la birlikte, daha önce tanımlanmış effectively final değişkenleri de try-with-resources'ta kullanabilirsin:

// Java 9+
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
try (br) { // Önceden tanımlanmış değişken
    System.out.println(br.readLine());
}

Kendi AutoCloseable Sınıfını Yazma

Sadece JDK sınıfları değil, kendi sınıflarını da try-with-resources ile kullanabilirsin. Tek yapman gereken AutoCloseable interface'ini implement etmek:

public class DatabaseConnection implements AutoCloseable {
    private String url;
    
    public DatabaseConnection(String url) {
        this.url = url;
        System.out.println("Bağlantı açıldı: " + url);
    }
    
    public void query(String sql) {
        System.out.println("Sorgu çalıştırılıyor: " + sql);
    }
    
    @Override
    public void close() {
        System.out.println("Bağlantı kapatıldı: " + url);
    }
}

Kullanımı:

try (DatabaseConnection db = new DatabaseConnection("jdbc:mysql://localhost/test")) {
    db.query("SELECT * FROM users");
    db.query("SELECT * FROM orders");
} // close() otomatik çağrılır

Suppressed Exceptions

try-with-resources'ta ilginç bir durum var. Ya try bloğu da exception fırlatırsa, close() da exception fırlatırsa? İkisi birden olamaz — Java birini ana exception, diğerini suppressed exception yapar.

public class FaultyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        throw new Exception("close() hatası");
    }
}

try (FaultyResource r = new FaultyResource()) {
    throw new Exception("try bloğu hatası");
} catch (Exception e) {
    System.out.println("Ana: " + e.getMessage());      // "try bloğu hatası"
    for (Throwable s : e.getSuppressed()) {
        System.out.println("Suppressed: " + s.getMessage()); // "close() hatası"
    }
}

try bloğundaki exception önceliklidir. close()'tan gelen exception suppressed olarak eklenir. getSuppressed() ile bunlara erişebilirsin.

try-catch-finally Kombinasyonları

Java'da bu blokların birkaç geçerli kombinasyonu var:

// 1. try-catch
try { } catch (Exception e) { }

// 2. try-finally (catch olmadan da olabilir)
try { } finally { }

// 3. try-catch-finally (tam combo)
try { } catch (Exception e) { } finally { }

// 4. try-with-resources (catch ve finally opsiyonel)
try (Resource r = new Resource()) { }

// 5. try-with-resources + catch + finally
try (Resource r = new Resource()) {
    // ...
} catch (Exception e) {
    // ...
} finally {
    // ...
}

⚠️ Önemli: Sadece try { } yazmak geçersizdir. En az bir catch veya finally bloğu olmalıdır.

Nested try-catch

try-catch blokları iç içe olabilir. Ama genellikle bu, kodu karmaşıklaştırır:

try {
    System.out.println("Dış try");
    try {
        int result = 10 / 0;
    } catch (ArithmeticException e) {
        System.out.println("İç catch: " + e.getMessage());
    }
    System.out.println("Dış try devam ediyor");
} catch (Exception e) {
    System.out.println("Dış catch: " + e.getMessage());
}

İç try-catch kendi hatasını yakalarsa, dış blok etkilenmez. Ama iç catch hatayı yakalamazsa (ya da yeniden fırlatırsa), exception dış catch'e yükselir.

Gerçek Dünya Örneği: Dosya İşleme

Öğrendiklerimizi birleştirelim:

public class FileProcessor {
    
    public static List<String> readLines(String path) {
        List<String> lines = new ArrayList<>();
        
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            String line;
            while ((line = reader.readLine()) != null) {
                lines.add(line.trim());
            }
        } catch (FileNotFoundException e) {
            System.err.println("Dosya bulunamadı: " + path);
        } catch (IOException e) {
            System.err.println("Okuma hatası: " + e.getMessage());
        }
        
        return lines;
    }
    
    public static void main(String[] args) {
        List<String> data = readLines("config.txt");
        System.out.println("Okunan satır sayısı: " + data.size());
    }
}

Bu örnekte:

  • try-with-resources ile dosya otomatik kapatılır

  • FileNotFoundException ve IOException ayrı ayrı yakalanır (özelden genele)

  • Dosya bulunamazsa boş liste döner — program çökmez

Gerçek Dünya Örneği: Kullanıcı Girişi Doğrulama

public class InputValidator {
    
    public static int getAge(String input) {
        try {
            int age = Integer.parseInt(input);
            if (age < 0 || age > 150) {
                throw new IllegalArgumentException("Yaş 0-150 arasında olmalı");
            }
            return age;
        } catch (NumberFormatException e) {
            System.out.println("Geçersiz sayı formatı: " + input);
            return -1;
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
            return -1;
        }
    }
    
    public static void main(String[] args) {
        System.out.println(getAge("25"));    // 25
        System.out.println(getAge("abc"));   // -1
        System.out.println(getAge("200"));   // -1
    }
}

Performans Notu

Exception handling'in bir maliyeti var mı? Evet, ama düşündüğünden farklı:

  • try bloğuna girmek neredeyse sıfır maliyet. JVM bunu optimize eder.

  • Maliyet, exception fırladığında oluşur. Stack trace oluşturmak pahalı bir işlemdir.

  • Bu yüzden exception'ları kontrol akışı için kullanma. "Eğer dosya varsa aç" diye kontrol et, "aç, hata verirse yakala" yapma (mümkünse).

// KÖTÜ — exception'ı kontrol akışı olarak kullanıyor
try {
    int value = map.get(key);
} catch (NullPointerException e) {
    value = defaultValue;
}

// İYİ — önce kontrol et
int value = map.containsKey(key) ? map.get(key) : defaultValue;

// DAHA İYİ — Java 8+
int value = map.getOrDefault(key, defaultValue);

Özet

  • try-catch programın çökmesini engeller, hataları kontrollü şekilde yakalar

  • catch blokları özelden genele sıralanmalı — üst sınıf exception'ı önce yazma

  • Multi-catch (Java 7+): catch (IOException | ParseException e) ile tekrarı önle

  • finally bloğu ne olursa olsun çalışır — kaynak temizliği için ideal, ama return yazma

  • try-with-resources (Java 7+) AutoCloseable kaynakları otomatik kapatır — en temiz yöntem

  • Exception handling'in maliyeti try'a girmekte değil, exception fırlatmakta — kontrol akışı için kullanma