Voorbereiding op Java-backend-interviews

Java-interviewvragen en -antwoorden 2026

Een set van 30 Java-interviewvragen voor backend-ontwikkelaars, variërend van de fundamenten van de taal en het gedrag van de JVM tot collecties, de Stream API en concurrency.

Start Java-proefinterviewGeen creditcard nodig. 1 gratis sessie beschikbaar.
Oefen technische interviews in het EngelsEen modus waarin anderstaligen kunnen oefenen met het afleggen van technische interviews.

Java en de JVM

1Wat is Java en waarom wordt het als platformonafhankelijk beschouwd?

Java is een statisch getypeerde programmeertaal voor algemeen gebruik. Broncode wordt gecompileerd naar bytecode, en bytecode draait op de JVM (Java Virtual Machine). Dezelfde .class- of .jar-bestanden kunnen doorgaans zonder hercompilatie op verschillende besturingssystemen draaien als er een compatibele JVM beschikbaar is. Dit is echter geen absolute onafhankelijkheid: native bibliotheken, platformspecifieke paden, systeemcommando's en het gedrag van het besturingssysteem kunnen een applicatie alsnog platformafhankelijk maken.

Java source -> javac -> bytecode -> JVM -> machine code
Probeer deze vraag te beantwoorden met een AI-begeleider

2Wat is het verschil tussen de JVM (Java Virtual Machine), JRE (Java Runtime Environment) en JDK (Java Development Kit)?

De JVM laadt en voert bytecode uit en beheert geheugen, threads en de garbage collection. De JRE betekende van oudsher de JVM plus de bibliotheken die nodig zijn om applicaties uit te voeren. De JDK omvat de uitvoeringsomgeving en ontwikkeltools zoals `javac`, `java`, een debugger, `javadoc`, `jar` en diagnostische tools. In moderne distributies is een aparte JRE minder gebruikelijk, omdat applicaties vaak worden geleverd met een aangepaste runtime-image.

JDK = development tools + runtime
Runtime = JVM + standard libraries
Probeer deze vraag te beantwoorden met een AI-begeleider

3Hoe voert de JVM (Java Virtual Machine) Java-code uit en waarom is JIT (Just-In-Time) nodig?

`javac` compileert de broncode naar bytecode. De JVM laadt klassen en begint met het interpreteren of uitvoeren van bytecode-instructies. Vaak uitgevoerde codepaden worden tijdens de uitvoering gedetecteerd en door de JIT-compiler gecompileerd naar geoptimaliseerde machinecode. JIT kan methoden inlinen, lussen optimaliseren, escape-analyse toepassen en sommige toewijzingen van geheugen verwijderen. Dit is de reden waarom Java-applicaties sneller kunnen worden na een opwarmfase, en waarom microbenchmarks zonder JMH (Java Microbenchmark Harness) vaak misleidend zijn.

Probeer deze vraag te beantwoorden met een AI-begeleider

4Hoe werkt een `ClassLoader`?

Een `ClassLoader` laadt klassedefinities in de JVM (Java Virtual Machine). De meest voorkomende loaders zijn de Bootstrap, Platform, Application en custom loaders. Een klasse wordt niet alleen geïdentificeerd door de volledig gekwalificeerde naam, maar ook door de `ClassLoader` die de klasse heeft geladen. Twee klassen met dezelfde naam afkomstig van verschillende loaders kunnen dus verschillende typen zijn. Java maakt meestal gebruik van ouderdelegatie: een loader vraagt eerst zijn ouder om een klasse te laden, om zo het risico op het overschrijven van platformklassen te verkleinen.

Loading
Linking: verification, preparation, resolution
Initialization
Probeer deze vraag te beantwoorden met een AI-begeleider

5Hoe is het geheugen van de JVM (Java Virtual Machine) georganiseerd?

Belangrijke geheugengebieden van de JVM omvatten de heap voor objecten en arrays, Java-stacks met callframes voor elke thread, Metaspace voor klasse-metadata, de code cache voor JIT (Just-In-Time)-gecompileerde code, het PC-register en native stacks. Vermijd de oversimplificatie dat primitieve typen altijd op de stack leven en objecten altijd op de heap: een primitief veld wordt opgeslagen in het bijbehorende object, en JIT-escape-analyse kan geheugentoewijzingen weglaten of de representatie van een object wijzigen.

Probeer deze vraag te beantwoorden met een AI-begeleider

