← Kursa Dön
📄 Text · 15 min

Debugging Teknikleri

Kod yazarsın, çalıştırırsın, beklediğin sonuç gelmez. "Ama bu çalışmalıydı!" dersin. Ekrana birkaç System.out.println serpiştirir, tekrar çalıştırırsın. Bazen sorunu bulursun — çoğu zaman daha da kafan karışır. Tanıdık geldi mi?

Debugging, yazılım geliştirmenin en çok zaman harcanan kısmıdır. Araştırmalar, geliştiricilerin zamanlarının %30-50'sini bug bulmak ve düzeltmekle geçirdiğini gösteriyor. Bu kadar zaman harcanan bir iş için doğru araçları bilmek, kariyerinin en karlı yatırımlarından biri.

Debugging'i dedektiflik gibi düşün. Bir cinayet masası dedektifi olay yerine geldiğinde rastgele ortalığı araştırmaz. Delilleri toplar, tanıkları dinler, timeline oluşturur, hipotez kurar ve test eder. İyi bir debugger da aynısını yapar: hatanın semptomlarını gözlemler, log ve stack trace'lerden ipuçları toplar, hipotez kurar, breakpoint koyar ve adım adım doğrular.

Bu derste println debugging'den profesyonel araçlara geçişi yapacaksın. IDE debugger'ın gücünü keşfedecek, stack trace okumayı öğrenecek ve en sık karşılaşılan bug kalıplarını tanıyacaksın.


System.out Debugging: Neden Yetersiz?

Herkesin ilk debugging yöntemi System.out.println'dir. Ve küçük programlarda gerçekten işe yarar. Ama ciddiye bindiğinde sorunlar başlar.

public double calculateDiscount(Order order) {
    System.out.println("order: " + order);
    System.out.println("items size: " + order.getItems().size());
    double total = 0;
    for (Item item : order.getItems()) {
        System.out.println("item: " + item.getName() + " price: " + item.getPrice());
        total += item.getPrice();
        System.out.println("running total: " + total);
    }
    double discount = total > 100 ? total * 0.1 : 0;
    System.out.println("discount: " + discount);
    return discount;
}

Bu kodda beş tane println var — tek bir method için. Bunu 20 method'a yayınca konsol çöplüğe dönüyor. Hangisi hangi method'dan geldi? Hangi thread'den? Hangi çağrıda? Hiçbir fikrin yok.

println debugging'in temel sorunları:

  • Yeniden derleme gerektirir. Her println ekleyip çıkardığında kodu yeniden compile edip çalıştırman gerekir. Büyük projelerde bu dakikalar sürer.

  • Kirli kod bırakır. Bazen println'leri silmeyi unutursun, production'a kadar gider. Ya da "bir daha lazım olur" diye yorum satırı yaparsın — kod çöplüğe döner.

  • Dinamik inceleme yapamaz. Bir değişkenin değerini görmek istiyorsun ama hangi noktada bakacağını bilmiyorsun. Her yere println koymak mümkün değil. Debugger'da ise istediğin anda istediğin değişkeni inceleyebilirsin.

  • Koşullu gözlem yok. "Sadece userId=42 olan kullanıcıda bu sorunu görmek istiyorum" diyemezsin — println herkesi basar. if ile sararsan kod daha da kirlenmiş olur.

  • Thread-safe değil. Multi-threaded uygulamalarda farklı thread'lerin println çıktıları birbirine karışır. Hangi satır hangi thread'e ait anlaşılmaz.

println kullanacaksan bile en azından şunu yap:

System.out.printf("[DEBUG][%s][%s] discount: %.2f%n",
    Thread.currentThread().getName(),
    getClass().getSimpleName(),
    discount);

Ama daha iyisi var: IDE debugger.


IDE Debugger Temelleri

Modern IDE'ler (IntelliJ IDEA, Eclipse, VS Code) güçlü debugger araçları içerir. Debugger, programını kontrollü bir şekilde çalıştırmanı sağlar: istediğin yerde durur, değişkenleri incelersin, adım adım ilerlersin.

