Préparation entretien Java backend

Questions et réponses d'entretien Java 2026

Une sélection de 30 questions Java pour les entretiens backend, des bases du langage à la JVM, aux collections, aux streams et à la concurrence.

Commencer un mock interview Java en français Pratique d'entretien technique en anglaisUn mode où les non-natifs peuvent s'entraîner à passer des entretiens techniques.

Java et JVM

1Qu'est-ce que Java et pourquoi est-il considéré comme indépendant de la plateforme ?

Java est un langage généraliste à typage statique. Le code source est compilé en bytecode, et le bytecode s'exécute sur la JVM. Le même fichier .class ou .jar peut généralement fonctionner sur différents systèmes d'exploitation sans recompilation si une JVM compatible est disponible. Ce n'est pas une indépendance absolue : les bibliothèques natives, les chemins spécifiques à la plateforme, les commandes système et le comportement du système d'exploitation peuvent toujours rendre une application dépendante de la plateforme.

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

2Quelle est la différence entre JVM, JRE et JDK ?

La JVM charge et exécute le bytecode et gère la mémoire, les threads et la collecte des déchets (garbage collection). Le JRE signifiait historiquement la JVM plus les bibliothèques nécessaires pour exécuter des applications. Le JDK comprend l'environnement d'exécution et les outils de développement tels que javac, java, debugger, javadoc, jar et des outils diagnostiques. Dans les distributions modernes, un JRE séparé est moins courant car les applications sont souvent livrées avec une image d'exécution personnalisée.

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

3Comment la JVM exécute-t-elle le code Java et pourquoi le JIT est-il nécessaire ?

javac compile le code source en bytecode. La JVM charge les classes et commence à interpréter ou à exécuter des instructions de bytecode. Les chemins chauds (hot paths) fréquemment exécutés sont détectés à l'exécution et compilés par le compilateur JIT en code machine optimisé. Le JIT peut effectuer l'inlining des méthodes, optimiser les boucles, appliquer l'analyse d'évasion (escape analysis) et supprimer certaines allocations. C'est pourquoi les applications Java peuvent accélérer après un échauffement, et pourquoi les microbenchmarks sans JMH sont souvent trompeurs.

4Comment fonctionne ClassLoader ?

Un ClassLoader charge des définitions de classes dans la JVM. Les chargeurs courants sont Bootstrap, Platform, Application et des chargeurs personnalisés. Une classe est identifiée non seulement par son nom entièrement qualifié mais aussi par le ClassLoader qui l'a chargée, de sorte que deux classes portant le même nom provenant de chargeurs différents peuvent être de types différents. Java utilise généralement la délégation parentale : un chargeur demande d'abord à son parent pour réduire le risque de remplacer les classes de plateforme.

Loading
Linking: verification, preparation, resolution
Initialization

5Comment la mémoire JVM est-elle organisée ?

Les principales zones de mémoire de la JVM comprennent le tas (heap) pour les objets et les tableaux, les piles Java (Java stacks) avec des cadres d'appel pour chaque thread, Metaspace pour les métadonnées de classe, le code cache pour le code compilé par JIT, le registre PC et les piles natives. Évitez la simplification excessive selon laquelle les types primitifs vivent toujours sur la pile et que les objets vivent toujours dans le tas : un champ primitif est stocké à l'intérieur de son objet, et l'analyse d'évasion du JIT peut supprimer des allocations ou modifier la représentation de l'objet.

6Comment fonctionne Garbage Collector ?

Le Garbage Collector libère la mémoire occupée par les objets qui ne sont plus atteignables depuis les Racines GC (GC Roots). Les racines comprennent les variables locales des méthodes actives, les champs statiques, les threads actifs et les références JNI. De nombreux collecteurs reposent sur l'observation que la plupart des nouveaux objets meurent jeunes, de sorte que le tas est souvent divisé en générations jeunes et anciennes. Le GC ne ferme pas de manière déterministe les fichiers ou les connexions ; les ressources externes doivent être fermées avec try-with-resources.

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

POO et types

7Quels sont les quatre principes de POO pris en charge par Java ?

Java prend en charge l'encapsulation, l'abstraction, l'héritage et le polymorphisme. L'encapsulation cache l'état interne derrière une API publique. L'abstraction se concentre sur le comportement essentiel. L'héritage crée un nouveau type basé sur un type existant. Le polymorphisme permet au code de fonctionner avec différentes implémentations via un contrat partagé. En pratique, la composition est souvent préférable à l'héritage car elle crée un couplage plus faible.

