← Kursa Dön
📄 Text · 12 min

throw ve throws

Şimdiye kadar exception yakalamayı öğrendin. Ama ya senin kodun bir sorun tespit ederse? O zaman kendin exception fırlatırsın. Ve eğer bir method exception fırlatabiliyorsa, bunu bildirmesi gerekir. İşte throw ve throws burada devreye girer.

İkisini karıştırmak çok kolay ama rolleri çok farklı:

  • throw → Exception nesnesini fırlatır (eylem)

  • throws → Method'un exception fırlatabileceğini bildirir (deklarasyon)

Posta analojisiyle düşün: throw postaya mektup atmak, throws ise kapıdaki "dikkat, köpek var" tabelası.

throw: Exception Fırlatma

throw keyword'ü ile bir exception nesnesi fırlatırsın. O andan itibaren normal akış durur ve JVM en yakın uygun catch'i arar.

public static int divide(int a, int b) {
    if (b == 0) {
        throw new ArithmeticException("Sıfıra bölme yapılamaz");
    }
    return a / b;
}

public static void main(String[] args) {
    try {
        int result = divide(10, 0);
    } catch (ArithmeticException e) {
        System.out.println(e.getMessage()); // "Sıfıra bölme yapılamaz"
    }
}

throw ile fırlattığın şey bir nesne olmalı. throw new ...() kalıbını çok göreceksin. Şunu yazamazsın: throw "hata oldu" — String throwable değil.

// throw kullanım kuralları
throw new NullPointerException("değer null olamaz");  // OK
throw new RuntimeException("genel hata");              // OK

Exception e = new IOException("dosya hatası");
throw e;  // Önceden oluşturulmuş nesne de olabilir

// throw "hata";        // COMPILE ERROR — String throwable değil
// throw 404;           // COMPILE ERROR — int throwable değil

Validasyon İçin throw

En yaygın kullanım: method parametrelerini kontrol etmek. Geçersiz değer geldiğinde hemen hata fırlatırsın — fail-fast prensibi.

public class User {
    private String name;
    private int age;
    
    public User(String name, int age) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("İsim boş olamaz");
        }
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Yaş 0-150 arasında olmalı: " + age);
        }
        this.name = name;
        this.age = age;
    }
    
    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Negatif yaş: " + age);
        }
        this.age = age;
    }
}

💡 İpucu: IllegalArgumentException — method'a geçersiz argüman geldi. IllegalStateException — nesne doğru durumda değil. Bu ikisi validasyon için en çok kullandığın unchecked exception'lardır.

public class BankAccount {
    private double balance;
    
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Çekim miktarı pozitif olmalı");
        }
        if (amount > balance) {
            throw new IllegalStateException("Yetersiz bakiye. Mevcut: " + balance);
        }
        balance -= amount;
    }
}

throws: Method Deklarasyonunda Bildirim

throws keyword'ü method imzasında kullanılır. "Bu method şu exception'ları fırlatabilir" diye bildirir.

Checked exception fırlatan method'lar bunu bildirmek zorunda. Unchecked (RuntimeException) için zorunlu değil ama belgelemek amacıyla yazılabilir.

// Checked exception — throws ZORUNLU
public void readFile(String path) throws IOException {
    BufferedReader reader = new BufferedReader(new FileReader(path));
    String line = reader.readLine();
    reader.close();
}

// Birden fazla exception
public void processData(String path) throws IOException, ParseException {
    // Dosya oku + parse et
}

// Unchecked exception — throws yazmak opsiyonel
public int divide(int a, int b) { // throws ArithmeticException yazmana gerek yok
    return a / b;
}

throws ile bildirilen method'u çağıran kod ya try-catch ile yakalamalı ya da kendisi de throws ile yukarı bildirmelidir:

// Seçenek 1: Yakala
public void processFile() {
    try {
        readFile("data.txt");
    } catch (IOException e) {
        System.out.println("Dosya hatası: " + e.getMessage());
    }
}

// Seçenek 2: Yukarı bildir
public void processFile() throws IOException {
    readFile("data.txt");
    // IOException'ı biz yakalamıyoruz, çağırana bırakıyoruz
}

⚠️ throws zinciri: Eğer herkes throws ile yukarı bildirirse, sonunda main method'a kadar çıkar. main de bildirirse, JVM default exception handler'a düşer (program çöker, stack trace basılır).

public static void main(String[] args) throws IOException {
    readFile("data.txt");
    // IOException olursa program çöker — kötü kullanıcı deneyimi
}

throw ve throws Birlikte

Tipik senaryo: method hem kendi exception'ını fırlatır, hem de bunu bildirir.

public class FileValidator {
    