Breakpoint

Breakpoint, "burada dur" demektir. Satır numarasının yanına tıklarsın, kırmızı bir nokta belirir. Program o satıra geldiğinde çalışmayı duraklatır — tamamen durdurmaz, sadece bekler. Sen inceleme yaptıktan sonra devam etmesini söylersin.

IntelliJ'de breakpoint koymak: satır numarasının solundaki boşluğa tıkla (ya da Ctrl+F8).

public User findUser(long id) {
    User user = userRepository.findById(id);  // ← buraya breakpoint koy
    if (user == null) {
        throw new UserNotFoundException("User not found: " + id);
    }
    return user;
}

Breakpoint koyduğun satıra program geldiğinde, o anda bellekteki tüm değişkenleri görebilirsin: id kaç, user null mı değil mi, userRepository içinde ne var... Her şey.

Conditional Breakpoint

Normal breakpoint her seferinde durur. Ama sen sadece belirli bir koşulda durmak istiyorsan? Mesela sadece id == 42 olduğunda?

IntelliJ'de breakpoint'e sağ tıkla → Condition alanına id == 42 yaz. Artık program sadece o koşul sağlandığında duracak. Bu, döngülerde hayat kurtarır — 10.000 iterasyonun hepsinde durmak yerine sadece sorunlu olan iterasyonda durursun.

for (Order order : orders) {
    processOrder(order);  // Conditional breakpoint: order.getId() == 1057
}

Watch ve Evaluate

Debugger durduktan sonra iki güçlü aracın var:

Watch: Bir ifadeyi sürekli izlersin. Mesela user.getOrders().size() — her adımda güncel değerini görürsün. IntelliJ'de Variables panelinde "+" butonuyla ya da Ctrl+Shift+F8 ile watch eklersin.

Evaluate Expression (Alt+F8): Anlık olarak herhangi bir Java ifadesini çalıştırabilirsin. Değişkenin bir method'unu çağırabilir, karşılaştırma yapabilir, hatta yeni nesne oluşturabilirsin. Bu müthiş güçlü — "acaba bu null mı?" diye merak etmek yerine, debugger'da doğrudan test edersin.

// Evaluate Expression örnekleri (debugger durmuşken):
user.getEmail()                    → "ali@example.com"
order.getItems().stream().count()  → 3
user != null && user.isActive()    → true
new SimpleDateFormat("yyyy-MM-dd").format(new Date()) → "2024-03-15"

⚠️ Dikkat: Evaluate Expression yan etki yaratabilir! Eğer user.delete() gibi bir şey evaluate edersen, gerçekten silinir. Sadece okuma amaçlı ifadeler kullan — ya da ne yaptığını çok iyi bil.


Adım Adım Çalıştırma

Breakpoint'te durduktan sonra programı adım adım ilerletebilirsin. Dört temel adım türü var ve her birini bilmek şart.

Step Over (F8)

Mevcut satırı çalıştır, bir sonraki satıra geç. Method çağrısı varsa method'un içine girmez — çağırır, sonucunu alır, devam eder.

public void processOrder(Order order) {
    double total = calculateTotal(order);   // ← Step Over: calculateTotal çalışır,
                                            //   ama içine girmezsin. total'a değer atanır.
    double tax = total * 0.18;              // ← Buraya gelirsin
    order.setFinalPrice(total + tax);
}

Step Over en çok kullanılan adımdır. Bir method'un doğru çalıştığından eminsen, içine girip zaman kaybetme.

Step Into (F7)

Mevcut satırdaki method çağrısının içine girer. Method'un ilk satırına atlarsın.

double total = calculateTotal(order);  // ← Step Into: calculateTotal'ın
                                       //   ilk satırına gidersin

Sorunu hangi method'da olduğunu bilmediğinde Step Into kullanırsın. "Acaba calculateTotal içinde mi hata?" diye düşündüğünde, içine girip satır satır incelersin.

Step Out (Shift+F8)