interface PaymentService {
    void pay(BigDecimal amount);
}

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

8Quelle est la différence entre le surchargement (overloading) et la substitution (overriding) de méthodes ?

Le surchargement signifie que plusieurs méthodes ont le même nom mais des paramètres différents ; le compilateur choisit la méthode en fonction des types d'arguments statiques. La substitution signifie qu'une sous-classe remplace l'implémentation d'une méthode d'instance d'une classe parente ; le dispatch à l'exécution choisit la méthode en fonction du type réel de l'objet. Les méthodes statiques sont masquées, non substituées, et les méthodes privées ne sont pas substituées car elles ne sont pas héritées comme membres accessibles.

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"); }
}

9Quelle est la différence entre une interface et une classe abstraite ?

Une interface définit un contrat et peut contenir des méthodes abstraites, des méthodes par défaut (default methods), des méthodes statiques, des constantes et des helpers privés. Une classe abstraite peut en plus avoir un état d'instance, des constructeurs, des champs protégés, une implémentation partagée et des méthodes abstraites ou concrètes. Une classe ne peut étendre qu'une seule autre classe mais implémenter plusieurs interfaces. Utilisez une interface pour un contrat indépendant et une classe abstraite pour des types étroitement liés avec une implémentation et un état partagés.

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

10Que signifie le mot-clé final ?

Une variable final ne peut être assignée qu'une seule fois, mais une référence final ne rend pas l'objet référencé immuable. Une méthode final ne peut pas être substituée (overridden). Une classe final ne peut pas être étendue. `final` communique une intention et limite les changements de références ou l'héritage, mais en soi il ne fournit pas un modèle d'immuabilité complet ni la sécurité des threads.

final int maxAttempts = 3;

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

public final void validate() {}

public final class Token {}

11Qu'est-ce qu'un record dans Java ?

Un record est une syntaxe compacte pour les porteurs de données immuables. Le compilateur génère des champs privés final, un constructeur, des accesseurs, `equals`, `hashCode` et `toString`. Les records sont utiles pour les DTOs (Data Transfer Objects), les objets valeur et les résultats de méthodes. Ils ne remplacent pas chaque entité métier : la validation peut être ajoutée dans un constructeur compact, mais le comportement mutable complexe, le chargement paresseux et les modèles basés sur l'héritage conviennent généralement mal aux records.

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

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

12Comment créer un objet immuable en Java ?

Rendre la classe final ou restreindre soigneusement l'héritage, rendre les champs private final, éviter les setters et effectuer la validation dans le constructeur. Pour les entrées mutables, créer des copies défensives et retourner des copies ou des vues non modifiables. L'immuabilité profonde exige que tout état imbriqué soit également immuable. Les champs final aident à une publication sécurisée, mais un objet peut toujours être logiquement mutable s'il expose une collection mutable ou un objet imbriqué mutable.

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;
    }
}

Références, chaînes et comparaison

13Java passe-t-il par valeur ou par référence ?

Java passe toujours les arguments par valeur. Pour les types primitifs, la valeur elle-même est copiée. Pour les objets, la valeur de la référence est copiée. Une méthode peut muter l'objet via la référence copiée, mais elle ne peut pas remplacer la référence de l'appelant par un autre objet. Cette distinction explique pourquoi le changement d'un champ peut être visible pour l'appelant, tandis que l'assignation du paramètre à un nouvel objet ne l'est pas.

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

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

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

14Quelle est la différence entre == et equals() ?

Pour les types primitifs, == compare des valeurs. Pour les références d'objets, == vérifie si les deux références pointent vers le même objet. equals() est une méthode qui devrait comparer l'égalité logique si la classe l'implémente correctement. String, Integer, LocalDate et de nombreuses classes de valeur outrepassent (override) equals. Pour les objets de domaine personnalisés, equals doit refléter l'identité ou la sémantique de valeur prévue.

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

15Qu'est-ce que le contrat equals/hashCode ?

Si deux objets sont égaux selon equals, ils doivent retourner le même hashCode. L'inverse n'est pas requis : des objets différents peuvent avoir le même hashCode. equals doit être réflexif, symétrique, transitif, cohérent et retourner false pour null. Rompre ce contrat corrompt le comportement dans HashMap, HashSet et autres collections basées sur les hachages : un objet peut devenir impossible à trouver même s'il est présent.