    public static void validateFile(String path) throws IOException {
        if (path == null || path.isEmpty()) {
            throw new IllegalArgumentException("Dosya yolu boş olamaz");
            // Bu unchecked — throws'a eklemeye gerek yok
        }
        
        File file = new File(path);
        if (!file.exists()) {
            throw new FileNotFoundException("Dosya bulunamadı: " + path);
            // Bu checked — throws IOException ile kapsanıyor
            // (FileNotFoundException extends IOException)
        }
        
        if (!file.canRead()) {
            throw new IOException("Dosya okunamıyor: " + path);
        }
        
        System.out.println("Dosya geçerli: " + path);
    }
}

Custom Exception Yazma

JDK'daki exception'lar bazen yeterli gelmez. Kendi iş mantığına özel exception'lar yazman gerekir. Çok basit:

// Checked custom exception
public class InsufficientFundsException extends Exception {
    private final double amount;
    private final double balance;
    
    public InsufficientFundsException(double amount, double balance) {
        super(String.format("%.2f çekilemez, bakiye: %.2f", amount, balance));
        this.amount = amount;
        this.balance = balance;
    }
    
    public double getAmount() { return amount; }
    public double getBalance() { return balance; }
}
// Unchecked custom exception
public class UserNotFoundException extends RuntimeException {
    private final long userId;
    
    public UserNotFoundException(long userId) {
        super("Kullanıcı bulunamadı: ID=" + userId);
        this.userId = userId;
    }
    
    public long getUserId() { return userId; }
}

Kullanımı:

public class BankService {
    
    public void withdraw(Account account, double amount) 
            throws InsufficientFundsException {
        if (amount > account.getBalance()) {
            throw new InsufficientFundsException(amount, account.getBalance());
        }
        account.setBalance(account.getBalance() - amount);
    }
}

public class UserService {
    
    public User findById(long id) {
        User user = database.find(id);
        if (user == null) {
            throw new UserNotFoundException(id); // Unchecked — throws gerekmez
        }
        return user;
    }
}

Checked mı unchecked mı yapayım? Genel kural:

  • Çağıranın mantıklı bir kurtarma yolu varsa → checked

  • Programcı hatası veya kurtarılamaz durum → unchecked

  • Şüpheye düşersen → modern Java dünyası unchecked'ı tercih eder

Custom Exception İçin 4 Constructor Pattern

Standart exception sınıflarının 4 constructor'ı var. Kendi exception'larında da bunları sağlamak iyi pratik:

public class AppException extends RuntimeException {
    
    // 1. Parametresiz
    public AppException() {
        super();
    }
    
    // 2. Sadece mesaj
    public AppException(String message) {
        super(message);
    }
    
    // 3. Mesaj + cause
    public AppException(String message, Throwable cause) {
        super(message, cause);
    }
    
    // 4. Sadece cause
    public AppException(Throwable cause) {
        super(cause);
    }
}

En azından 2. ve 3. constructor'ları mutlaka yaz. cause parametresi "exception chaining" için kritik — birazdan göreceğiz.

Exception Chaining (Zincirleme)

Bazen bir exception'ı yakalayıp, başka bir exception olarak yeniden fırlatırsın. Ama orijinal hatayı kaybetmemek istersin. İşte cause burada devreye girer.

public class DataService {
    
    public List<User> getUsers() {
        try {
            return database.query("SELECT * FROM users");
        } catch (SQLException e) {
            // Orijinal exception'ı cause olarak ekliyoruz
            throw new DataAccessException("Kullanıcılar alınamadı", e);
        }
    }
}
// Chaining olmadan — orijinal hata kaybolur!
catch (SQLException e) {
    throw new DataAccessException("Hata oluştu"); // e nereye gitti?
}

// Chaining ile — orijinal hata korunur
catch (SQLException e) {
    throw new DataAccessException("Hata oluştu", e); // e cause olarak saklanır
}

Yakalayan tarafta orijinal hataya erişebilirsin:

try {
    dataService.getUsers();
} catch (DataAccessException e) {
    System.out.println("Üst hata: " + e.getMessage());
    System.out.println("Asıl sebep: " + e.getCause().getMessage());
    e.printStackTrace(); // Tüm zinciri gösterir
}

Stack trace çıktısı şöyle görünür:

DataAccessException: Kullanıcılar alınamadı
    at DataService.getUsers(DataService.java:12)
    at Main.main(Main.java:5)
Caused by: java.sql.SQLException: Connection refused
    at Database.query(Database.java:45)
    at DataService.getUsers(DataService.java:10)
    ... 1 more

💡 İpucu: "Caused by" kısmı debug yaparken altın değerinde. Exception chaining yapmazsan bu bilgi kaybolur ve hata bulmak çok zorlaşır.

initCause() ile Sonradan Ekleme

Bazı eski exception sınıflarında cause parametreli constructor yoktur. Bu durumda initCause() kullanabilirsin:

