Vorbereitung auf Java Backend Interviews

Java Interviewfragen und Antworten 2026

Eine Sammlung von 30 Java-Fragen für Backend-Interviews: Sprache, JVM, Collections, Stream API und Nebenläufigkeit.

Java Mock Interview auf Deutsch starten Technische Interviews auf Englisch übenEin Modus, in dem Nicht-Muttersprachler technische Interviews üben können.

Java und JVM

1Was ist Java und warum gilt es als plattformunabhängig?

Java ist eine statisch typisierte allgemeine Programmiersprache. Der Quellcode wird in Bytecode kompiliert, und der Bytecode läuft auf der JVM. Das gleiche .class- oder .jar-File kann normalerweise auf verschiedenen Betriebssystemen ohne Neukompilierung ausgeführt werden, sofern eine kompatible JVM verfügbar ist. Dies ist keine absolute Unabhängigkeit: Native Bibliotheken, plattformspezifische Pfade, Systembefehle und OS-Verhalten können eine Anwendung dennoch plattformabhängig machen.

Java source -> javac -> bytecode -> JVM -> machine code

2Was ist der Unterschied zwischen JVM, JRE und JDK?

Die JVM lädt und führt Bytecode aus und verwaltet Speicher, Threads und Garbage Collection. Die JRE bedeutete historisch die JVM plus Bibliotheken, die zum Ausführen von Anwendungen erforderlich sind. Das JDK enthält die Laufzeit- und Entwicklungswerkzeuge wie javac, java, Debugger, javadoc, jar und Diagnosewerkzeuge. In modernen Distributionen ist ein separates JRE weniger üblich, da Anwendungen oft mit einem benutzerdefinierten Runtime Image ausgeliefert werden.

JDK = development tools + runtime
Runtime = JVM + standard libraries

3Wie führt die JVM Java-Code aus und warum ist JIT nötig?

javac kompiliert den Quellcode in Bytecode. Die JVM lädt Klassen und beginnt mit der Interpretation oder Ausführung von Bytecode-Anweisungen. Häufig ausgeführte Hot Paths werden zur Laufzeit erkannt und vom JIT Compiler in optimierten Maschinencode kompiliert. JIT kann Methoden einbetten (inline), Schleifen optimieren, Escape Analysis anwenden und einige Allokationen entfernen. Deshalb können Java-Anwendungen nach dem Warm-up schneller werden, und deshalb sind Microbenchmarks ohne JMH oft irreführend.

4Wie funktioniert ClassLoader?

Ein ClassLoader lädt Klassen-Definitionen in die JVM. Die gängigen Loader sind Bootstrap, Platform, Application und benutzerdefinierte Loader. Eine Klasse wird nicht nur durch ihren vollständig qualifizierten Namen, sondern auch durch den ClassLoader identifiziert, der sie geladen hat; daher können zwei Klassen mit demselben Namen aus verschiedenen Loadern unterschiedliche Typen sein. Java verwendet normalerweise die Parent Delegation: ein Loader fragt zuerst seinen Elternteil, um das Risiko zu verringern, Plattformklassen zu überschreiben.

Loading
Linking: verification, preparation, resolution
Initialization

5Wie ist der JVM-Speicher organisiert?

Zu den wichtigsten JVM-Speicherbereichen gehören der Heap für Objekte und Arrays, Java Stacks mit Call Frames für jeden Thread, Metaspace für Klassenmetadaten, ein Code Cache für JIT-kompilierte Code, der PC Register und native Stacks. Vermeiden Sie die Übervereinfachung, dass Primitive immer auf dem Stack und Objekte immer auf dem Heap leben: Ein primitives Feld wird innerhalb seines Objekts gespeichert, und JIT Escape Analysis kann Allokationen entfernen oder die Objektrepräsentation ändern.

6Wie funktioniert der Garbage Collector?

Der Garbage Collector gibt Speicher frei, der von Objekten belegt wird, die nicht mehr von GC Roots erreichbar sind. Zu den Wurzeln gehören lokale Variablen aktiver Methoden, statische Felder, aktive Threads und JNI-Referenzen. Viele Collector verlassen sich auf die Beobachtung, dass die meisten neuen Objekte jung sterben, daher wird der Heap oft in junge und alte Generationen unterteilt. GC schließt Dateien oder Verbindungen nicht deterministisch; externe Ressourcen sollten mit try-with-resources geschlossen werden.

try (var stream = Files.newInputStream(path)) {
    // use stream
}

OOP und Typen

7Welche vier OOP-Prinzipien unterstützt Java?