@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);
}

16Pourquoi String est-il immuable ?

String est immuable pour des raisons de sécurité, de prévisibilité et de performance. L'immuabilité permet l'interning de chaînes, le partage sécurisé entre threads, la mise en cache fiable du hashCode et une utilisation sécurisée dans les chemins (paths), les URL, les noms de classes et les valeurs sensibles à la sécurité. Des opérations comme concat, replace et substring créent de nouvelles chaînes. Depuis Java 9, String peut utiliser en interne byte[] avec une représentation compacte, mais le contrat public d'immuabilité reste le même.

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

17Quelle est la différence entre String, StringBuilder et StringBuffer ?

String est immuable, donc la concaténation répétée dans des boucles peut créer de nombreux objets intermédiaires, bien que le compilateur puisse optimiser les cas simples. StringBuilder est mutable et préféré pour construire des chaînes dans un seul thread. StringBuffer est similaire mais synchronisé, il est donc généralement plus lent et principalement pertinent pour les API héritées sécurisées par les threads. Pour le formatage, String.format ou les API de style modèle peuvent être plus clairs, mais ils ne sont pas toujours plus rapides.

StringBuilder builder = new StringBuilder();

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

String result = builder.toString();

String message = "Hello, " + name;

Génériques et collections

18Qu'est-ce que les génériques et qu'est-ce que l'effacement de type (type erasure) ?

Les génériques permettent au code d'exprimer des conteneurs et des API sûrs en termes de types, tels que List<String>. Au moment de la compilation, le compilateur vérifie les types et insère des casts si nécessaire. Au moment de l'exécution, la plupart des informations de type générique sont effacées, de sorte que List<String> et List<Integer> partagent la même classe d'exécution. C'est pourquoi vous ne pouvez pas créer directement new T(), utiliser des paramètres de types primitifs, ou vérifier de manière fiable instanceof List<String>.

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

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

19Qu'est-ce que le Java Collections Framework ?

Le Collections Framework est un ensemble d'interfaces et d'implémentations pour stocker et traiter des groupes d'objets. Les interfaces principales comprennent List, Set, Queue, Deque et Map. Les implémentations incluent ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap et PriorityQueue. Choisissez une collection en fonction du modèle d'accès, des exigences de classement, de l'unicité, de la complexité de recherche, de la surcharge mémoire et des besoins de concurrence.

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

20Comment fonctionne HashMap ?

HashMap stocke les entrées dans des buckets sélectionnés par un hachage de la clé. Pour la recherche, il calcule le hachage, trouve le bucket et compare les clés avec equals. Les collisions sont gérées à l'intérieur du bucket ; dans Java moderne, de longues chaînes peuvent être transformées en un arbre équilibré lorsque les conditions sont remplies. HashMap permet une clé null et des valeurs nulles, n'est pas thread-safe, et peut redimensionner lorsque le seuil du facteur de charge est dépassé.

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

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

21Quelle est la différence entre HashMap, LinkedHashMap, TreeMap et ConcurrentHashMap ?

HashMap fournit un accès rapide non ordonné et n'est pas thread-safe. LinkedHashMap préserve l'ordre d'insertion ou l'ordre d'accès et est utile pour les caches de type LRU. TreeMap maintient les clés triées par ordre naturel ou Comparator et possède des opérations en O(log n). ConcurrentHashMap est conçu pour un accès concurrent avec une synchronisation segmentée/interne, ne permet pas de clés ou de valeurs nulles, et est préférable à synchronizedMap sous charge.

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

22Quelle est la différence entre ArrayList et LinkedList ?

ArrayList est basé sur un tableau. Il fournit un accès indexé rapide, un agencement mémoire compact et une itération efficace, mais l'insertion au milieu nécessite le décalage des éléments. LinkedList est une liste doublement chaînée. Elle a une insertion moins coûteuse si le nœud est déjà connu, mais l'accès indexé est en O(n), la surcharge mémoire est plus élevée et la localité du cache est pire. Dans de nombreuses applications réelles, ArrayList est plus rapide même lorsque certaines insertions sont impliquées.

23Quelle est la différence entre Comparable et Comparator ?

Comparable définit l'ordre naturel à l'intérieur de la classe via compareTo. Comparator est un objet séparé ou un lambda qui définit un ordre externe. Comparable est utile lorsqu'un type a un ordre par défaut évident. Comparator est préférable lorsqu'il y a plusieurs ordres, tels que par nom, date ou priorité. compare et compareTo doivent être cohérents et de préférence compatibles avec equals lorsqu'ils sont utilisés dans des ensembles ou des cartes triées.

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);

