Du kennst das: Irgendwas funktioniert nicht, du hast keinen Plan warum, und der schnellste Griff geht zu System.out.println(). Einmal rauswerfen, nochmal laufen lassen, Ausgabe anschauen - fertig. Das fühlt sich an wie Debugging, ist es aber nicht. Es ist eher eine improvisierte Spurensuche mit Taschenlampe, während du eigentlich ein komplettes Werkzeugset neben dir liegen hast.

System.out.println() ist nicht verboten. Es ist nur ein sehr stumpfes Werkzeug, das dich schnell an Grenzen bringt - und dir im schlechtesten Fall neue Probleme einbaut, die vorher gar nicht da waren.

 

Was System.out.println() wirklich macht

System.out.println() schreibt Text nach stdout. Mehr nicht. Du drückst damit Informationen in eine Ausgabe, die du irgendwo abgreifst: IDE-Konsole, Server-Log, Docker-Log, Terminal.

Das Entscheidende: Du bekommst nur das, was du vorher manuell formulierst. Und zwar zu einem Zeitpunkt, den du ebenfalls manuell festlegst. Wenn du an der falschen Stelle druckst, siehst du die falschen Werte. Wenn du die falschen Werte druckst, interpretierst du die falschen Ursachen. Und wenn du nebenbei den Programmfluss veränderst, jagst du einem Bug hinterher, den du selbst erzeugt hast.

Für ganz einfache Fälle kann das reichen. Beispiel: Du willst schnell sehen, ob eine Methode überhaupt aufgerufen wird.

System.out.println("save() reached");

Das Problem ist: Sobald du mehr willst als "wurde erreicht?", fängt das Ding an zu zerbröseln. Dann wird aus einem Print ein Dutzend Prints, aus einem Dutzend werden 50, und plötzlich ist deine Konsole ein Wasserfall aus Text, in dem du die wichtige Zeile suchst.

 

Warum Prints dich beim Debuggen aktiv behindern

Der größte Nachteil ist nicht, dass es "unsauber" aussieht. Der größte Nachteil ist, dass du damit Debugging-Signale erzeugst, die schlecht skalieren, schlecht reproduzierbar sind und oft die Realität verzerren.

Erstens: Du verlierst Kontext. Eine Print-Zeile sagt dir selten, in welchem Call-Stack du bist, welche Request-Parameter gerade aktiv sind, welcher User oder welche Transaktion betroffen ist, oder was kurz vorher passiert ist. Du kannst dir das alles in Text zusammenbauen - aber dann baust du dir dein eigenes Logging-System per Hand, ohne Levels, ohne Struktur, ohne Filter.

Zweitens: Du machst den Code lauter, nicht klarer. Debugging soll Komplexität reduzieren: Du willst gezielt sehen, was relevant ist. Prints sind das Gegenteil: Du streust Output, hoffst auf einen Treffer und entfernst danach wieder alles. In der Realität bleibt oft etwas liegen - und irgendwann committest du versehentlich Debug-Ausgaben.

Drittens: Du veränderst Timing und Verhalten. Das ist besonders fies bei Nebenläufigkeit, IO, Performance-Problemen und allem, was in einem Application Server wie WildFly läuft. Ausgabe ist langsam. Und println() ist synchron. Wenn du an einer "kritischen" Stelle ausgibst, kann allein das den Bug verschwinden lassen oder neu erzeugen. Das ist kein theoretisches Problem - das passiert in der Praxis erstaunlich oft.

Viertens: Du debugst nicht, du protokollierst. Debugging heißt: du untersuchst den Zustand deines Programms, während es läuft - mit Zugriff auf Variablen, Objekte, Stack, Bedingungen, Ausdrücke. println() gibt dir nur ein Foto, das du vorher selbst arrangiert hast.

Fünftens: In produktionsnahen Umgebungen ist stdout oft der falsche Kanal. In JavaEE-Setups landet stdout je nach Container irgendwo, wird anders geroutet, oder ist in Log-Aggregation nur als unstrukturierter Text vorhanden. Wenn du später wirklich nach Fehlern suchst, willst du Logs, die du filtern, korrelieren und sinnvoll durchsuchen kannst.

 

Was du stattdessen nutzen solltest

Wenn du sauber debuggen willst, kombinierst du in der Regel zwei Dinge:

  • einen Debugger in IntelliJ für interaktives Analysieren
  • Logging für nachvollziehbare Laufzeit-Informationen

Der Debugger ist für den Moment, in dem du verstehen willst, was in genau dieser Ausführung passiert. Logging ist für alles, was du später nachvollziehen musst - auch dann, wenn du den Fehler nicht lokal reproduzieren kannst.

IntelliJ macht dir das sehr leicht. Breakpoint setzen, Request auslösen, Step Into, Step Over, Werte ansehen. Das klingt banal, ist aber der Kern von Debugging: Du lässt den Code laufen und schaust ihm dabei zu, statt ihn mit Ausgaben zu überreden, dir etwas zu erzählen.