6Hoe werkt de Garbage Collector (GC)?

De GC (Garbage Collector) maakt geheugen vrij dat wordt ingenomen door objecten die niet langer bereikbaar zijn vanuit GC-roots. Roots omvatten lokale variabelen van actieve methoden, statische velden, actieve threads en JNI-referenties. Veel collectors vertrouwen op de observatie dat de meeste nieuwe objecten een korte levensduur hebben, waardoor de heap vaak wordt opgesplitst in jonge en oude generaties. De GC sluit bestanden of verbindingen niet deterministisch; externe resources moeten worden gesloten met `try-with-resources`.

try (var stream = Files.newInputStream(path)) {
    // use stream
}
Probeer deze vraag te beantwoorden met een AI-begeleider

OOP en typen

7Welke vier OOP-principes (Object-Oriented Programming) ondersteunt Java?

Java ondersteunt inkapseling, abstractie, overerving en polymorfisme. Inkapseling verbergt de interne status achter een publieke API. Abstractie richt zich op essentieel gedrag. Overerving creëert een nieuw type op basis van een bestaand type. Polymorfisme stelt code in staat om via een gedeeld contract met verschillende implementaties te werken. In de praktijk heeft compositie vaak de voorkeur boven overerving, omdat het resulteert in een lossere koppeling.

interface PaymentService {
    void pay(BigDecimal amount);
}

class CardPaymentService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        // payment implementation
    }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

8Wat is het verschil tussen het overladen (overloading) en overschrijven (overriding) van methoden?

Overladen betekent dat meerdere methoden dezelfde naam hebben, maar verschillende parameters; de compiler kiest de methode op basis van de statische typen van de argumenten. Overschrijven betekent dat een subklasse de implementatie van een instantiemethode van een bovenliggende klasse vervangt; tijdens de uitvoering wordt de juiste methode gekozen op basis van het daadwerkelijke objecttype. Statische methoden worden verborgen, niet overschreven, en privémethoden worden niet overschreven omdat ze niet worden geërfd als toegankelijke leden.

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"); }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

9Wat is het verschil tussen een interface en een abstracte klasse?

Een interface definieert een contract en kan abstracte methoden, `default`-methoden, statische methoden, constanten en private helper-methoden bevatten. Een abstracte klasse kan daarnaast instantiestatus, constructors, `protected` velden, gedeelde implementatie en abstracte of concrete methoden hebben. Een klasse kan slechts één klasse uitbreiden, maar meerdere interfaces implementeren. Gebruik een interface voor een onafhankelijk contract en een abstracte klasse voor nauw verwante typen met een gedeelde implementatie en status.

interface Repository<T> {
    Optional<T> findById(long id);
}
Probeer deze vraag te beantwoorden met een AI-begeleider

10Wat betekent het sleutelwoord final?

Een final variabele kan slechts één keer worden toegewezen, maar een final referentie maakt het gerefereerde object niet onveranderlijk. Een final methode kan niet worden overschreven. Een final klasse kan niet worden uitgebreid door overerving. Het sleutelwoord final communiceert intentie en beperkt referentiewijzigingen of overerving, maar biedt op zichzelf geen volledig onveranderlijk model of threadveiligheid.

final int maxAttempts = 3;

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

public final void validate() {}

public final class Token {}
Probeer deze vraag te beantwoorden met een AI-begeleider

11Wat zijn records in Java?

Een record is een compacte syntaxis voor onveranderlijke gegevensdragers. De compiler genereert private final-velden, een constructor, toegangsmethoden, equals, hashCode en toString. Records zijn nuttig voor DTO's (Data Transfer Objects), waarde-objecten en methoderesultaten. Ze zijn geen vervanging voor elke domeinentiteit: validatie kan worden toegevoegd in een compacte constructor, maar complex veranderlijk gedrag, lazy loading en op overerving gebaseerde modellen passen doorgaans slecht bij records.

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

public record Order(List<String> items) {
    public Order {
        items = List.copyOf(items);
    }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

12Hoe maak je een onveranderlijk object in Java?

Maak de klasse `final` of beperk overerving zorgvuldig, maak velden `private final`, vermijd setters en voer validatie uit in de constructor. Maak voor veranderlijke invoer defensieve kopieën en retourneer kopieën of onveranderlijke weergaven. Diepe onveranderlijkheid vereist dat alle geneste status eveneens onveranderlijk is. `final`-velden helpen bij veilige publicatie, maar een object kan logisch gezien nog steeds veranderlijk zijn als het een veranderlijke collectie of een veranderlijk genest object blootstelt.

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;
    }
}
Probeer deze vraag te beantwoorden met een AI-begeleider

