{
  "assessmentTests": {
    "java_test": {
      "name": "Test Java",
      "desc": "30 questions de scénarios sur la syntaxe/types, les méthodes/surcharge/scope, les collections/génériques/contrôle de flux, et les exceptions/concurrence/idiomes — découvrez si votre Java correspond à ce qu'une offre d'emploi entend par maîtrise Java.",
      "recommendation": "Votre profil de compétences Java",
      "results": {
        "beginner": {
          "name": "Débutant",
          "desc": "Vous pouvez écrire du code Java fonctionnel et comprendre la structure de base, mais les questions que vous avez manquées révèlent une confusion autour des comparaisons de références vs contenu, du boxing/unboxing des Integer, et de la façon dont finally redéfinit une valeur de retour sans que personne ne le remarque. Ce ne sont pas des failles de programmation ; ce sont des cas particuliers spécifiques où le code se comporte d'une manière que beaucoup d'observateurs superficiels n'anticipent pas. Elles importent au travail car chacune est un endroit où une pull request passe la revue mais échoue silencieusement en production.",
          "recommendation": "Commencez par trois choses, dans cet ordre : pourquoi == sur deux String s'appelle une comparaison de référence, jamais de contenu (et .equals() est obligatoire), comment Integer.valueOf() met les valeurs en cache en dessous de 128 donc == peut sembler fonctionner mais échoue au-dessus de cette limite, et pourquoi un return dans un try n'est jamais le dernier mot si une clause finally existe. Les tutoriels Oracle Java et la documentation du collections framework couvrent tous les trois avec des exemples exécutables."
        },
        "intermediate": {
          "name": "Intermédiaire",
          "desc": "Vous écrivez confortablement du Java pour des systèmes petits-à-moyens — vous ne seriez pas ralenti par la maintenance de routine ou une petite fonctionnalité sur une base de code existante. L'écart entre ici et Avancé concerne surtout la résolution de surcharge, le comportement de la sous-jacente des generics, et ce qui se passe quand les exceptions interagissent avec le contrôle de flux. Une méthode héritée en surcharge silencieuse par une classe enfant qui change le type de retour covariant. Un TypeErasure qui rend deux méthodes surchargées ambiguës au runtime. Un NullPointerException à partir d'un autoboxing qui échoue silencieusement plutôt que de jeter clairement. Ce sont les types de bugs qui passent un test simple et ne s'affichent que sous charge ou dans des cas particuliers du monde réel.",
          "recommendation": "Concentrez-vous sur la façon dont les mécanismes s'interagissent plutôt que ce que chacun fait seul : la résolution de surcharge quand une classe enfant redéfinit une méthode avec un type de retour différent (et pourquoi c'est valide), la suppression de type (TypeErasure) et ce qu'elle signifie pour les generics au runtime, et les garanties de NullPointerException autour du boxing. Puis les interactions Stream/lambda avec exception handling, puisque les exceptions au sein d'une Stream.map ne sortent pas où l'on pourrait s'y attendre."
        },
        "advanced": {
          "name": "Avancé",
          "desc": "C'est le niveau que la plupart des offres d'emploi entendent par « bon Java ». Vous lisez une classe métier complexe et pouvez prédire ce qu'un bloc de code retourne sans l'exécuter, vous savez pourquoi une race condition s'est produite plutôt que seulement qu'elle l'a fait, et vous recourez à un CopyOnWriteArrayList ou à un AtomicInteger au lieu d'une synchronisation naïve quand c'est l'outil plus clair. Ce qui sépare cette bande du sommet est le côté défensif du travail : savoir quel type de collection est thread-safe, ce qu'une clause synchronized laisse réellement non protégé, et ce qu'une classe immutable ou une API stream finale vous gagne en échange.",
          "recommendation": "Progressez dans les parties qui vous permettent de raisonner avec certitude sur la concurrence et la performance : la différence entre synchronized et les Collections thread-safe, ce que volatile et synchronized laissent réellement non protégés, et pourquoi une classe immutable ou une API stream peut remplacer une boucle mutable. Utilisez la documentation Java Concurrency in Practice ou les articles approfondis de Modern Java en Concurrence comme prochaine étape."
        },
        "expert": {
          "name": "Expert",
          "desc": "Vous avez obtenu un score au sommet de chaque section — syntaxe/types, méthodes/scope, collections/génériques, et exceptions/concurrence. Concrètement, cela signifie qu'on peut vous donner un extrait de code impliquant de l'héritage multiple via les interfaces, du boxing/unboxing implicite, des Stream imbriquées, et de la concurrence avec des locks explicites, et vous pouvez expliquer ce qu'il retourne et pourquoi. À ce niveau, le langage est rarement le facteur limitant ; la limite est généralement la conception architecturale ou la gestion de la complexité de l'état mutuel.",
          "recommendation": "Les rendements se font maintenant dans les patterns, la conception et l'architecture : utiliser les interfaces plutôt que l'héritage pour la flexibilité, les immutables plutôt que les boucles mutables pour la concurrence, et les Streams plutôt que les boucles explicites pour la lisibilité du code. Si vous êtes filtré pour un rôle, décrivez un bug de concurrence réel que vous avez découvert en production — une race condition sur un accès non atomique, un deadlock dû à un ordre d'acquisition de lock — plutôt que de simplement nommer une API Java. Cela démontre le raisonnement, pas seulement le vocabulaire."
        }
      },
      "questions": [
        {
          "question": "Vous créez deux objets String : String a = new String(\"cat\"); String b = new String(\"cat\"); Que retourne a == b ?",
          "options": [
            {
              "icon": "",
              "label": "true — les littéraux String avec les mêmes caractères sont toujours le même objet"
            },
            {
              "icon": "",
              "label": "true — new String(...) réutilise le string pool, donc le texte identique partage toujours un objet"
            },
            {
              "icon": "",
              "label": "Une erreur de compilation — == ne peut pas être utilisé pour comparer des objets String"
            },
            {
              "icon": "",
              "label": "false — new String(...) alloue toujours un nouvel objet sur le heap, donc == compare deux références différentes même si .equals(b) retournerait true"
            }
          ]
        },
        {
          "question": "Integer x = 100; Integer y = 100; System.out.println(x == y); puis Integer x2 = 200; Integer y2 = 200; System.out.println(x2 == y2); Qu'affichent les deux lignes ?",
          "options": [
            {
              "icon": "",
              "label": "true puis true — les objets Integer autoboxés sont toujours mis en cache, quelle que soit leur valeur"
            },
            {
              "icon": "",
              "label": "false puis false — == sur des objets Integer est toujours une comparaison de référence, donc des valeurs égales ne correspondent jamais"
            },
            {
              "icon": "",
              "label": "true puis false, mais uniquement parce que 200 dépasse la capacité d'un byte — la limite du cache n'a rien à voir avec -128..127"
            },
            {
              "icon": "",
              "label": "true puis false — les valeurs Integer autoboxées de -128 à 127 sont mises en cache et partagées, donc 100 réutilise un seul objet, mais 200 sort du cache et s'autobox en deux objets distincts"
            }
          ]
        },
        {
          "question": "int max = Integer.MAX_VALUE; System.out.println(max + 1); Qu'est-ce qui se passe ?",
          "options": [
            {
              "icon": "",
              "label": "Il lève une ArithmeticException pour un dépassement entier"
            },
            {
              "icon": "",
              "label": "Il affiche Integer.MAX_VALUE à nouveau, parce que Java plafonne au maximum du type"
            },
            {
              "icon": "",
              "label": "Il affiche Integer.MIN_VALUE — l'arithmétique int s'enroule silencieusement en cas de dépassement au lieu de lever une exception"
            },
            {
              "icon": "",
              "label": "C'est une erreur de compilation — le compilateur détecte le dépassement à l'avance"
            }
          ]
        },
        {
          "question": "System.out.println(0.1 + 0.2 == 0.3); Qu'affiche cette ligne, et pourquoi ?",
          "options": [
            {
              "icon": "",
              "label": "false — 0.1, 0.2 et 0.3 ne peuvent pas être représentés exactement en virgule flottante binaire, donc la somme accumule une erreur d'arrondi qui la rend différente bit à bit de 0.3"
            },
            {
              "icon": "",
              "label": "true — Java arrondit l'arithmétique des double à la décimale représentable la plus proche avant de comparer"
            },
            {
              "icon": "",
              "label": "true — l'addition de deux double est toujours exacte pour des valeurs à deux décimales"
            },
            {
              "icon": "",
              "label": "Une exception est levée, car == n'est pas défini pour le type double en Java"
            }
          ]
        },
        {
          "question": "Une classe déclare deux champs d'instance sans initialisation : int count; et Integer total; Avant que le constructeur ne s'exécute, quelles sont leurs valeurs par défaut ?",
          "options": [
            {
              "icon": "",
              "label": "Les deux par défaut à 0, parce qu'Integer s'autobox en int lors de l'initialisation du champ"
            },
            {
              "icon": "",
              "label": "count est 0 et total est 0, auto-boxé lors de la première lecture"
            },
            {
              "icon": "",
              "label": "count est 0 et total est null — les champs primitifs numériques par défaut à zéro, mais un champ de référence wrapper non initialisé par défaut à null comme n'importe quelle autre référence objet"
            },
            {
              "icon": "",
              "label": "Les deux par défaut à null jusqu'à être explicitement assignés, puisque Java n'a pas de défauts numériques implicites"
            }
          ]
        },
        {
          "question": "Une boucle s'exécute 1000 fois, chaque fois en faisant result += \"x\"; sur une String result. Qu'est-ce qui se passe réellement sous le capot à chaque itération ?",
          "options": [
            {
              "icon": "",
              "label": "Le tableau de caractères interne de l'objet String existant est étendu sur place"
            },
            {
              "icon": "",
              "label": "Java regroupe automatiquement les concaténations et n'alloue qu'un seul objet String final"
            },
            {
              "icon": "",
              "label": "Il compile en un seul appel StringBuilder.append() partagé sur les 1000 itérations, sans objet supplémentaire alloué par itération"
            },
            {
              "icon": "",
              "label": "Un nouvel objet String est créé et result est réassigné pour pointer dessus — l'objet String précédent est rejeté, parce que String est immuable et += ne peut pas le modifier sur place"
            }
          ]
        },
        {
          "question": "final List<String> names = new ArrayList<>(); Lequel de ceux-ci est vrai ?",
          "options": [
            {
              "icon": "",
              "label": "names.add(\"Ana\") compile et fonctionne correctement — final empêche seulement de réassigner la référence names elle-même, pas de muter l'objet vers lequel elle pointe"
            },
            {
              "icon": "",
              "label": "names.add(\"Ana\") est une erreur de compilation, parce que final rend la liste elle-même non modifiable"
            },
            {
              "icon": "",
              "label": "La liste est thread-safe pour les écritures concurrentes parce qu'elle a été déclarée final"
            },
            {
              "icon": "",
              "label": "final sur une variable locale n'a aucun effet sauf si le type est également déclaré immuable"
            }
          ]
        },
        {
          "question": "int[] a = {1, 2, 3}; int[] b = {1, 2, 3}; System.out.println(a == b); System.out.println(Arrays.equals(a, b)); Qu'affichent les deux lignes ?",
          "options": [
            {
              "icon": "",
              "label": "false puis true — == compare les références du tableau (deux objets tableau différents), tandis qu'Arrays.equals compare les éléments"
            },
            {
              "icon": "",
              "label": "true puis true — les tableaux avec des contenus identiques sont le même objet en Java"
            },
            {
              "icon": "",
              "label": "false puis false — Arrays.equals ne fonctionne que pour les tableaux d'objets, pas pour les tableaux int primitifs"
            },
            {
              "icon": "",
              "label": "true puis false — == sur les tableaux compare le contenu, et Arrays.equals est redondant"
            }
          ]
        },
        {
          "question": "void show(Object o) { print(\"Object\"); } void show(String s) { print(\"String\"); } Object ref = \"hello\"; show(ref); Quelle surcharge s'exécute, et pourquoi ?",
          "options": [
            {
              "icon": "",
              "label": "show(String) s'exécute, parce que Java inspecte la classe d'exécution réelle de l'objet avant de choisir la surcharge"
            },
            {
              "icon": "",
              "label": "show(Object) s'exécute — la résolution de surcharge est décidée à la compilation en utilisant le type déclaré de la variable, pas le type d'exécution réel de l'objet auquel elle se réfère"
            },
            {
              "icon": "",
              "label": "C'est une erreur de compilation — Object ne peut pas être passé où une surcharge plus spécifique existe"
            },
            {
              "icon": "",
              "label": "Les deux méthodes s'exécutent, une fois chacune, parce que Java résout les surcharges en essayant chaque correspondance"
            }
          ]
        },
        {
          "question": "La classe Base a static void greet() { print(\"Base\"); }. La classe Derived extends Base et déclare également static void greet() { print(\"Derived\"); }. Vous écrivez : Base ref = new Derived(); ref.greet(); Qu'affiche cela ?",
          "options": [
            {
              "icon": "",
              "label": "Derived — les méthodes statiques se surchargent tout comme les méthodes d'instance, en suivant le type d'exécution réel de l'objet"
            },
            {
              "icon": "",
              "label": "C'est une erreur de compilation — les méthodes statiques ne peuvent pas être appelées via une référence d'instance"
            },
            {
              "icon": "",
              "label": "Base — les méthodes statiques ne sont pas polymorphes ; l'appel via une référence est résolu par le type déclaré de la référence à la compilation, pas le type réel de l'objet"
            },
            {
              "icon": "",
              "label": "À la fois Base et Derived s'affichent, parce que l'appel se résout vers les deux versions cachée et cachante"
            }
          ]
        },
        {
          "question": "À l'intérieur d'une méthode, vous écrivez int count = 0; puis vous essayez de référencer count à l'intérieur d'un lambda passé à une autre méthode. Que doit vérifier count pour que cela compile ?",
          "options": [
            {
              "icon": "",
              "label": "count doit être effectively final — jamais réaffectée après sa valeur initiale — car un lambda capture un instantané, pas une référence vivante vers une variable locale mutable"
            },
            {
              "icon": "",
              "label": "Rien — n'importe quelle variable locale peut être librement lue et réaffectée depuis un lambda"
            },
            {
              "icon": "",
              "label": "count doit être déclarée volatile pour que le lambda voie toujours sa dernière valeur"
            },
            {
              "icon": "",
              "label": "count doit être un champ, pas une variable locale — les lambdas ne peuvent capturer aucune variable locale"
            }
          ]
        },
        {
          "question": "void rename(StringBuilder sb) { sb = new StringBuilder(\"new\"); } est appelée comme StringBuilder original = new StringBuilder(\"old\"); rename(original); Qu'est-ce que original est après l'appel ?",
          "options": [
            {
              "icon": "",
              "label": "Devient \"new\" — les objets sont passés par référence en Java, donc réassigner le paramètre change aussi la variable de l'appelant"
            },
            {
              "icon": "",
              "label": "Devient \"new\" seulement pour les types mutables comme StringBuilder, mais pas pour les types immuables"
            },
            {
              "icon": "",
              "label": "Reste \"old\" — Java passe la référence elle-même par valeur, donc réassigner le paramètre à l'intérieur de la méthode ne repointe que la copie locale de la référence, laissant l'original de l'appelant intouché"
            },
            {
              "icon": "",
              "label": "Lève une exception à l'exécution, parce que sb a été réassigné à l'intérieur de la méthode"
            }
          ]
        },
        {
          "question": "Une classe a un bloc d'initialisation statique, un bloc d'initialisation d'instance, et un constructeur, dans cet ordre dans le source. Vous créez deux objets de cette classe l'un après l'autre. Dans quel ordre ceux-ci s'exécutent-ils ?",
          "options": [
            {
              "icon": "",
              "label": "Le bloc statique s'exécute exactement une fois, la première fois que la classe est chargée ; puis pour chaque objet, le bloc d'instance s'exécute puis le corps du constructeur s'exécute"
            },
            {
              "icon": "",
              "label": "Les trois s'exécutent à nouveau, dans l'ordre du source, pour chaque objet créé"
            },
            {
              "icon": "",
              "label": "Le constructeur s'exécute en premier pour chaque objet, puis le bloc d'instance, puis le bloc statique une seule fois à la toute fin"
            },
            {
              "icon": "",
              "label": "Le bloc statique s'exécute une fois par objet, juste avant le bloc d'instance"
            }
          ]
        },
        {
          "question": "Une classe a void log(String s) et void log(String... args). Vous appelez log(\"hi\"). Laquelle s'exécute ?",
          "options": [
            {
              "icon": "",
              "label": "log(String... args) — les surcharges varargs sont toujours préférées quand les deux sont applicables"
            },
            {
              "icon": "",
              "label": "C'est une erreur de compilation — l'appel est ambigu entre les deux surcharges"
            },
            {
              "icon": "",
              "label": "log(String s) — quand une surcharge d'arité fixe correspond exactement, Java la préfère toujours à une surcharge varargs, qui n'est utilisée qu'en dernier recours"
            },
            {
              "icon": "",
              "label": "Celle déclarée en premier dans le fichier source s'exécute"
            }
          ]
        },
        {
          "question": "La première ligne d'un constructeur appelle this(0); pour déléguer à un autre constructeur dans la même classe. Où cela est-il autorisé à apparaître ?",
          "options": [
            {
              "icon": "",
              "label": "N'importe où dans le corps du constructeur, tant qu'il s'exécute avant que l'objet ne soit retourné"
            },
            {
              "icon": "",
              "label": "Seulement comme la toute première instruction du constructeur — this() (ou super()) doit être la première ligne, et un constructeur ne peut pas appeler à la fois this() et super()"
            },
            {
              "icon": "",
              "label": "Seulement comme la dernière instruction, après que toute l'initialisation du champ soit complète"
            },
            {
              "icon": "",
              "label": "Seulement dans les constructeurs marqués explicitement comme déléguant, en utilisant une keyword delegate"
            }
          ]
        },
        {
          "question": "Vous avez besoin d'une liste qui aura des éléments insérés au front extrêmement souvent, et les accès aléatoires en lecture sont rares. Lequel est le meilleur ajustement structurel, et pourquoi ?",
          "options": [
            {
              "icon": "",
              "label": "ArrayList — son tableau de sauvegarde contigu rend chaque opération, y compris l'insertion au front, plus rapide qu'une structure liée"
            },
            {
              "icon": "",
              "label": "Ils performent de manière identique, parce que les deux implémentent l'interface List avec les mêmes garanties de temps"
            },
            {
              "icon": "",
              "label": "LinkedList — l'insertion au front est O(1) parce qu'elle relie juste un nœud, tandis qu'ArrayList doit décaler chaque élément existant de un, rendant l'insertion au front O(n)"
            },
            {
              "icon": "",
              "label": "LinkedList, parce qu'elle supporte l'accès aléatoire en O(1) tandis qu'ArrayList ne le fait pas"
            }
          ]
        },
        {
          "question": "Vous insérez cinq entrées dans un plain HashMap dans un ordre spécifique, puis vous l'itérez avec une boucle for-each. Dans quel ordre les entrées ressortent-elles ?",
          "options": [
            {
              "icon": "",
              "label": "Dans le même ordre que les entrées ont été insérées, puisque les maps Java préservent toujours l'ordre d'insertion"
            },
            {
              "icon": "",
              "label": "Aucun ordre garanti du tout — l'ordre d'itération de HashMap dépend du placement du bucket de hachage, pas de l'ordre d'insertion, et peut même changer entre les exécutions ; LinkedHashMap est ce qui préserve l'ordre d'insertion"
            },
            {
              "icon": "",
              "label": "Triés par clé automatiquement, de la même manière qu'une TreeMap se comporte"
            },
            {
              "icon": "",
              "label": "Ordre inverse de l'insertion, parce que HashMap utilise en interne une pile"
            }
          ]
        },
        {
          "question": "Combien de clés null un plain java.util.HashMap peut-il contenir à la fois, et combien de valeurs null ?",
          "options": [
            {
              "icon": "",
              "label": "Aucune clé null et aucune valeur null ne sont jamais autorisées dans aucune implémentation de Map"
            },
            {
              "icon": "",
              "label": "Une clé null au maximum, et n'importe quel nombre de valeurs null — HashMap permet une seule clé null, contrairement à Hashtable qui n'en permet aucune"
            },
            {
              "icon": "",
              "label": "Clés null illimitées et valeurs null illimitées, puisque null est traité comme n'importe quelle autre clé"
            },
            {
              "icon": "",
              "label": "Une clé null et une valeur null au maximum, les deux plafonnées à un"
            }
          ]
        },
        {
          "question": "Vous itérez une List avec une boucle for-each et appelez list.remove(item) directement sur la liste depuis le corps de la boucle, sur certains mais pas tous les éléments. Qu'est-ce qui se passe ?",
          "options": [
            {
              "icon": "",
              "label": "Cela fonctionne correctement et supprime exactement les éléments prévus"
            },
            {
              "icon": "",
              "label": "Cela saute silencieusement l'élément après celui supprimé, mais sinon se termine sans erreur"
            },
            {
              "icon": "",
              "label": "Cela lève IndexOutOfBoundsException une fois que la boucle atteint la taille ancienne de la liste"
            },
            {
              "icon": "",
              "label": "Cela lève ConcurrentModificationException — la modification de la structure de la liste directement pendant qu'un itérateur implicite la parcourt est détectée et rejetée ; Iterator.remove() doit être utilisé à la place"
            }
          ]
        },
        {
          "question": "Au runtime, étant donné une List<String> list, qu'est-ce que vous pouvez réellement déterminer sur son paramètre de type générique via la réflexion ou instanceof ?",
          "options": [
            {
              "icon": "",
              "label": "Vous pouvez appeler list.getElementType() pour récupérer String.class au runtime"
            },
            {
              "icon": "",
              "label": "instanceof List<String> compile et vérifie correctement le type d'élément"
            },
            {
              "icon": "",
              "label": "La JVM stocke le paramètre de type comme métadonnées cachées accessibles via list.getGenericType()"
            },
            {
              "icon": "",
              "label": "Rien — les informations de type générique sont effacées à la compilation, donc au runtime l'objet est juste une List, et il n'y a aucun moyen de récupérer si elle a été déclarée List<String> ou List<Integer>"
            }
          ]
        },
        {
          "question": "switch (day) { case 1: case 2: print(\"Weekday\"); break; case 6: print(\"Saturday\"); case 7: print(\"Sunday\"); break; default: print(\"?\"); } Si day est 6, qu'est-ce qui s'affiche ?",
          "options": [
            {
              "icon": "",
              "label": "Seulement Saturday — chaque bloc case dans un switch s'arrête toujours après sa propre instruction print"
            },
            {
              "icon": "",
              "label": "Seulement Sunday — l'appariement commence à case 6 mais seulement le dernier label correspondant avant break s'exécute"
            },
            {
              "icon": "",
              "label": "Rien ne s'affiche, parce que day 6 n'a pas de case correspondante avec son propre break directement en dessous"
            },
            {
              "icon": "",
              "label": "Saturday puis Sunday — case 6 n'a pas de break, donc l'exécution traverse dans le code du case suivant avant de frapper le break qui suit"
            }
          ]
        },
        {
          "question": "Une classe Product a besoin d'un seul ordre de tri « naturel » évident par prix, plus la capacité de trier aussi par nom ou par niveau de stock dans différents endroits de la base de code. Quelle combinaison est la bonne conception ?",
          "options": [
            {
              "icon": "",
              "label": "Implémenter Comparable<Product> trois fois séparément, une par classement, et laisser l'appelant choisir quel compareTo s'exécute"
            },
            {
              "icon": "",
              "label": "Implémenter Comparable<Product> pour le classement de prix naturel, et écrire des instances Comparator<Product> séparées pour les classements par nom et niveau de stock utilisés ailleurs"
            },
            {
              "icon": "",
              "label": "Utiliser seulement Comparator pour chaque classement, y compris le prix, puisque Comparable n'a aucun avantage ici"
            },
            {
              "icon": "",
              "label": "Utiliser seulement Comparable en ajoutant trois méthodes compareTo surchargées, une par classement"
            }
          ]
        },
        {
          "question": "Une méthode appelle new FileReader(path), qui déclare throws IOException. IOException extends Exception, pas RuntimeException. Que devez-vous faire pour que cela compile ?",
          "options": [
            {
              "icon": "",
              "label": "Rien — le compilateur n'impose cela que pour les exceptions qui extends RuntimeException"
            },
            {
              "icon": "",
              "label": "Soit attraper IOException dans un try/catch soit déclarer throws IOException sur votre propre méthode — les exceptions vérifiées doivent être traitées ou propagées explicitement, contrairement aux sous-classes unchecked RuntimeException"
            },
            {
              "icon": "",
              "label": "Envelopper l'appel dans un try/catch pour RuntimeException, puisque IOException est automatiquement unchecked dans le Java moderne"
            },
            {
              "icon": "",
              "label": "Déclarer la méthode comme static — les méthodes statiques sont exemptées du traitement des exceptions vérifiées"
            }
          ]
        },
        {
          "question": "try (Resource a = new Resource(\"A\"); Resource b = new Resource(\"B\")) { ... } utilise deux ressources implémentant AutoCloseable. Si le bloc try se termine normalement, dans quel ordre sont-elles fermées ?",
          "options": [
            {
              "icon": "",
              "label": "A est fermée en premier, puis B, en correspondant à l'ordre de déclaration"
            },
            {
              "icon": "",
              "label": "B est fermée en premier, puis A — try-with-resources ferme les ressources dans l'ordre inverse de celui dans lequel elles ont été déclarées"
            },
            {
              "icon": "",
              "label": "Les deux sont fermées simultanément, puisque try-with-resources parallélise le nettoyage"
            },
            {
              "icon": "",
              "label": "Seulement B est fermée automatiquement — A doit toujours être fermée manuellement dans un bloc finally"
            }
          ]
        },
        {
          "question": "int getValue() { try { return 1; } finally { return 2; } } Que retourne l'appel à getValue() ?",
          "options": [
            {
              "icon": "",
              "label": "1 — la valeur de retour du bloc try est déjà validée avant l'exécution de finally, donc finally ne peut pas la modifier"
            },
            {
              "icon": "",
              "label": "Cela lève une exception au runtime, parce qu'une méthode ne peut pas retourner depuis deux endroits différents"
            },
            {
              "icon": "",
              "label": "2 — une instruction return à l'intérieur de finally écrase et remplace tout retour déjà en cours depuis le bloc try, abandonnant entièrement la valeur 1"
            },
            {
              "icon": "",
              "label": "C'est une erreur de compilation — finally n'a pas le droit de contenir une instruction return"
            }
          ]
        },
        {
          "question": "try { ... } catch (IOException e) { ... } catch (FileNotFoundException e) { ... }, et FileNotFoundException extends IOException. Qu'est-ce qui se passe quand vous essayez de compiler cela ?",
          "options": [
            {
              "icon": "",
              "label": "Une erreur de compilation — le bloc catch FileNotFoundException est inatteignable parce que le bloc catch IOException plus général correspondrait déjà à chaque FileNotFoundException"
            },
            {
              "icon": "",
              "label": "Il compile correctement, et le bloc FileNotFoundException plus spécifique s'exécute chaque fois que ce type exact est levé"
            },
            {
              "icon": "",
              "label": "Il compile correctement, et les deux blocs catch s'exécutent dans l'ordre pour une FileNotFoundException"
            },
            {
              "icon": "",
              "label": "C'est correct à la compilation mais lève une erreur au runtime la première fois qu'une FileNotFoundException se produit réellement"
            }
          ]
        },
        {
          "question": "Une classe a une méthode d'instance synchronized process() et une méthode statique synchronized configure(). Spécifiquement, qu'est-ce que chacun verrouille ?",
          "options": [
            {
              "icon": "",
              "label": "process() verrouille le moniteur de l'instance d'objet spécifique sur lequel elle est appelée, tandis que configure() verrouille le moniteur de l'objet Class lui-même — partagé par chaque instance"
            },
            {
              "icon": "",
              "label": "Les deux verrouillent le même seul verrou global pour l'ensemble de la JVM, indépendamment de l'instance ou de la classe"
            },
            {
              "icon": "",
              "label": "process() verrouille l'objet Class, et configure() verrouille l'instance qui l'appelle"
            },
            {
              "icon": "",
              "label": "Ni l'un ni l'autre ne verrouille réellement quoi que ce soit sauf si un bloc synchronized est également utilisé à l'intérieur du corps de la méthode"
            }
          ]
        },
        {
          "question": "Un champ est déclaré volatile int counter = 0;, et plusieurs threads exécutent counter++ dessus simultanément. volatile empêche-t-il les mises à jour perdues ici ?",
          "options": [
            {
              "icon": "",
              "label": "Oui — volatile rend chaque opération sur le champ atomique, y compris les incrémentations"
            },
            {
              "icon": "",
              "label": "Non — volatile garantit seulement que les lectures voient la dernière écriture entre threads (visibilité) ; counter++ est une opération de lecture-modification-écriture en plusieurs étapes, et volatile ne rend aucune de ces étapes atomique"
            },
            {
              "icon": "",
              "label": "Oui, mais seulement pour les champs int et long spécifiquement, en raison de la façon dont la JVM gère les valeurs 64 bits"
            },
            {
              "icon": "",
              "label": "Non, et volatile ne garantit pas non plus la visibilité pour les types primitifs comme int"
            }
          ]
        },
        {
          "question": "Interface A et interface B chacune déclarent une méthode default describe(). Une classe implémente les deux A et B et ne redéfinit pas describe() elle-même. Qu'est-ce qui se passe ?",
          "options": [
            {
              "icon": "",
              "label": "Le compilateur choisit la version d'interface A automatiquement, puisqu'elle est listée en premier dans la clause implements"
            },
            {
              "icon": "",
              "label": "Les deux versions s'exécutent, l'une après l'autre, chaque fois que describe() est appelée"
            },
            {
              "icon": "",
              "label": "C'est correct à la compilation, mais lève une AmbiguousMethodException la première fois que describe() est appelée"
            },
            {
              "icon": "",
              "label": "Une erreur de compilation — quand deux interfaces contribuent la même méthode default, la classe implémentante doit la redéfinir elle-même pour résoudre l'ambiguïté, puisque Java ne devinera pas celle que vous entendiez"
            }
          ]
        },
        {
          "question": "list.stream().filter(x -> x > 0).map(x -> x * 2); est écrit mais le résultat n'est jamais assigné à une opération terminale comme .collect() ou .forEach(). Qu'est-ce qui se passe réellement quand cette ligne s'exécute ?",
          "options": [
            {
              "icon": "",
              "label": "Rien ne se passe aux éléments de la liste du tout — filter et map sont des opérations intermédiaires lazy qui construisent seulement une description de pipeline ; sans une opération terminale, aucune partie de ce pipeline ne s'exécute jamais réellement"
            },
            {
              "icon": "",
              "label": "Chaque élément est filtré et mappé immédiatement, exactement comme si une opération terminale était appelée"
            },
            {
              "icon": "",
              "label": "Seulement le filter s'exécute immédiatement ; map est reporté jusqu'à ce qu'une opération terminale apparaisse"
            },
            {
              "icon": "",
              "label": "Cela lève une IllegalStateException, parce qu'un pipeline stream demande une opération terminale pour compiler"
            }
          ]
        }
      ],
      "optionOrderVersion": "0154f9576054ad6f"
    }
  }
}