Java unterstützt Kapselung (Encapsulation), Abstraktion (Abstraction), Vererbung (Inheritance) und Polymorphismus (Polymorphism). Kapselung verbirgt den internen Zustand hinter einer öffentlichen API. Abstraktion konzentriert sich auf das wesentliche Verhalten. Vererbung erstellt einen neuen Typ basierend auf einem bestehenden Typ. Polymorphismus ermöglicht es dem Code, mit verschiedenen Implementierungen über einen gemeinsamen Vertrag zu arbeiten. In der Praxis ist Composition oft vorzuziehen gegenüber Inheritance, da sie eine geringere Kopplung erzeugt.

interface PaymentService {
    void pay(BigDecimal amount);
}

class CardPaymentService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        // payment implementation
    }
}

8Was ist der Unterschied zwischen Method Overloading und Overriding?

Overloading bedeutet, dass mehrere Methoden denselben Namen, aber unterschiedliche Parameter haben; der Compiler wählt die Methode basierend auf statischen Argumenttypen. Overriding bedeutet, dass eine Unterklasse die Implementierung einer Instanzmethode aus einer Elternklasse ersetzt; die Laufzeit-Dispatch wählt die Methode basierend auf dem tatsächlichen Objekttyp. Statische Methoden werden versteckt (hidden), nicht überschrieben (overridden), und private Methoden können nicht überschrieben werden, da sie nicht als zugängliche Mitglieder vererbt werden.

void send(String message) {}
void send(String message, int priority) {}

class Animal {
    void speak() { System.out.println("Animal"); }
}

class Dog extends Animal {
    @Override
    void speak() { System.out.println("Dog"); }
}

9Was ist der Unterschied zwischen einer Interface und einer abstrakten Klasse?

Ein Interface definiert einen Vertrag und kann abstrakte Methoden, Default-Methoden, statische Methoden, Konstanten und private Helfer enthalten. Eine abstrakte Klasse kann zusätzlich Instanzzustand (instance state), Konstruktoren, geschützte Felder (protected fields), gemeinsame Implementierung sowie abstrakte oder konkrete Methoden haben. Eine Klasse kann nur von einer anderen Klasse erben, aber von mehreren Interfaces implementieren. Verwenden Sie ein Interface für einen unabhängigen Vertrag und eine abstrakte Klasse für eng verwandte Typen mit gemeinsamem Zustand und Implementierung.

interface Repository<T> {
    Optional<T> findById(long id);
}

10Was bedeutet das Schlüsselwort final?

Eine finale Variable kann nur einmal zugewiesen werden, aber eine finale Referenz macht das referenzierte Objekt nicht unveränderlich (immutable). Eine finale Methode kann nicht überschrieben werden. Eine finale Klasse kann nicht erweitert werden. `final` kommuniziert die Absicht und begrenzt Änderungen der Referenz oder Vererbung, bietet aber allein kein vollständiges unveränderliches Modell oder Thread-Sicherheit.

final int maxAttempts = 3;

final List<String> names = new ArrayList<>();
names.add("Alice"); // allowed

public final void validate() {}

public final class Token {}

11Was sind Records in Java?

Ein Record ist eine kompakte Syntax für unveränderliche Datenträger (immutable data carriers). Der Compiler generiert private finale Felder, einen Konstruktor, Accessoren, `equals`, `hashCode` und `toString`. Records sind nützlich für DTOs, Wertobjekte (value objects) und Methoden-Ergebnisse. Sie ersetzen jedoch nicht jede Domänenentität: Validierung kann in einem kompakten Konstruktor hinzugefügt werden, aber komplexes veränderliches Verhalten, Lazy Loading und auf Vererbung basierende Modelle passen normalerweise schlecht zu Records.

public record User(long id, String name) {}

public record Order(List<String> items) {
    public Order {
        items = List.copyOf(items);
    }
}

12Wie erstellt man ein unveränderliches Objekt in Java?

Machen Sie die Klasse final oder beschränken Sie die Vererbung sorgfältig. Machen Sie Felder private und final, vermeiden Sie Setter und führen Sie Validierungen im Konstruktor durch. Für veränderliche Eingaben erstellen Sie defensive Kopien und geben Sie entweder Kopien oder unmodifizierbare Ansichten (unmodifiable views) zurück. Tiefe Unveränderlichkeit erfordert, dass auch alle verschachtelten Zustände unveränderlich sind. `final` Felder helfen bei der sicheren Veröffentlichung (safe publication), aber ein Objekt kann immer noch logisch veränderbar sein, wenn es eine veränderliche Sammlung oder ein veränderliches verschachteltes Objekt freigibt.

public final class User {
    private final String name;
    private final List<String> roles;

