Questions avancées sur Java pour les entretiens techniques

  1. Différentes méthodes de création d'objets

  1. Utilisation du mot-clé new : Cette approche permet de créer une nouvelle instance dans la mémoire heap.

  2. Via la réflexion : Le mécanisme de réflexion de Java offre la possibilité de créer dynamiquement des objets pendant l'exécution. On peut utiliser la méthode newInstance() de la classe Class ou du Constructor. ```

    // Avec la classe Class MonClasse instance = (MonClasse) Class.forName("MonClasse").newInstance();

    // Avec la classe Constructor Constructor<MonClasse> constructeur = MonClasse.class.getConstructor(); MonClasse instance = constructeur.newInstance();

  3. Par la méthode clone() : L'interface Cloneable définit une méthode clone() qui duplique un objet. La classe doit implémenter cette interface.

  4. Via la désérialisation : Un objet sérialisé peut être reconverti en utilisant la classe ObjectInputStream et sa méthode readObject().

  5. En utilisant un pattern Fabrique : Définir une classe fabrique avec des méthodes statiques permet de centraliser la logique de création et de renvoyer des types variés.

  6. En utilisant le pattern Singleton : Ce modèle garantit l'unicité d'une instance via des mécanismes comme des blocs statiques ou des méthodes de classe.

  7. Par un framework d'injection de dépendances : Des outils comme Spring gèrent automatiquement l'instanciation et l'injection des objets.

  8. Mécanisme de réflexion en Java


La réflexion est un mécanisme permettant d'inspecter et de manipuler dynamiquement les composants d'un programme (classes, méthodes, champs) à l'exécution. Elle offre une flexibilité importante pour les architectures comme les frameworks, le proxy dynamique ou le traitement des annotations.

Classes clés impliquées :

  • Class : Représente la métadonnée d'une classe. On peut obtenir ses constructeurs, méthodes et champs.
  • Constructor : Modélise un constructeur et permet de créer des instances.
  • Method : Représente une méthode et permet son invocation.
  • Field : Représente un champ et permet d'accéder à sa valeur.

Attention : l'usage de la réflexion impacte les performances et contournent les contrôles d'accès. Elle doit être employée avec discernement.

  1. Structure du framework Collections

(Contenu à détailler selon le diagramme des interfaces et classes des collections Java)

  1. Comparaison entre ArrayList et LinkedList

  • Structure de données : ArrayList repose sur un tableau dynamique, LinkedList sur une liste doublement chaînée.
  • Performances des opérations : ArrayList est rapide pour l'accès aléatoire (O(1)), mais lent pour les insertions/suppressions au milieu. LinkedList offre de meilleures performances pour les modifications structurelles au milieu, mais un accès séquentiel coûteux (O(n)).
  • Mémoire : ArrayList peut gaspiller de l'espace lors du redimensionnement. Chaque nœud de LinkedList consomme plus de mémoire pour stocker les références.
  • Sécurité des threads : Aucune des deux n'est intrinsèquement thread-safe.