Ein paar Dinge, die du im Debugger sofort bekommst, ohne auch nur eine Zeile Code zu ändern:

  • Call-Stack: Wo kommst du her, wer ruft wen auf?
  • Variablenansicht: Was steht gerade wirklich in dem Objekt?
  • Bedingungen an Breakpoints: Stoppe nur, wenn orderId == null oder list.isEmpty().
  • Evaluate Expression: Werte Ausdrücke aus, ohne Code zu ändern.

Logging ergänzt das, wenn du Zustände in einer laufenden Umgebung beobachten willst. Und Logging ist das, was du später auch behalten darfst, weil es kontrollierbar ist.

In Java-Projekten arbeitet man typischerweise mit einem Logger statt mit stdout. Beispiel mit SLF4J:

private static final Logger log = LoggerFactory.getLogger(MyService.class);

log.debug("User id: {}", userId);
log.info("Order saved: {}", orderId);
log.warn("No address for user {}", userId);

Wichtig sind hier ein paar Prinzipien:

  • Levels: debug, info, warn, error - du kannst später filtern.
  • Platzhalter statt String-Konkatenation: verständlich und effizient.
  • Aussagekräftige Messages: lieber "Order saved" als "here".

Wenn du auf WildFly unterwegs bist, spielt das Zusammenspiel mit dem Server-Logging eine Rolle. Grundsätzlich gilt: Lieber in das Logging des Systems integrieren als wild stdout zu spammen. Damit landen deine Logs dort, wo sie hingehören - inklusive Rotation, Konfiguration und zentraler Auswertung.

 

Ein realistischer Workflow für die Praxis

Wenn etwas nicht funktioniert, brauchst du keinen perfekten Prozess. Du brauchst einen, der dich schnell und reproduzierbar zur Ursache bringt.

  1. Reproduziere den Fehler so klein wie möglich Wenn du den Bug nicht reproduzieren kannst, debugst du im Nebel. Versuche, den kleinsten Trigger zu finden: ein bestimmter Request, ein bestimmter Datensatz, eine bestimmte Kombination aus Parametern.

  2. Formuliere eine Hypothese Nicht philosophisch, sondern technisch: "customer ist null", "die Liste ist leer", "die Query liefert mehr Zeilen als erwartet", "der Mapper setzt das Feld nicht".

  3. Debugger zuerst, Print zuletzt Setze einen Breakpoint an die Stelle, an der deine Hypothese überprüfbar ist. Steige von dort rückwärts oder vorwärts durch den Code. Wenn du merkst, dass du ständig neu starten musst, arbeite mit bedingten Breakpoints oder Watches.

  4. Logging gezielt ergänzen Wenn du Werte über mehrere Requests, Threads oder Zeiträume sehen musst, ergänze Logs - aber bewusst:

  • logge Schlüsselwerte (IDs, Status, Größen)
  • logge an Systemgrenzen (Controller, Service, Integration, Persistence)
  • logge nicht jede Schleifeniteration, außer du willst wirklich genau das
  1. Aufräumen gehört dazu Wenn du ad hoc Debug-Logs einfügst, entferne oder downgrade sie. Das gilt besonders für sehr detaillierte Ausgaben. Was bleibt, sollte einen echten Nutzen haben.

 

Ein typisches Beispiel: Print liefert dir das falsche Gefühl von Sicherheit

Angenommen, du suchst einen Fehler in einer Validierung und machst das:

System.out.println("email=" + email);

Du siehst etwas in der Konsole und denkst: "Email ist da, also ist alles gut." Dabei weißt du immer noch nicht:

  • ob email vielleicht nur Whitespace enthält
  • ob das Objekt später verändert wird
  • ob du gerade im richtigen Request gelandet bist
  • ob die Validierung überhaupt die gleiche Instanz nutzt

Mit Debugger siehst du sofort, ob email null ist, wie es gesetzt wurde, und was danach damit passiert. Mit Logging kannst du zusätzlich eine saubere Spur erzeugen - inklusive Kontext.

Wenn du unbedingt schnell etwas ausgeben willst, ist der Debugger oft sogar schneller als println(): Breakpoint setzen, einmal laufen lassen, und du hast die Werte. Kein Code ändern, kein Build, kein Commit-Risiko.

 

Fazit

System.out.println() ist eine Ausgabe, kein Debugging-Werkzeug. Für sehr kleine Checks kann es dir helfen - aber sobald du Ursachen finden willst, arbeitest du damit gegen dich: zu wenig Kontext, zu viel Rauschen, potenziell verändertes Laufzeitverhalten.

Wenn du dir angewöhnst, mit IntelliJ zu debuggen und sauberes Logging zu verwenden, wirst du schneller, präziser und deutlich entspannter beim Fehlerfinden. Und du baust nebenbei eine Codebasis, die du auch in ein paar Monaten noch verstehst.