Referenties, strings en vergelijkingen

13Maakt Java gebruik van pass-by-value of pass-by-reference?

Java geeft argumenten altijd door via pass-by-value. Voor primitieve typen wordt de waarde zelf gekopieerd. Voor objecten wordt de waarde van de referentie gekopieerd. Een methode kan het object muteren via de gekopieerde referentie, maar kan de referentie van de aanroeper niet vervangen door een ander object. Dit onderscheid verklaart waarom het wijzigen van een veld zichtbaar kan zijn voor de aanroeper, terwijl het toewijzen van de parameter aan een nieuw object dat niet is.

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

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

void replace(User user) {
    user = new User("New");
}
Probeer deze vraag te beantwoorden met een AI-begeleider

14Wat is het verschil tussen == en equals()?

Voor primitieve typen vergelijkt == de waarden. Voor objectreferenties controleert == of beide referenties naar hetzelfde object wijzen. equals() is een methode die de logische gelijkheid zou moeten vergelijken als de klasse deze correct implementeert. String, Integer, LocalDate en veel andere waardeklassen overschrijven equals. Voor eigen domeinobjecten moet equals de bedoelde identiteit of waardesemantiek weerspiegelen.

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
Probeer deze vraag te beantwoorden met een AI-begeleider

15Wat is het contract tussen `equals` en `hashCode`?

Als twee objecten gelijk zijn volgens `equals`, moeten ze dezelfde `hashCode` retourneren. Het omgekeerde is niet vereist: verschillende objecten mogen dezelfde `hashCode` hebben. De implementatie van `equals` moet reflexief, symmetrisch, transitief en consistent zijn, en false retourneren voor `null`. Het schenden van dit contract verstoort het gedrag in `HashMap`, `HashSet` en andere op hashes gebaseerde collecties: een object kan onvindbaar worden in de collectie, zelfs als het wel degelijk aanwezig is.

@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);
}
Probeer deze vraag te beantwoorden met een AI-begeleider

16Waarom is `String` onveranderlijk?

`String` is onveranderlijk voor veiligheid, voorspelbaarheid en prestaties. Onveranderlijkheid maakt interning van strings, veilig delen tussen threads, betrouwbare caching van `hashCode` en veilig gebruik in paden, URL's, klassenamen en veiligheidsgevoelige waarden mogelijk. Operaties zoals `concat`, `replace` en `substring` creëren nieuwe strings. Sinds Java 9 kan `String` intern `byte[]` gebruiken met een compacte weergave, maar het publieke contract van onveranderlijkheid blijft hetzelfde.

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
Probeer deze vraag te beantwoorden met een AI-begeleider

17Wat is het verschil tussen String, StringBuilder en StringBuffer?

String is onveranderlijk, waardoor herhaaldelijke aaneenschakeling in lussen veel tussenliggende objecten kan creëren, hoewel de compiler eenvoudige gevallen kan optimaliseren. StringBuilder is veranderlijk en krijgt de voorkeur voor het opbouwen van strings binnen een enkele thread. StringBuffer is vergelijkbaar maar gesynchroniseerd, waardoor het meestal trager is en voornamelijk relevant is voor oudere thread-veilige API's. Voor opmaak kunnen String.format of template-achtige API's overzichtelijker zijn, maar deze zijn niet altijd sneller.

StringBuilder builder = new StringBuilder();

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

String result = builder.toString();

String message = "Hello, " + name;
Probeer deze vraag te beantwoorden met een AI-begeleider

Generics en collecties

18Wat zijn generics en wat is type-uitwissing?

Generics maken het mogelijk om in code typeveilige containers en API's uit te drukken, zoals List<String>. Tijdens het compileren controleert de compiler de typen en voegt, waar nodig, type-casts in. Tijdens de uitvoering wordt de meeste informatie over generieke typen uitgewist (type-uitwissing), waardoor List<String> en List<Integer> dezelfde runtimeklasse delen. Dit is de reden waarom je niet direct new T() kunt aanroepen, geen primitieve typeparameters kunt gebruiken, of op een betrouwbare manier instanceof List<String> kunt controleren.

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