Choix : privilégiez ArrayList pour les accès fréquents par index, LinkedList pour les manipulations fréquentes au milieu de la liste.

  1. Rendre un ArrayList thread-safe

  1. Avec Collections.synchronizedList : ```

    List<String> listeSynchro = Collections.synchronizedList(new ArrayList<>());

  2. En utilisant un verrou explicite (par ex. ReentrantLock) : ```

    List<String> liste = new ArrayList<>(); ReentrantLock verrou = new ReentrantLock();

    verrou.lock(); try { // Opérations sur la liste } finally { verrou.unlock(); }

  3. En utilisant CopyOnWriteArrayList (conçu pour les lectures fréquentes et rares écritures) : ```

    List<String> listeCOW = new CopyOnWriteArrayList<>();

    
    
  4. Différences entre HashMap et Hashtable


  • Synchronisation : Hashtable est thread-safe (méthodes synchronisées). HashMap n'est pas thread-safe.
  • Valeurs nulles : Hashtable interdit les clés ou valeurs null. HashMap les autorise.
  • Héritage : Hashtable hérite de la classe obsolète Dictionary. HashMap hérite d'AbstractMap.
  • Itérateur : L'itérateur de Hashtable est fail-fast (lance une exception en cas de modification concurrente).
  1. Architecture interne de HashMap

À partir de Java 8, HashMap utilise une combinaison de tableau + liste chaînée + arbre rouge-noir.

  • Le tableau principal stocke les buckets.
  • En cas de collision, une liste chaînée est utilisée jusqu'à un certain seuil (8 par défaut).
  • Au-delà du seuil, et si la capacité totale dépasse 64, la liste se transforme en arbre rouge-noir pour améliorer la complexité de recherche en O(log n).
  1. Garantie de la thread-safety dans ConcurrentHashMap

Depuis Java 8, ConcurrentHashMap abandonne le verrouillage par segment au profit d'une approche plus fine combinant :

  • Opérations CAS (Compare-And-Swap) : Pour des mises à jour atomiques sans verrou.
  • Bloc synchronized : Appliqué de manière granulaire sur les nœuds des listes ou des arbres pour les modifications structurelles.

Cette combinaison permet un accès concurrent performant avec une contention réduite.

  1. Méthodes pour créer des threads

  1. Hériter de la classe Thread et redéfinir run().

  2. Implémenter l'interface Runnable (plus flexible, car Java n'a pas d'héritage multiple).

  3. Utiliser Callable et FutureTask lorsque le thread doit retourner un résultat ou lancer une exception.

  4. Utiliser un pool de threads via le framework Executor pour gérer efficacement les ressources.

  5. Présentation du framwork Executor


Le framework Executor (paquet java.util.concurrent) sépare la soumission des tâches de leur exécution. Il fournit des pools de threads configurables.

Interfaces et classes clés :

  • Executor : Interface de base avec la méthode execute().
  • ExecutorService : Ajoute la gestion du cycle de vie, la soumission de Callable et le retour de Future.
  • Executors : Classe utilitaire pour créer des pools (ex: newFixedThreadPool, newCachedThreadPool).
  1. Concurrence vs Parallélisme

  • Concurrence : Capacité à gérer plusieurs tâches pendant le même intervalle de temps, en partageant les ressources via un mécanisme de commutation. C'est une illusion de simultanéité sur un cœur unique.
  • Parallélisme : Exécution physique simultanée de plusieurs tâches sur plusieurs cœurs ou processeurs. Cela requiert du matériel dédié.
  1. Différence entre processus et thread

  • Processus : Unité d'allocation de ressources par le système d'exploitation. Espaces mémoire isolés. Communication via IPC (Inter-Process Communication).
  • Thread : Unité d'exécution au sein d'un processus. Partagent l'espace mémoire et les ressources du processus parent. Communication directe mais nécessite une synchronisation.
  1. Context switching

Le changement de contexte est le processus par lequel le système d'exploitation sauvegarde l'état d'un thread/processus (registres, PC, etc.) et restaure l'état d'un autre. C'est un mécanisme essentiel au multitâche mais coûteux en temps et en ressources.

  1. États d'un thread

  1. NEW : Créé mais pas encore démarré.

  2. RUNNABLE : Prêt à s'exécuter ou en cours d'exécution.

  3. BLOCKED : En attente d'acquérir un verrou.

  4. WAITING : Attend indéfiniment qu'un autre thread effectue une action (ex: wait(), join()).

  5. TIMED_WAITING : Attend pendant une durée spécifiée.

  6. TERMINATED : Exécution terminée.

  7. Deadlock


Un deadlock survient lorsque deux ou plusieurs threads sont bloqués indéfiniment, chacun attendant une ressource détenue par un autre. Les quatre conditions nécessaires sont : exclusion mutuelle, détention et attente, non-préemption et attente circulaire.

  1. Comparaison entre synchronized et Lock

  • Usage : synchronized est un mot-clé intégré. Lock est une interface (ex: ReentrantLock) nécessitant un verrouillage/déverrouillage explicite.
  • Flexibilité : Lock offre des fonctions avancées comme les tentatives de verrouillage, les timeouts et l'équité (fairness).
  • Gestion des exceptions : synchronized libère automatiquement le verrou. Avec Lock, il faut le faire dans un bloc finally.
  1. AQS (AbstractQueuedSynchronizer)

AQS est un framework pour construire des synchroniseurs (verrous, sémaphores, etc.). Il maintient un état volatile (int) et une file d'attente FIFO de threads bloqués. L'état est manipulé via des opérations CAS atomiques.

  1. Exemples de synchroniseurs basés sur AQS

  • ReentrantLock : Verrou réentrant exclusif.
  • ReentrantReadWriteLock : Verrou partagé pour lecture, exclusif pour écriture.
  • Semaphore : Contrôle l'accès concurrent à une ressource limitée.
  • CountDownLatch : Permet à un ou plusieurs threads d'attendre qu'un ensemble d'opérations soit complété.
  • CyclicBarrier : Fait en sorte qu'un groupe de threads attende qu'un point de rendez-vous soit atteint.
  1. Pourquoi AQS utilise une liste doublement chaînée

La liste doublement chaînée permet de maintenir l'ordre d'attente des threads (FIFO), assure l'insertion/suppression efficace des nœuds et facilite la gestion des états des threads (cancelation, propagation pour les verrous partagés).

  1. Différence entre yield() et join()

  • yield() : Indique au scheduler que le thread courant peut céder le processeur à d'autres threads de même priorité. C'est un conseil, pas une injonction.
  • join() : Bloque le thread appelant jusqu'à la terminaison du thread cible. Utile pour la synchronisation d'exécution.
  1. Les sept paramètres d'un ThreadPoolExecutor

  1. corePoolSize : Nombre de threads à garder dans le pool.

  2. maximumPoolSize : Nombre maximum de threads autorisés.

  3. keepAliveTime : Durée pendant laquelle les threads excédentaires restent en vie avant terminaison.

  4. unit : Unité de temps pour le keepAliveTime.

  5. workQueue : File d'attente pour les tâches en attente.

  6. threadFactory : Fabrique pour créer de nouveaux threads.

  7. handler : Politique de rejet lorsque le pool et la file sont saturés (ex: AbortPolicy).

  8. Modèle de mémoire Java (JMM)


La JMM définit les règles de visibilité et d'ordonnancement des opérations mémoire entre threads. Concepts clés :

  • Mémoire principale : Mémoire partagée contenant les variables.
  • Mémoire de travail : Copie locale à chaque thread.
  • Visibilité : Le mot-clé volatile garantit que les lectures/écritures se font directement en mémoire principale.
  • Atomicité : Assurée pour les types primitifs et via les classes java.util.concurrent.atomic.
  • Barrières mémoire : Instructions qui contrôlent l'ordre des opérations (avant/après).

Étiquettes: Java collections multithreading Concurrency JVM

Publié le 12 juin à 23h36