Şu anda bulunduğun method'u tamamla ve çağıran method'a geri dön. Bir method'a Step Into yaptın ama sorunun burada olmadığını anladın — Step Out ile hızla geri çıkarsın.

Step Return / Run to Cursor

IntelliJ'de Run to Cursor (Alt+F9) çok kullanışlıdır: imlecin bulunduğu satıra kadar çalıştır, orada dur. Breakpoint koymana gerek yok — geçici bir "buraya kadar gel" komutu.

public void processOrders(List<Order> orders) {
    for (Order order : orders) {
        validate(order);
        calculateTotal(order);
        applyDiscount(order);
        saveOrder(order);        // ← İmleci buraya koy, Alt+F9 bas.
                                 //   Yukarıdaki satırlar çalışır, burada durur.
    }
}

💡 İpucu: Debugging'de en etkili strateji genellikle şudur: Hatanın oluştuğu yere yakın bir breakpoint koy → Step Over ile ilerle → şüphelendiğin method'a Step Into yap → sorunu bul → Step Out ile geri çık. Bu döngüyü tekrarla.


Stack Trace Okuma

Stack trace, Java'nın sana verdiği en değerli debugging bilgisidir. Bir exception fırlatıldığında, o anda çağrı zincirinin tamamını gösterir — en son çağrılan method'dan başlayarak, programın başladığı yere kadar.

Anatomi

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()"
  because "str" is null
    at com.example.StringUtils.capitalize(StringUtils.java:15)
    at com.example.UserService.formatName(UserService.java:42)
    at com.example.UserController.createUser(UserController.java:28)
    at com.example.Main.main(Main.java:12)

Bu stack trace'i aşağıdan yukarıya oku:

  1. Main.main (satır 12) → UserController.createUser'ı çağırmış

  2. UserController.createUser (satır 28) → UserService.formatName'i çağırmış

  3. UserService.formatName (satır 42) → StringUtils.capitalize'ı çağırmış

  4. StringUtils.capitalize (satır 15) → Burada patlama olmuş! str null'mış ve .length() çağrılmaya çalışılmış.

Kural: Stack trace'in en üst satırı, hatanın gerçekleştiği yer. Ama hatanın sebebi genellikle birkaç satır aşağıda — yani çağrı zincirinin önceki halkalarında.

Bu örnekte StringUtils.capitalize null bir string almış — ama asıl soru şu: Kim gönderdi bu null'u? Cevap bir alt satırda: UserService.formatName. Oraya bakmalısın.

Caused By Zinciri

Bazen bir exception başka bir exception'ı sarar (wrap). Bu durumda Caused by zincirleri oluşur:

Exception in thread "main" com.example.ServiceException: Failed to process user
    at com.example.UserService.process(UserService.java:35)
    at com.example.Main.main(Main.java:10)
Caused by: java.sql.SQLException: Connection refused
    at com.mysql.jdbc.ConnectionImpl.connect(ConnectionImpl.java:456)
    at com.example.Database.getConnection(Database.java:22)
    at com.example.UserService.process(UserService.java:33)
    ... 1 more

Kural: En alttaki "Caused by" asıl sebeptir (root cause). Bu örnekte asıl sorun SQLException: Connection refused — veritabanına bağlanılamamış. Üstteki ServiceException sadece bir sarmalama.

Stack trace okurken şu sırayı takip et:

  1. En alttaki Caused by'a bak — root cause budur

  2. Exception mesajını oku — genelde sorunu açıkça söyler

  3. Kendi kodunun satır numaralarına bak — framework satırlarını atla

  4. O satıra git, breakpoint koy, debugger'da incele

"at" Satırlarını Filtreleme

Gerçek bir Spring uygulamasında stack trace 50-100 satır olabilir. Çoğu framework kodu. Kendi paket adını (mesela com.example) ara, sadece o satırlara odaklan.

// Bunları atla:
at org.springframework.web.servlet.FrameworkServlet.service(...)
at javax.servlet.http.HttpServlet.service(...)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(...)

// Bunlara odaklan:
at com.example.UserService.process(UserService.java:35)  ← BURASI