try {
    // ...
} catch (IOException e) {
    NoSuchElementException nse = new NoSuchElementException("Eleman bulunamadı");
    nse.initCause(e); // Sonradan cause ekleme
    throw nse;
}

Ama modern kodda genellikle constructor ile geçirmeyi tercih et.

Re-throw: Yakalayıp Tekrar Fırlatma

Bazen exception'ı yakalar, bir şey yaparsın (loglama gibi), sonra tekrar fırlatırsın:

public void process() throws IOException {
    try {
        readData();
    } catch (IOException e) {
        logger.error("Veri okunamadı", e);
        throw e; // Aynı exception'ı tekrar fırlat
    }
}

Java 7+ ile compiler daha akıllıdır — re-throw edilen exception'ın gerçek türünü tanır:

public void handle() throws FileNotFoundException, ParseException {
    try {
        // FileNotFoundException veya ParseException fırlatabilir
        openAndParse();
    } catch (Exception e) {
        logger.error("Hata", e);
        throw e; // Compiler bilir ki e sadece FileNotFoundException veya ParseException olabilir
        // throws'a Exception yazmana gerek yok
    }
}

throws ve Kalıtım

Override edilen method'larda throws kuralları var:

class Parent {
    public void doSomething() throws IOException {
        // ...
    }
}

class Child extends Parent {
    // OK — aynı exception
    @Override
    public void doSomething() throws IOException { }
    
    // OK — daha spesifik exception
    // public void doSomething() throws FileNotFoundException { }
    
    // OK — hiç exception bildirmemek
    // public void doSomething() { }
    
    // HATA — daha geniş exception bildiremezsin
    // public void doSomething() throws Exception { } // Compile error!
}

⚠️ Kural: Override eden method, üst sınıf method'undan daha geniş checked exception bildiremez. Aynı veya daha dar olmalı. Unchecked exception'lar bu kuraldan muaf.

Bu kural Liskov Substitution Principle ile ilgili. Eğer alt sınıf daha fazla exception fırlatabilseydi, üst sınıf referansıyla çalışan kod beklenmedik exception'larla karşılaşırdı.

Gerçek Dünya: Katmanlı Mimari

Büyük projelerde exception'lar katmanlar arasında dönüştürülür:

// Repository katmanı — veritabanı exception'ları
public class UserRepository {
    public User findById(long id) throws SQLException {
        // JDBC kodu
        ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE id = " + id);
        if (!rs.next()) {
            return null;
        }
        return mapToUser(rs);
    }
}

// Service katmanı — iş mantığı exception'ları
public class UserService {
    private final UserRepository repo;
    
    public User getUser(long id) {
        try {
            User user = repo.findById(id);
            if (user == null) {
                throw new UserNotFoundException(id);
            }
            return user;
        } catch (SQLException e) {
            throw new DataAccessException("Kullanıcı sorgulanamadı", e);
        }
    }
}

// Controller katmanı — HTTP response'a dönüştür
public class UserController {
    private final UserService service;
    
    public Response getUser(long id) {
        try {
            User user = service.getUser(id);
            return Response.ok(user);
        } catch (UserNotFoundException e) {
            return Response.notFound(e.getMessage());
        } catch (DataAccessException e) {
            return Response.serverError("Sunucu hatası");
        }
    }
}

Her katman kendi seviyesine uygun exception kullanır. Alt katmanın teknik detayları üst katmana sızmaz. Bu abstraction prensibidir.

Exception Factory Pattern

Çok sayıda benzer exception fırlatıyorsan, factory method'lar temiz bir çözüm:

public class Exceptions {
    
    public static UserNotFoundException userNotFound(long id) {
        return new UserNotFoundException("Kullanıcı bulunamadı: " + id);
    }
    
    public static ValidationException invalidField(String field, Object value) {
        return new ValidationException(
            String.format("Geçersiz %s değeri: %s", field, value)
        );
    }
    
    public static PermissionException accessDenied(String resource) {
        return new PermissionException("Erişim reddedildi: " + resource);
    }
}

// Kullanım — temiz ve okunabilir
throw Exceptions.userNotFound(42);
throw Exceptions.invalidField("email", "not-an-email");
throw Exceptions.accessDenied("/admin/users");

Özet

  • throw exception nesnesi fırlatır — throw new XException("mesaj") — o andan itibaren normal akış durur

  • throws method imzasında bildirim yapar — checked exception fırlatan method'lar bunu yazmak zorunda

  • Custom exception yazmak basit: Exception veya RuntimeException'dan extend et, en az mesajlı ve cause'lu constructor'ları ekle

  • Exception chaining orijinal hatayı kaybetmemek için kritik — her zaman cause parametresini geçir

  • Override kuralı: Alt sınıf, üst sınıftan daha geniş checked exception bildiremez

  • Katmanlı mimaride her katman kendi seviyesine uygun exception kullanır — alt katman detayları yukarı sızmamalı