    public User(String name, List<String> roles) {
        this.name = name;
        this.roles = List.copyOf(roles);
    }

    public List<String> getRoles() {
        return roles;
    }
}

Referenzen, Strings und Vergleich

13Überträgt Java per Wert oder per Referenz?

Java übergibt Argumente immer per Wert. Bei Primitiven wird der Wert selbst kopiert. Bei Objekten wird der Wert der Referenz kopiert. Eine Methode kann das Objekt über die kopierte Referenz mutieren, aber sie kann nicht die Referenz des Aufrufers durch ein anderes Objekt ersetzen. Diese Unterscheidung erklärt, warum eine Änderung eines Feldes für den Aufrufer sichtbar sein kann, während die Zuweisung des Parameters zu einem neuen Objekt dies nicht ist.

void change(int value) {
    value = 100;
}

void rename(User user) {
    user.setName("Kate");
}

void replace(User user) {
    user = new User("New");
}

14Was ist der Unterschied zwischen == und equals()?

Bei Primitiven vergleicht == Werte. Bei Objektreferenzen prüft ==, ob beide Referenzen auf dasselbe Objekt zeigen. equals() ist eine Methode, die logische Gleichheit vergleichen sollte, wenn die Klasse sie korrekt implementiert. String, Integer, LocalDate und viele Wertklassen überschreiben equals. Für benutzerdefinierte Domänenobjekte muss equals die beabsichtigte Identität oder Semantik des Werts widerspiegeln.

int a = 10;
int b = 10;
System.out.println(a == b); // true

String first = new String("java");
String second = new String("java");

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // true

15Was ist der equals/hashCode-Vertrag?

Wenn zwei Objekte nach equals gleich sind, müssen sie denselben hashCode zurückgeben. Das Gegenteil ist nicht erforderlich: Unterschiedliche Objekte können denselben hashCode haben. equals sollte reflexiv, symmetrisch, transitiv, konsistent sein und null für null zurückgeben. Die Verletzung dieses Vertrags korrumpiert das Verhalten in HashMap, HashSet und anderen hash-basierten Sammlungen: ein Objekt kann unfindbar werden, obwohl es vorhanden ist.

@Override
public boolean equals(Object object) {
    if (this == object) return true;
    if (!(object instanceof User other)) return false;
    return id == other.id;
}

@Override
public int hashCode() {
    return Long.hashCode(id);
}

16Warum ist String unveränderlich (immutable)?

String ist aus Gründen der Sicherheit, Vorhersagbarkeit und Leistung unveränderlich. Die Unveränderlichkeit ermöglicht String-Interning, sicheres Teilen zwischen Threads, zuverlässiges hashCode-Caching und die sichere Verwendung in Pfaden, URLs, Klassennamen und sicherheitskritischen Werten. Operationen wie concat, replace und substring erstellen neue Strings. Seit Java 9 kann String intern ein byte[] mit einer kompakten Darstellung verwenden, aber der öffentliche Unveränderlichkeitsvertrag bleibt derselbe.

String value = "Java";
value.toUpperCase();

System.out.println(value); // Java

String a = "java";
String b = "java";

System.out.println(a == b); // often true due to interning

17Was ist der Unterschied zwischen String, StringBuilder und StringBuffer?

String ist unveränderlich, daher kann wiederholtes Konkatenieren in Schleifen viele Zwischenobjekte erstellen, obwohl der Compiler einfache Fälle optimieren kann. StringBuilder ist veränderbar (mutable) und wird für das Erstellen von Strings in einem einzigen Thread bevorzugt. StringBuffer ist ähnlich, aber synchronisiert, weshalb es normalerweise langsamer ist und hauptsächlich für Legacy thread-sichere APIs relevant ist. Für Formatierungen können String.format oder Template-Style APIs klarer sein, sind aber nicht immer schneller.

StringBuilder builder = new StringBuilder();

for (String item : items) {
    builder.append(item).append(',');
}

String result = builder.toString();

String message = "Hello, " + name;

Generics und Sammlungen

18Was sind Generics und was ist Type Erasure?

Generics ermöglichen es dem Code, typsichere Container und APIs auszudrücken, wie z. B. List<String>. Zum Kompilierzeit überprüft der Compiler die Typen und fügt bei Bedarf Casts ein. Zur Laufzeit wird die meisten generischen Typinformation entfernt (erased), sodass List<String> und List<Integer> dieselbe Runtime-Klasse teilen. Deshalb können Sie nicht direkt new T() erstellen, primitive Typ-Parameter verwenden oder zuverlässig instanceof List<String> prüfen.

List<String> names = new ArrayList<>();
names.add("Alice");

