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.
ifile 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 gidersinSorunu 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:
Main.main(satır 12) →UserController.createUser'ı çağırmışUserController.createUser(satır 28) →UserService.formatName'i çağırmışUserService.formatName(satır 42) →StringUtils.capitalize'ı çağırmışStringUtils.capitalize(satır 15) → Burada patlama olmuş!strnull'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 moreKural: 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:
En alttaki Caused by'a bak — root cause budur
Exception mesajını oku — genelde sorunu açıkça söyler
Kendi kodunun satır numaralarına bak — framework satırlarını atla
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) ← BURASIRemote 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.jarBu parametreler ne yapıyor:
transport=dt_socket: TCP soket üzerinden debug bağlantısıserver=y: JVM debug sunucusu olarak beklersuspend=n: Debugger bağlanmadan da çalışır (yyapsan 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 nullBu 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ı:
Stack trace'in üst satırı:
OrderService.calculateTotal, satır 20.p.pricenull.Soru: Hangi ürünün price'ı null? Stack trace bunu söylemez.
Breakpoint koy:
total += p.price;satırına.Conditional breakpoint:
p.price == nullkoşuluyla.Debug modda çalıştır → tam "Mouse" ürününde duracak.
Çözüm: Ya null kontrolü ekle, ya da
Doubleyerinedouble(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 ister6. 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+F8 | Breakpoint ekle/kaldır |
Shift+F9 | Debug modda çalıştır |
F8 | Step Over |
F7 | Step Into |
Shift+F8 | Step Out |
Alt+F9 | Run to Cursor |
F9 | Resume (devam et) |
Alt+F8 | Evaluate Expression |
Ctrl+Shift+F8 | Breakpoint 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 yaparDeadlock 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,
==vsequals) 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.
AI Asistan
Sorularını yanıtlamaya hazır