Remote Debugging

Bazen hata sadece belirli bir ortamda oluşur — yerel makinende değil, staging veya test sunucusunda. Bu durumda remote debugging kullanırsın.

Java uygulamasını debug modda başlatmak için JVM'e özel parametreler verirsin:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 \
     -jar myapp.jar

Bu parametreler ne yapıyor:

  • transport=dt_socket: TCP soket üzerinden debug bağlantısı

  • server=y: JVM debug sunucusu olarak bekler

  • suspend=n: Debugger bağlanmadan da çalışır (y yapsan başlangıçta bekler)

  • address=*:5005: 5005 portunda dinler

IntelliJ'de: Run → Edit Configurations → Remote JVM Debug → Host ve port gir → Debug butonuna bas. Bağlandığında aynen yerel debugger gibi breakpoint koyar, step edersin.

⚠️ Dikkat: Remote debugging'i production'da kullanma. Breakpoint koyduğunda o thread durur — gerçek kullanıcıların istekleri bekler. Sadece test/staging ortamlarında kullan. Production sorunları için logging ve monitoring kullan.


Common Bug Patterns

Yıllar içinde bazı bug'lar o kadar sık tekrarlanır ki kalıplar oluşur. Bunları tanımak, debugging sürecini dramatik şekilde kısaltır.

1. NullPointerException

Java dünyasının en ünlü hatası. Bir referans null olduğunda üzerinde method çağırmaya çalışırsan patlar.

// Hatalı
public String getUserCity(User user) {
    return user.getAddress().getCity();  // address null ise → NPE!
}

// Doğru — null kontrolü
public String getUserCity(User user) {
    if (user == null || user.getAddress() == null) {
        return "Unknown";
    }
    return user.getAddress().getCity();
}

// Daha iyi — Optional kullanımı
public String getUserCity(User user) {
    return Optional.ofNullable(user)
        .map(User::getAddress)
        .map(Address::getCity)
        .orElse("Unknown");
}

Java 14+ ile NPE mesajları çok daha açıklayıcı hale geldi:

// Java 14 öncesi:
NullPointerException

// Java 14+ (Helpful NullPointerExceptions):
NullPointerException: Cannot invoke "Address.getCity()"
  because the return value of "User.getAddress()" is null

Bu mesaj sana tam olarak neyin null olduğunu söylüyor. -XX:+ShowCodeDetailsInExceptionMessages flag'i Java 14'te varsayılan, 15+ sürümlerde her zaman açık.

2. Off-By-One Error

Döngü sınırlarında bir fazla veya bir eksik iterasyon. Klasik hata.

// Hatalı — ArrayIndexOutOfBoundsException
for (int i = 0; i <= array.length; i++) {  // <= yerine < olmalı
    System.out.println(array[i]);
}

// Doğru
for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]);
}

// En doğru — enhanced for loop veya Stream
for (String item : array) {
    System.out.println(item);
}

3. ConcurrentModificationException

Bir koleksiyonu iterate ederken aynı anda değiştirmeye çalışmak.

// Hatalı — ConcurrentModificationException
List<String> names = new ArrayList<>(List.of("Ali", "Veli", "Deli"));
for (String name : names) {
    if (name.startsWith("D")) {
        names.remove(name);  // iterate ederken silme → BOOM!
    }
}

// Doğru — Iterator kullan
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("D")) {
        it.remove();  // Iterator'ın kendi remove method'u
    }
}

// Daha temiz — removeIf
names.removeIf(name -> name.startsWith("D"));

4. String Karşılaştırma Hatası

== referans karşılaştırması yapar, .equals() içerik karşılaştırması. Klasik ama hâlâ insanları düşüren tuzak.

// Hatalı
String input = new String("hello");
if (input == "hello") {        // false! Farklı referanslar
    System.out.println("eşit");
}

// Doğru
if ("hello".equals(input)) {   // true — ve input null olsa bile NPE almaz
    System.out.println("eşit");
}