List<? extends Number> producer;
List<? super Integer> consumer;

19Was ist das Java Collections Framework?

Das Collections Framework ist eine Reihe von Schnittstellen und Implementierungen zum Speichern und Verarbeiten von Gruppen von Objekten. Zu den Kernschnittstellen gehören List, Set, Queue, Deque und Map. Zu den Implementierungen zählen ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap und PriorityQueue. Wählen Sie eine Sammlung basierend auf dem Zugriffsmuster, den Anforderungsanforderungen an die Reihenfolge, die Einzigartigkeit, die Suchkomplexität, den Speicher-Overhead und die Konkurrenzbedürfnisse.

List<String> list = new ArrayList<>();
Set<Long> ids = new HashSet<>();
Map<Long, User> usersById = new HashMap<>();
Deque<String> queue = new ArrayDeque<>();

20Wie funktioniert HashMap?

HashMap speichert Einträge in Buckets, die durch einen Hash des Schlüssels ausgewählt werden. Für die Suche berechnet es den Hash, findet das Bucket und vergleicht Schlüssel mit equals. Kollisionen werden innerhalb des Buckets behandelt; in modernem Java können lange Ketten unter bestimmten Bedingungen in einen ausgewogenen Baum umgewandelt werden (treeified). HashMap erlaubt einen null-Schlüssel und null-Werte, ist nicht thread-sicher und kann sich vergrößern, wenn der Load Factor Schwellenwert überschritten wird.

Map<Long, User> users = new HashMap<>();
users.put(user.id(), user);

User found = users.get(user.id());

21Was ist der Unterschied zwischen HashMap, LinkedHashMap, TreeMap und ConcurrentHashMap?

HashMap bietet schnellen unsortierten Zugriff und ist nicht thread-sicher. LinkedHashMap bewahrt die Einfüge- oder Zugriffsreihenfolge und ist nützlich für LRU-ähnliche Caches. TreeMap hält Schlüssel nach natürlicher Reihenfolge oder einem Comparator sortiert und hat O(log n) Operationen. ConcurrentHashMap ist für den gleichzeitigen Zugriff mit segmentierter/interner Synchronisierung konzipiert, erlaubt keine null-Schlüssel oder -werte und ist unter Last gegenüber synchronizedMap vorzuziehen.

map.computeIfAbsent(key, ignored -> loadValue());

22Was ist der Unterschied zwischen ArrayList und LinkedList?

ArrayList basiert auf einem Array. Es bietet schnellen indexbasierten Zugriff, eine kompakte Speicheranordnung und effiziente Iteration, aber das Einfügen in die Mitte erfordert das Verschieben von Elementen. LinkedList ist eine doppelt verlinkte Liste. Sie hat ein günstigeres Einfügen, wenn der Knoten bereits bekannt ist, aber der indexbasierte Zugriff ist O(n), der Speicher-Overhead ist höher und die Cache-Lokalität ist schlechter. In vielen realen Anwendungen ist ArrayList schneller, selbst wenn einige Einfügungen beteiligt sind.

23Was ist der Unterschied zwischen Comparable und Comparator?

Comparable definiert die natürliche Reihenfolge innerhalb der Klasse über compareTo. Comparator ist ein separates Objekt oder Lambda, das eine externe Sortierung definiert. Comparable ist nützlich, wenn ein Typ eine offensichtliche Standardreihenfolge hat. Comparator ist besser geeignet, wenn es mehrere Ordnungen gibt, wie z. B. nach Name, Datum oder Priorität. compare und compareTo müssen konsistent und vorzugsweise mit equals kompatibel sein, wenn sie in sortierten Sets oder Maps verwendet werden.

class User implements Comparable<User> {
    private int age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(age, other.age);
    }
}

Comparator<User> byName =
    Comparator.comparing(User::getName);

Comparator<User> byAgeThenName =
    Comparator.comparingInt(User::getAge)
              .thenComparing(User::getName);

Ausnahmen und Ressourcen

24Was ist der Unterschied zwischen checked und unchecked Exceptions?

Checked Exceptions erben von Exception, aber nicht von RuntimeException, und der Compiler verlangt von Aufrufern, sie zu behandeln oder zu deklarieren. Unchecked Exceptions erben von RuntimeException und sind in Methodensignaturen nicht erforderlich. Checked Exceptions eignen sich für wiederherstellbare Situationen an API-Grenzen. Unchecked Exceptions eignen sich für Programmierfehler, ungültige Zustände und Vertragsverletzungen. In modernem Service-Code vermeiden Teams oft übermäßige checked Exceptions, da diese durch die Schichten „verrauscht“ übertragen werden.