Exceptions et ressources

24Quelle est la différence entre les exceptions vérifiées (checked) et non vérifiées (unchecked) ?

Les exceptions vérifiées héritent de Exception mais pas de RuntimeException, et le compilateur exige que les appelants les gèrent ou les déclarent. Les exceptions non vérifiées héritent de RuntimeException et ne sont pas requises dans les signatures de méthodes. Les exceptions vérifiées conviennent aux situations récupérables aux limites des API. Les exceptions non vérifiées conviennent aux erreurs de programmation, aux états invalides et aux violations de contrat. Dans le code de service moderne, les équipes évitent souvent un excès d'exceptions vérifiées car elles se propagent bruyamment à travers les couches.

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

throw new IllegalArgumentException("Invalid id");

25Quelle est la différence entre throw, throws et try-with-resources ?

throw lance immédiatement une exception. throws déclare une exception possible dans une signature de méthode. try-with-resources ferme automatiquement les ressources AutoCloseable dans l'ordre inverse. Si le bloc principal et close lancent tous deux des exceptions, la seconde est conservée en tant qu'exception supprimée (suppressed exception). C'est plus sûr et plus clair que les blocs finally manuels lorsque plusieurs ressources sont impliquées.

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

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

API Stream et style fonctionnel

26Qu'est-ce que la Stream API ?

La Stream API permet de traiter les séquences de manière déclarative. Les opérations intermédiaires telles que filter, map et sorted sont paresseuses (lazy) ; les opérations terminales telles que toList, collect, reduce et forEach déclenchent l'exécution. Un stream n'est pas une collection, est généralement utilisé une seule fois et fonctionne mieux avec des fonctions pures sans effets de bord. parallelStream ne garantit pas une exécution plus rapide et peut nuire aux performances ou à la correction.

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

27Qu'est-ce qu'Optional et quand devriez-vous l'utiliser ?

Optional<T> représente explicitement l'absence d'un résultat et fonctionne bien comme type de retour. Les méthodes utiles comprennent map, flatMap, filter, orElse, orElseGet et orElseThrow. orElse évalue son argument avec empressement (eagerly), tandis que le fournisseur dans orElseGet ne s'exécute que lorsque la valeur est absente. Optional n'est généralement pas recommandé pour les champs d'entité, les paramètres de méthode ou les DTO sérialisés, sauf s'il y a une raison impérieuse.

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

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

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

Concurrence

28Qu'est-ce que les classes synchronized, volatile et atomic garantissent ?

synchronized fournit une exclusion mutuelle et une relation happens-before. volatile garantit la visibilité des dernières écritures et restreint le réordonnancement, mais il ne rend pas les opérations composées atomiques. Les classes Atomic fournissent des opérations atomiques individuelles telles que incrementAndGet. Utilisez des verrous ou synchronized pour plusieurs changements liés, volatile pour de simples drapeaux d'état et AtomicInteger ou LongAdder pour les compteurs en cas de contention.

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

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

29Qu'est-ce qu'une condition de concurrence (race condition), un blocage (deadlock) et la sécurité des threads ?

Une condition de concurrence se produit lorsque le résultat dépend de l'ordre d'exécution imprévisible des threads. Un blocage se produit lorsque les threads attendent indéfiniment les ressources les uns des autres. Pour atteindre la sécurité des threads, réduisez l'état mutable partagé, maintenez un ordre de verrouillage cohérent, raccourcissez les sections critiques, évitez d'appeler du code externe sous des verrous et utilisez des objets immuables, des collections concurrentes, des délais d'attente ou le passage de messages. La sécurité des threads est une propriété du protocole d'accès à l'état entier.

30Quelle est la différence entre ExecutorService, CompletableFuture et les virtual threads ?

ExecutorService gère un pool de threads et l'exécution des tâches. CompletableFuture aide à composer des opérations asynchrones. Les virtual threads sont des threads JVM légers qui sont pratiques pour un grand nombre de tâches I/O bloquantes. Ils simplifient un modèle thread-par-requête, mais ils n'accélèrent pas le travail lié au CPU, ne suppriment pas les limites de base de données ou de ressources externes, ni ne rendent automatiquement l'état mutable sûr pour les threads.

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());
}