List<? extends Number> producer;
List<? super Integer> consumer;
Probeer deze vraag te beantwoorden met een AI-begeleider

19Wat is het Java Collections Framework?

Het Collections Framework is een verzameling van interfaces en implementaties voor het opslaan en verwerken van groepen objecten. Kerninterfaces zijn onder andere `List`, `Set`, `Queue`, `Deque` en `Map`. Implementaties omvatten `ArrayList`, `LinkedList`, `HashSet`, `TreeSet`, `HashMap`, `TreeMap` en `PriorityQueue`. Kies een collectie op basis van het toegangspatroon, vereisten voor sortering, uniekheid, zoekcomplexiteit, geheugenoverhead en concurrency-vereisten.

List<String> list = new ArrayList<>();
Set<Long> ids = new HashSet<>();
Map<Long, User> usersById = new HashMap<>();
Deque<String> queue = new ArrayDeque<>();
Probeer deze vraag te beantwoorden met een AI-begeleider

20Hoe werkt een HashMap?

Een `HashMap` slaat items op in buckets die worden geselecteerd op basis van een hash van de sleutel. Voor het opzoeken wordt de hash berekend, de juiste bucket gezocht en worden sleutels vergeleken met behulp van de `equals`-methode. Collisions (botsingen) worden binnen de bucket afgehandeld; in moderne Java-versies kunnen lange ketens worden omgezet in een gebalanceerde boomstructuur wanneer aan bepaalde voorwaarden wordt voldaan. Een `HashMap` staat maximaal één `null`-sleutel en meerdere `null`-waarden toe, is niet thread-safe en kan van grootte veranderen (resizen) wanneer de drempelwaarde van de load factor wordt overschreden.

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

User found = users.get(user.id());
Probeer deze vraag te beantwoorden met een AI-begeleider

21Wat is het verschil tussen HashMap, LinkedHashMap, TreeMap en ConcurrentHashMap?

HashMap biedt snelle, ongeordende toegang en is niet thread-safe. LinkedHashMap behoudt de invoegvolgorde of toegangsvolgorde en is nuttig voor LRU-achtige caches. TreeMap houdt sleutels gesorteerd op natuurlijke volgorde of via een Comparator en voert bewerkingen uit in O(log n)-tijd. ConcurrentHashMap is ontworpen voor gelijktijdige toegang met gesegmenteerde of interne synchronisatie, staat geen null-sleutels of -waarden toe, en geniet onder belasting de voorkeur over synchronizedMap.

map.computeIfAbsent(key, ignored -> loadValue());
Probeer deze vraag te beantwoorden met een AI-begeleider

22Wat is het verschil tussen ArrayList en LinkedList?

ArrayList is gebaseerd op een array. Het biedt snelle toegang op basis van een index, een compacte geheugenindeling en efficiënte iteratie, maar voor invoegen in het midden moeten bestaande elementen worden opgeschoven. LinkedList is een dubbel gekoppelde lijst. Invoegen kost minder tijd als het knooppunt al bekend is, maar toegang via een index is O(n), de geheugenoverhead is groter en de cachelokaliteit is slechter. In veel praktische toepassingen is ArrayList sneller, zelfs wanneer er af en toe invoegingen plaatsvinden.

Probeer deze vraag te beantwoorden met een AI-begeleider

23Wat is het verschil tussen Comparable en Comparator?

Comparable definieert de natuurlijke ordening binnen de klasse via compareTo. Comparator is een afzonderlijk object of een lambda die een externe ordening definieert. Comparable is handig wanneer een type één duidelijke standaardordening heeft. Comparator is beter wanneer er meerdere ordeningen zijn, zoals op naam, datum of prioriteit. compare en compareTo moeten consistent zijn en bij voorkeur compatibel met equals bij gebruik in gesorteerde sets of maps.

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);
Probeer deze vraag te beantwoorden met een AI-begeleider

Uitzonderingen en resources

24Wat is het verschil tussen checked en unchecked exceptions?

Checked exceptions erven van Exception, maar niet van RuntimeException, en de compiler vereist dat aanroepers ze afhandelen of declareren. Unchecked exceptions erven van RuntimeException en zijn niet vereist in methodesignaturen. Checked exceptions zijn geschikt voor herstelbare situaties op de grenzen van een API. Unchecked exceptions zijn geschikt voor programmeerfouten, een ongeldige status en schendingen van contracten. In moderne servicecode vermijden teams vaak overmatige checked exceptions, omdat deze zich op een storende manier door de verschillende lagen heen verspreiden.

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