void load() throws IOException {
    // ...
}

throw new IllegalArgumentException("Invalid id");

25Was ist der Unterschied zwischen throw, throws und try-with-resources?

throw wirft sofort eine Ausnahme. throws deklariert eine mögliche Ausnahme in einer Methodensignatur. try-with-resources schließt AutoCloseable Ressourcen automatisch in umgekehrter Reihenfolge. Wenn sowohl der Hauptblock als auch close Ausnahmen werfen, wird die sekundäre als unterdrückte Ausnahme beibehalten. Dies ist sicherer und klarer als manuelle finally Blöcke, wenn mehrere Ressourcen beteiligt sind.

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}

Stream API und funktionaler Stil

26Was ist die Stream API?

Die Stream API ermöglicht es dem Code, Sequenzen deklarativ zu verarbeiten. Zwischenoperationen wie filter, map und sorted sind faul (lazy); Terminaloperationen wie toList, collect, reduce und forEach lösen die Ausführung aus. Ein Stream ist keine Sammlung, wird normalerweise nur einmal verwendet und funktioniert am besten mit reinen Funktionen ohne Nebenwirkungen. parallelStream garantiert keine schnellere Ausführung und kann die Leistung oder Korrektheit beeinträchtigen.

List<String> names = users.stream()
    .filter(User::isActive)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .toList();

27Was ist Optional und wann sollte man es verwenden?

Optional<T> repräsentiert explizit das Fehlen eines Ergebnisses und eignet sich gut als Rückgabetyp. Nützliche Methoden sind map, flatMap, filter, orElse, orElseGet und orElseThrow. orElse evaluiert sein Argument eager (ausführlich), während der Supplier in orElseGet nur ausgeführt wird, wenn der Wert abwesend ist. Optional wird normalerweise nicht für Entitätsfelder, Methodenparameter oder serialisierte DTOs empfohlen, es sei denn, es gibt einen starken Grund.

Optional<User> user = repository.findById(id);

String name = user
    .map(User::getName)
    .orElse("Unknown");

optional.orElse(expensiveOperation());
optional.orElseGet(() -> expensiveOperation());

Konkurrenz

28Was garantieren synchronized, volatile und atomare Klassen?

synchronized bietet gegenseitigen Ausschluss (mutual exclusion) und eine happens-before-Beziehung. volatile garantiert die Sichtbarkeit der neuesten Schreibvorgänge und schränkt das Neuanordnen ein, macht jedoch keine zusammengesetzten Operationen atomar. Atomare Klassen bieten einzelne atomare Operationen wie incrementAndGet. Verwenden Sie Locks oder synchronized für mehrere zusammenhängende Änderungen, volatile für einfache Status-Flags und AtomicInteger oder LongAdder für Zähler unter hoher Belastung (Contention).

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();

29Was sind Race Condition, Deadlock und Thread Safety?

Ein Race Condition tritt auf, wenn das Ergebnis von der unvorhersehbaren Ausführungsreihenfolge der Threads abhängt. Ein Deadlock tritt auf, wenn Threads ewig auf die Ressourcen des jeweils anderen warten. Um Thread Safety zu erreichen, reduzieren Sie den gemeinsam genutzten veränderlichen Zustand (shared mutable state), halten Sie eine konsistente Sperrreihenfolge ein, verkürzen Sie kritische Abschnitte (critical sections), vermeiden Sie das Aufrufen externen Codes unter Locks und verwenden Sie unveränderliche Objekte (immutable objects), concurrente Sammlungen, Timeouts oder Nachrichtenübermittlung. Thread Safety ist eine Eigenschaft des gesamten Zustandzugriffsprotokolls.

30Was ist der Unterschied zwischen ExecutorService, CompletableFuture und virtual threads?

ExecutorService verwaltet einen Thread-Pool und die Aufgabenausführung. CompletableFuture hilft bei der Komposition asynchroner Operationen. Virtual Threads sind leichte JVM-Threads, die für eine große Anzahl von blockierenden I/O-Aufgaben praktisch sind. Sie vereinfachen ein Modell mit einem Thread pro Anfrage (thread-per-request), beschleunigen aber keine CPU-gebundene Arbeit, beseitigen keine Datenbank- oder externe Ressourcenlimits und machen den veränderlichen Zustand nicht automatisch thread-sicher.

try (var executor = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = executor.submit(() -> calculate());
    int result = future.get();
}

CompletableFuture<User> userFuture =
    CompletableFuture.supplyAsync(this::loadUser)
        .thenApply(this::enrichUser)
        .exceptionally(error -> fallbackUser());

try (var executor =
         Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handleRequest());
}