Neden "hello".equals(input) sırasıyla yazıyoruz? Çünkü input null olabilir. input.equals("hello") NPE fırlatır, ama "hello".equals(null) güvenle false döner.

5. Integer Caching Tuzağı

Integer a = 127;
Integer b = 127;
System.out.println(a == b);  // true — cached!

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false — farklı nesneler!

Java, -128 ile 127 arasındaki Integer'ları cache'ler. Bu aralık dışında her Integer yeni bir nesne oluşturur. == ile referans karşılaştırması yapınca beklenmedik sonuçlar alırsın. Her zaman .equals() kullan.

6. Mutable Default / Shared State

// Hatalı — statik mutable state paylaşımı
public class DateFormatter {
    // SimpleDateFormat thread-safe DEĞİL!
    private static final SimpleDateFormat FORMAT =
        new SimpleDateFormat("yyyy-MM-dd");

    public static String format(Date date) {
        return FORMAT.format(date);  // Multi-thread'de garip sonuçlar!
    }
}

// Doğru — thread-safe alternatif
public class DateFormatter {
    private static final DateTimeFormatter FORMAT =
        DateTimeFormatter.ofPattern("yyyy-MM-dd");  // Immutable, thread-safe

    public static String format(LocalDate date) {
        return date.format(FORMAT);
    }
}

Stack Trace Debugging: Pratik Senaryo

Gerçek bir senaryoda stack trace'den yola çıkarak hatayı bulmayı görelim.

import java.util.List;
import java.util.ArrayList;

class Product {
    String name;
    Double price;

    Product(String name, Double price) {
        this.name = name;
        this.price = price;
    }
}

class OrderService {
    List<Product> cart = new ArrayList<>();

    void addProduct(Product p) {
        cart.add(p);
    }

    double calculateTotal() {
        double total = 0;
        for (Product p : cart) {
            total += p.price;  // Satır 20 — p.price null ise NPE!
        }
        return total;
    }
}

class Main {
    public static void main(String[] args) {
        OrderService service = new OrderService();
        service.addProduct(new Product("Laptop", 15000.0));
        service.addProduct(new Product("Mouse", null));   // price null!
        service.addProduct(new Product("Keyboard", 500.0));

        double total = service.calculateTotal();  // Satır 33
        System.out.println("Total: " + total);
    }
}

Çalıştırınca şu stack trace'i alırsın:

Exception in thread "main" java.lang.NullPointerException:
  Cannot invoke "java.lang.Double.doubleValue()" because "p.price" is null
    at OrderService.calculateTotal(Main.java:20)
    at Main.main(Main.java:33)

Debugging adımları:

  1. Stack trace'in üst satırı: OrderService.calculateTotal, satır 20. p.price null.

  2. Soru: Hangi ürünün price'ı null? Stack trace bunu söylemez.

  3. Breakpoint koy: total += p.price; satırına.

  4. Conditional breakpoint: p.price == null koşuluyla.

  5. Debug modda çalıştır → tam "Mouse" ürününde duracak.

  6. Çözüm: Ya null kontrolü ekle, ya da Double yerine double (primitive) kullan.

// Çözüm 1: Null kontrolü
double calculateTotal() {
    double total = 0;
    for (Product p : cart) {
        if (p.price != null) {
            total += p.price;
        }
    }
    return total;
}

// Çözüm 2: Stream ile
double calculateTotal() {
    return cart.stream()
        .map(p -> p.price)
        .filter(price -> price != null)
        .mapToDouble(Double::doubleValue)
        .sum();
}

Logging ile Debugging Kombinasyonu

Debugger güçlüdür ama her yerde kullanılamaz — özellikle production'da. Bu yüzden logging ve debugging birlikte kullanılır. Logging sorunun genel resmini verir, debugger detaya inmenizi sağlar.

Stratejik Log Noktaları