throw new IllegalArgumentException("Invalid id");
Probeer deze vraag te beantwoorden met een AI-begeleider

25Wat is het verschil tussen throw, throws en try-with-resources?

throw werpt onmiddellijk een exception op. throws declareert een mogelijke exception in een methodesignatuur. try-with-resources sluit AutoCloseable-resources automatisch in omgekeerde volgorde. Als zowel het hoofdblok als de close-actie exceptions opwerpen, wordt de tweede bewaard als een onderdrukte exception. Dit is veiliger en duidelijker dan handmatige finally-blokken wanneer er meerdere resources bij betrokken zijn.

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}
Probeer deze vraag te beantwoorden met een AI-begeleider

Stream API en functionele stijl

26Wat is de Stream API?

De Stream API stelt code in staat om reeksen op een declaratieve manier te verwerken. Tussenliggende operaties zoals `filter`, `map` en `sorted` zijn lazy; eindoperaties zoals `toList`, `collect`, `reduce` en `forEach` activeren de uitvoering. Een stream is geen verzameling, wordt meestal maar één keer gebruikt en werkt het best met pure functies zonder neveneffecten. `parallelStream` garandeert geen snellere uitvoering en kan de prestaties of correctheid schaden.

List<String> names = users.stream()
    .filter(User::isActive)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .toList();
Probeer deze vraag te beantwoorden met een AI-begeleider

27Wat is `Optional` en wanneer moet je het gebruiken?

`Optional<T>` vertegenwoordigt expliciet de afwezigheid van een resultaat en werkt goed als retourtype. Handige methoden zijn onder andere `map`, `flatMap`, `filter`, `orElse`, `orElseGet` en `orElseThrow`. `orElse` evalueert zijn argument direct, terwijl de supplier in `orElseGet` alleen wordt uitgevoerd wanneer de waarde afwezig is. `Optional` wordt over het algemeen niet aanbevolen voor entiteitsvelden, methodeparameters of geserialiseerde DTO's (Data Transfer Objects), tenzij daar een zwaarwegende reden voor is.

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

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

optional.orElse(expensiveOperation());
optional.orElseGet(() -> expensiveOperation());
Probeer deze vraag te beantwoorden met een AI-begeleider

Concurrency

28Wat garanderen `synchronized`, `volatile` en atomaire klassen?

`synchronized` biedt wederzijdse uitsluiting en een happens-before-relatie. `volatile` garandeert de zichtbaarheid van de laatste schrijfacties en beperkt het herschikken van instructies, maar het maakt samengestelde bewerkingen niet atomair. Atomaire klassen bieden individuele atomaire operaties zoals `incrementAndGet`. Gebruik locks of `synchronized` voor meerdere gerelateerde wijzigingen, `volatile` voor eenvoudige statusvlaggen en `AtomicInteger` of `LongAdder` voor tellers bij contention.

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
Probeer deze vraag te beantwoorden met een AI-begeleider

29Wat zijn een race condition, deadlock en threadveiligheid?

Een race condition treedt op wanneer het resultaat afhangt van de onvoorspelbare uitvoeringsvolgorde van threads. Een deadlock ontstaat wanneer threads oneindig op elkaars resources wachten. Om threadveiligheid te bereiken, verminder je gedeelde veranderlijke state, behoud je een consistente lock-volgorde, verkort je kritieke secties, vermijd je het aanroepen van externe code binnen locks en gebruik je onveranderlijke objecten, concurrent-collecties, time-outs of message passing. Threadveiligheid is een eigenschap van het gehele protocol voor de toegang tot state.

Probeer deze vraag te beantwoorden met een AI-begeleider

30Wat is het verschil tussen `ExecutorService`, `CompletableFuture` en virtual threads?

`ExecutorService` beheert een threadpool en de uitvoering van taken. `CompletableFuture` helpt bij het samenstellen van asynchrone operaties. Virtual threads zijn lichtgewicht JVM (Java Virtual Machine)-threads die handig zijn voor grote aantallen blokkerende I/O-taken. Ze vereenvoudigen een thread-per-request-model, maar ze versnellen geen CPU-gebonden werk, nemen geen limieten van databases of externe resources weg, en maken veranderlijke state niet automatisch threadveilig.

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());
}
Probeer deze vraag te beantwoorden met een AI-begeleider