Her method'un girişini ve çıkışını loglamana gerek yok — bu aşırı gürültü yaratır. Ama kritik noktaları logla:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PaymentService {
    private static final Logger log = LoggerFactory.getLogger(PaymentService.class);

    public PaymentResult processPayment(PaymentRequest request) {
        log.info("Processing payment: userId={}, amount={}, method={}",
            request.getUserId(), request.getAmount(), request.getMethod());

        try {
            // Ödeme gateway'ine istek
            GatewayResponse response = gateway.charge(request);
            log.info("Payment successful: transactionId={}",
                response.getTransactionId());
            return PaymentResult.success(response.getTransactionId());

        } catch (InsufficientFundsException e) {
            log.warn("Insufficient funds: userId={}, amount={}",
                request.getUserId(), request.getAmount());
            return PaymentResult.declined("Insufficient funds");

        } catch (GatewayException e) {
            log.error("Payment gateway error: userId={}, amount={}",
                request.getUserId(), request.getAmount(), e);
            return PaymentResult.error("Gateway unavailable");
        }
    }
}

Dikkat et: log.error çağrısında son parametre olarak exception nesnesini (e) veriyoruz. SLF4J bunu otomatik olarak stack trace ile birlikte loglar. Bu, sorunun tam olarak nerede oluştuğunu görmeni sağlar.

Log Seviyeleri ile Debugging

Log seviyelerini stratejik kullan:

public User findUser(long userId) {
    log.debug("Looking up user: id={}", userId);          // Geliştirme detayı

    User user = cache.get(userId);
    if (user != null) {
        log.debug("User found in cache: id={}", userId);  // Cache hit
        return user;
    }

    log.debug("Cache miss, querying database: id={}", userId);
    user = repository.findById(userId).orElse(null);

    if (user == null) {
        log.warn("User not found: id={}", userId);        // Beklenmedik durum
        return null;
    }

    cache.put(userId, user);
    log.debug("User cached: id={}", userId);
    return user;
}

Production'da log seviyesini INFO yaparsın — debug mesajları görünmez, performans etkilenmez. Sorun olduğunda seviyeyi DEBUG'a çekersin, detaylı bilgi akar. Kodu değiştirmeden, sadece konfigürasyon değişikliğiyle.

Correlation ID ile İzleme

Dağıtık sistemlerde bir isteğin hangi servislerden geçtiğini izlemek için correlation ID kullanılır:

public class RequestContext {
    private static final ThreadLocal<String> CORRELATION_ID = new ThreadLocal<>();

    public static void setCorrelationId(String id) {
        CORRELATION_ID.set(id);
    }

    public static String getCorrelationId() {
        return CORRELATION_ID.get();
    }
}

// Kullanım — her log mesajında correlation ID
log.info("[{}] Processing order: orderId={}",
    RequestContext.getCorrelationId(), orderId);

Böylece bir kullanıcının isteğini tüm loglar arasında filtreleyerek takip edebilirsin: grep "abc-123-def" app.log.


Debugging Best Practices

Yılların deneyimiyle oluşan pratikler:

1. Önce reproduce et. Bug'ı güvenilir şekilde tekrarlayamıyorsan, düzeltemezsin. "Bazen oluyor" en tehlikeli cümledir — oluşma koşullarını netleştir.

2. Değişiklikleri minimumda tut. Bir seferde tek bir şey değiştir. Üç farklı fix'i aynı anda denersen, hangisinin düzelttiğini bilemezsin.

3. Rubber duck debugging. Sorunu birine (ya da lastik ördeğe) sesli olarak anlat. "Burada şu oluyor, sonra bu method çağrılıyor, oradan şu dönüyor..." derken genellikle kendi kendine çözümü bulursun. Beyni farklı bir modda çalıştırır.

4. Binary search debugging. Büyük bir kod bloğunda hatayı ararken, ortaya breakpoint koy. Sorun öncesinde mi sonrasında mı belirle. Sonra o yarıyı ikiye böl. Logaritmik hızla daraltırsın — 1000 satırlık kodda 10 adımda bulursun.

5. Git blame / git bisect kullan. Hata ne zaman girdi? git bisect binary search yaparak hatanın ilk ortaya çıktığı commit'i bulur. git blame bir satırı kimin, ne zaman yazdığını gösterir.

# Hata hangi commit'te girdi?
git bisect start
git bisect bad          # Şu an hata var
git bisect good v1.2.0  # Bu versiyonda yoktu
# Git otomatik olarak commit'leri test etmeni ister

6. Varsayımlarını sorgula. "Bu method kesinlikle null dönemez" → Gerçekten mi? Breakpoint koy, bak. En inatçı bug'lar yanlış varsayımlardan doğar.


IntelliJ IDEA Debugging Kısayolları

Hızlı referans — en çok kullanılan kısayollar:

Kısayolİşlem
Ctrl+F8Breakpoint ekle/kaldır
Shift+F9Debug modda çalıştır
F8Step Over
F7Step Into
Shift+F8Step Out
Alt+F9Run to Cursor
F9Resume (devam et)
Alt+F8Evaluate Expression
Ctrl+Shift+F8Breakpoint listesini aç

Exception Breakpoint

Sadece satır breakpoint'i değil, exception breakpoint de koyabilirsin. Herhangi bir NullPointerException fırlatıldığında — kodun neresinde olursa olsun — debugger durur.

IntelliJ'de: Run → View Breakpoints → "+" → Java Exception Breakpoints → Exception türünü yaz (mesela NullPointerException).

Bu özellikle şu durumda kullanışlıdır: Hata alıyorsun ama stack trace'de gösterilen satır framework kodu. Hangi satırda null referans oluştuğunu bilmiyorsun. Exception breakpoint koyduğunda, exception fırlatıldığı anda debugger durur ve o anki tüm context'i inceleyebilirsin.

Exception breakpoint türleri:
- Any exception: Her exception'da dur
- Caught exception: Yakalanan exception'larda dur
- Uncaught exception: Yakalanmayan exception'larda dur (varsayılan)

Genellikle uncaught exception breakpoint yeterlidir. "Caught" seçersen, try-catch içinde bilerek yakaladığın her exception'da da durur — çok gürültülü olur.


Debugging Multi-threaded Uygulamalar

Multi-threaded debugging, single-threaded'a göre çok daha zordur. Breakpoint koyduğunda varsayılan olarak tüm thread'ler durur. Ama bazen sadece bir thread'i durdurup diğerlerinin çalışmaya devam etmesini istersin.

IntelliJ'de breakpoint'e sağ tıkla → Suspend ayarını Thread olarak değiştir (varsayılan "All"). Böylece sadece o breakpoint'e gelen thread durur, diğerleri çalışmaya devam eder.

Threads paneli debugger'da tüm thread'leri listeler. Her birinin o anki stack trace'ini görebilirsin. "main" thread'i, "pool-1-thread-3" gibi isimlerle hangi thread'in nerede olduğunu takip edersin.

// Deadlock tespiti için thread dump
// IntelliJ'de debug sırasında: Run → Dump Threads
// Komut satırından:
// jstack <pid>    → Tüm thread'lerin stack trace'ini basar
// jcmd <pid> Thread.print → Aynı işi yapar

Deadlock durumunda jstack çıktısında "Found one Java-level deadlock" mesajını görürsün. Hangi thread'in hangi lock'u beklediğini açıkça gösterir.


Özet

  • System.out.println hızlı ama kirli bir yöntem — gerçek projeler için IDE debugger kullan.

  • Breakpoint koy, conditional breakpoint ile filtreleme yap, Step Over/Into/Out ile adım adım ilerle.

  • Stack trace okumayı öğren: en üst satır hatanın yeri, en alttaki "Caused by" asıl sebep, kendi paket adını filtrele.

  • Common bug patterns (NPE, off-by-one, ConcurrentModificationException, == vs equals) tanı — çoğu hatayı kalıp tanıyarak saniyeler içinde bulursun.

  • Logging ve debugging birlikte kullan: logging genel resmi verir, debugger detaya indirir. Stratejik log noktaları koy.

  • Varsayımlarını sorgula, değişiklikleri minimumda tut, rubber duck debugging dene — debugging bir araç becerisi kadar bir düşünce disiplinidir.