1. Les méthodes fondamentales de la classe Object
La classe racine Object en Java fournit un ensemble de méthodes essentielles :
protected Object clone()— Produit une copie indépendante de l'instance courante.equals(Object obj)— Détermine si deux instances sont logiquement équivalentes.protected void finalize()— Invoqué par le ramasse-miettes avant la destruction de l'objet.Class> getClass()— Restitue le type d'exécution de l'objet.int hashCode()— Renvoie une valeur de hachage numérique pour l'objet.void notify()— Réveille un thread en attente sur le moniteur de cet objet.void notifyAll()— Réveille tous les threads en attente sur le moniteur de cet objet.String toString()— Fournit une représentation textuelle de l'objet.void wait()— Suspend le thread courant jusqu'à réception d'une notification.void wait(long timeout)— Suspend le thread courant avec un délai maximal d'attente.void wait(long timeout, int nanos)— Suspend le thread courant avec un délai précis en nanosecondes.
2. L'architecture des collections Java
Java organise ses collections autour de deux hiérarchies principales : Collection (pour les éléments simples) et Map (pour les paires clé-valeur). Chaque branche propose des implémentations adaptées à différents cas d'usage en termes de performance, d'ordre et de concurrence.
3. Distinction entre ArrayList et LinkedList
- Structure interne :
ArrayListrepose sur un tableau dynamique, tandis queLinkedListutilise une liste chaînée doublement liée. - Accès aléatoire : Les opérations
getetsetsont plus rapides surArrayListgrâce à l'indexation directe ;LinkedListdoit parcourir les nœuds. - Insertion/Suppression :
LinkedListexcelle pour les opérations fréquentes d'ajout ou de retrait en milieu de liste. Toutefois, pour l'insertion en fin de liste,ArrayListpeut rester compétitif. Lors d'opérations en lot à des positions aléatoires,LinkedListoffre de meilleures performances carArrayListnécessite un décalage des éléments.
4. Pourquoi HashMap combine tableau, liste chaînée et arbre rouge-noir
- Tableau (
Node[]) : Constitue le squelette de la table de hachage. L'index d'insertion est déterminé par le code de hachage de la clé. - Liste chaînée : Gère les collisions de hachage. Lorsque plusieurs clés produisent le même index, elles sont chaînées ensemble dans ce même compartiment.
- Arbre rouge-noir : Introduit dans Java 8, il remplaec la liste chaînée au-delà de 8 nœuds, faisant passer la complexité de recherche de O(n) à O(log n).
- Rationale : Sans cette optimisation, une accumulation de collisions produirait des chaînes très longues, dégradant fortement les performances de recherche. L'arbre rouge-noir offre une solution équilibrée entre coût de maintenance et rapidité d'accès.
5. Comparaison entre HashMap et Hashtable
| Critère | HashMap | Hashtable |
|---|---|---|
| Thread-safety | Non synchronisé | Synchronisé (méthodes synchronized) |
| Clé/Valeur null | Une clé null autorisée, plusieurs valeurs null | Aucun null autorisé |
| Capacité par défaut | 16 | 11 |
| Agrandissement | Doublement de la capacité (puissance de 2) | 2×capacité + 1 |
| Exigence de taille | Doit être une puissance de 2 | Aucune contrainte de puissance de 2 |
6. Les différentes approches pour créer un thread
Java offre quatre mécanismes principaux pour exécuter du code en parallèle :
- Hériter de la classe
Thread - Implémenter l'interface
Runnable - Utiliser
CallableavecFuture(résultat de retour possible) - Recourir à un pool de threads via
ExecutorService
import java.util.concurrent.*;
public class DemoThreads {
public static void main(String[] args) throws Exception {
// Approche 1 : Héritage de Thread
MonThread t1 = new MonThread();
t1.start();
t1.join();
// Approche 2 : Implémentation de Runnable
Thread t2 = new Thread(new MaTacheRunnable());
t2.start();
t2.join();
// Approche 3 : Callable avec FutureTask
FutureTask<String> resultat = new FutureTask<>(new MaTacheCallable());
new Thread(resultat).start();
System.out.println("Résultat Callable : " + resultat.get());
// Approche 4 : ExecutorService
ExecutorService svc = Executors.newFixedThreadPool(3);
svc.submit(t1);
svc.shutdown();
svc.awaitTermination(5, TimeUnit.SECONDS);
}
}
class MonThread extends Thread {
@Override
public void run() {
System.out.println("Exécuté via héritage Thread : " + getName());
}
}
class MaTacheRunnable implements Runnable {
@Override
public void run() {
System.out.println("Exécuté via Runnable : " + Thread.currentThread().getName());
}
}
class MaTacheCallable implements Callable<String> {
@Override
public String call() {
return "Valeur retournée par Callable";
}
}
7. Cycle de vie d'un thread en Java
- NEW : L'objet thread est instancié mais
start()n'a pas encore été appelé. - RUNNABLE : Après l'appel à
start(), le thread est prêt à être planifié par l'ordonnanceur. - RUNNING : Le thread occupe effectivement un processeur et exécute son code.
- BLOCKED : Le thread est suspendu pour l'une des raisons suivantes :
wait()— attente d'une notification- Blocage sur un verrou
synchronizeddétenu par un autre thread sleep(),join()ou requête E/S en cours
- TERMINATED : La méthode
run()s'est terminée normalement ou par exception.
8. Les types de flux en Java (I/O)
Java distingue les flux d'octets (InputStream, OutputStream) des flux de caractères (Reader, Writer). Chaque catégorie se décline en flux de base et flux décorateurs (tamponnés, filtrés, de données, d'objets, etc.).
9. Exceptions RuntimeException fréquemment rencontrées
- NullPointerException : Invocation sur une référence non initialisée.
- ClassNotFoundException : Classe introuvable lors du chargement dynamique.
- NumberFormatException : Conversion invalide d'une chaîne en valeur numérique.
- IndexOutOfBoundsException : Accès à un indice hors limites d'un tableau ou d'une liste.
- IllegalArgumentException : Argument transmis incompatible avec la méthode.
- ClassCastException : Conversion de type incorrecte à l'exécution.
10. Le mécanisme de réflexion en Java
La réflexion permet d'inspecter et de manipuler dynamiquement les classes, méthodes, champs et constructeurs à l'exécution. Elle s'appuie sur quatre classes fondamentales : Class, Constructor, Field et Method.
Utilisations courantes :
- Identifier la classe d'un objet à l'exécution.
- Instancier dynamiquement des objets.
- Accéder aux membres (même privés) d'une classe.
- Invoquer des méthodes par leur nom, résolu à l'exécution.
11. Sérialisation en Java
La sérialisation transforme un objet en un flux d'octets, facilitant sa persistance ou sa transmission réseau. Pour rendre une classe sérialisable, il suffit d'implémenter l'interface marquée Serializable. On utilise ensuite ObjectOutputStream pour écrire et ObjectInputStream pour restaurer l'état de l'objet.
12. Codes de statut HTTP courants
| Code | Signification |
|---|---|
| 200 | Requête traitée avec succès |
| 301 | Redirection permanente vers une nouvelle URL |
| 302 | Redirection temporaire |
| 400 | Requête mal formée |
| 401 | Authentification requise |
| 403 | Accès refusé par le serveur |
| 404 | Ressource introuvable |
| 500 | Erreur interne du serveur |
| 503 | Service temporairement indisponible |
13. Différences entre GET et POST
- Placement des données : GET encode les paramètres dans l'URL (après
?), POST les inclut dans le corps de la requête. - Limité de taille : La taille de GET dépend de la limite imposée par le navigateur/serveur sur la longueur de l'URL (environ 2083 caractères pour IE). POST n'a pas de restriction intrinsèque du protocole.
- Sécurité : GET expose les données dans l'URL, ce qui les rend visibles dans l'historique du navigateur et les journaux serveur. POST offre une meilleure confidentialité pour les données sensibles.
- Sémantique : GET est idempotent et destiné à la récupération de données, POST est utilisé pour soumettre ou modifier des données.
14. Différences entre Cookie et Session
- Stockage : Les cookies résident côté client (navigateur), les sessions côté serveur.
- Persistance : Les sessions survivent à la navigation entre pages. Les cookies peuvent être configurés avec une date d'expiration.
- Données : Les sessions acceptent tout objet Java, les cookies ne stockent que des chaînes de caractères.
- Fonctionnement sans cookies : Les sessions peuvent fonctionner via réécriture d'URL si les cookies sont désactivés.
Chapitre — Concepts avancés Java
15. Analyse approfondie de HashMap
Avant Java 8, HashMap reposait sur un tableau de nœuds chaînés. Dès Java 8, un arbre rouge-noir remplace la liste chaînée lorsque celle-ci dépasse 8 éléments et que la taille du tableau atteint au moins 64. En deçà de 64 compartiments, on privilégie un redimensionnement du tableau plutôt qu'une conversion en arbre, car les rotations et recolorations de l'arbre seraient disproportionnées par rapport au gain.
16. Les zones mémoire de la JVM
- Heap (tas) : Mémoire partagée par tous les threads, lieu principal d'allocation des objets et cible du ramasse-miettes. Divisée en young generation (Eden, Survivor) et old generation.
- Stack (pile) : Mémoire privée à chaque thread. Chaque appel de méthode crée un cadre (stack frame) contenant les variables locales, la pile d'opérandes et les références dynamiques.
- Method Area : Stocke les métadonnées des classes chargées, les constantes et les variables statiques. Également appelée « permanent generation » avant Java 8.
- Native Method Stack : Pile dédiée aux méthodes natives.
- Program Counter : Registre privé par thread indiquant l'adresse de l'instruction bytecode en cours d'exécution.
17. Algorithmes de ramasse-miettes
- Copying (copie) : Utilisé dans la young generation. Les objets survivants sont copiés d'un espace Survivor à l'autre. Rapide mais consomme de la mémoire.
- Mark-Sweep (marquage-balayage) : Marque les objets atteignables puis libère les non-marqués. Génère de la fragmentation.
- Mark-Compact (marquage-compactage) : Combiné avec le balayage, mais déplace les objets survivants pour éliminer la fragmentation. Plus lent en raison des déplacements.
18. Détermination de l'état d'un objet par le GC
- Comptage de références : Chaque objet possède un compteur incrémenté à chaque nouvelle référence et décrémenté à chaque suppression. Un compteur à zéro signifie l'éligibilité au GC. Défaut majeur : ne gère pas les références circulaires.
- Atteignabilité (GC Roots) : L'algorithme part de racines prédéfinies (variables locales de la pile, attributs statiques, constantes, références JNI) et marque tous les objets accessibles. Tout objet non atteint est collecté. C'est l'approche utilisée par les JVM modernes.
19. StackOverflowError et OutOfMemoryError
StackOverflowError survient typiquement lors de récursions infinies, d'une profondeur excessive d'appels ou de tableaux locaux très volumineux dans la pile.
OutOfMemoryError se produit quand le tas est saturé : fuite mémoire, collections contenant des références libérées tardivement, ou paramètres de mémoire insuffisants.
Outils de diagnostic : jvisualvm, jmap, jhat permettent d'analyser les dumps mémoire.
public class ErreursMemoire {
private static int profondeur = 0;
public static void main(String[] args) {
// Décommenter pour simuler un StackOverflowError
// provoquerStackOverflow();
provoquerOutOfMemory();
}
static void provoquerStackOverflow() {
profondeur++;
provoquerStackOverflow();
}
static void provoquerOutOfMemory() {
java.util.List<byte[]> liste = new java.util.ArrayList<>();
while (true) {
liste.add(new byte[1024 * 1024]);
}
}
}
20. Les pools de threads
Un pool de threads maintient un ensemble réutilisable de threads pour éviter le coût de création/destruction répété. Les fabriques standard fournies par Executors sont :
newCachedThreadPool()— Crée des threads à la demande, les recycle après 60 secondes d'inactivité.newFixedThreadPool(n)— Pool à taille fixe de n threads.newSingleThreadExecutor()— Un unique thread exécutant les tâches séquentiellement.newScheduledThreadPool(n)— Pool supportant l'exécution différée et périodique.
Toutes ces fabriques reposent sur ThreadPoolExecutor. Les bonnes pratiques recommandent d'instancier directement ThreadPoolExecutor pour un contrôle fin des paramètres.
21. Raisons d'utiliser un pool de threads
- Réduction des ressources : Réutilisation des threads existants.
- Réactivité accrue : Les tâches s'exécutent immédiatement sans attendre la création d'un thread.
- Gestion centralisée : Limitation du nombre de threads actifs, surveillance et ajustement possibles.
22. Fonctionnement interne d'un ThreadPoolExecutor
- Au démarrage, le pool est vide de threads.
- Lors de la soumission d'une tâche via
execute(), si le nombre de threads courants est inférieur àcorePoolSize, un nouveau thread est créé. - Si le seuil de cœurs est atteint, la tâche est placée dans la file d'attente.
- Si la file est pleine et que le nombre de threads est inférieur à
maximumPoolSize, un thread supplémentaire est créé. - Si la file est pleine et le pool à son maximum, la politique de rejet est appliquée (par défaut :
AbortPolicy).
23. Paramètres de ThreadPoolExecutor
corePoolSize— Nombre de threads permanents (survivent même inactifs siallowCoreThreadTimeOutest faux).maximumPoolSize— Nombre maximal de threads autorisés.keepAliveTime— Durée avant recyclage des threads excédentaires.unit— Unité de temps pourkeepAliveTime.workQueue— File d'attente des tâches (ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue).threadFactory— Fabrique de threads personnalisable.handler— Politique de rejet.
Politiques de rejet :
AbortPolicy— LèveRejectedExecutionException(défaut).CallerRunsPolicy— Exécute la tâche dans le thread appelant.DiscardOldestPolicy— Abandonne la tâche la plus ancienne en attente.DiscardPolicy— Ignore silencieusement la tâche rejetée.
Dimensionnement : Pour des tâches intensives en CPU, on recommande nombre de cœurs + 1 threads. Pour des tâches d'E/S, 2 × nombre de cœurs est un bon point de départ. Une formule plus précise :
nbThreads = ((tempsAttente + tempsCPU) / tempsCPU) × nbCœurs
24. Conteneurs thread-safe courants
ConcurrentHashMap— Verrouillage par segment / par nœud selon la version.CopyOnWriteArrayList— Copie complète du tableau à chaque écriture.CopyOnWriteArraySet— Même principe, pour un ensemble sans doublons.
25. Classes atomiques (java.util.concurrent.atomic)
Ces classes garantissent des opérations atomiques sans synchronized, en s'appuyant sur les instructions CAS (Compare-And-Swap) du processeur. Parmi les plus utilisées :
AtomicInteger,AtomicLong,AtomicBoolean— Types primitifs atomiques.AtomicReference— Référence d'objet atomique.AtomicStampedReference— Référence avec version, résout le problème ABA.
Le fonctionnement interne combine CAS + volatile + méthodes natives de Unsafe pour garantir la visibilité et l'atomicité.
26. Synchronized vs Lock : implémentations et différences
Synchronized : Le JVM utilise les moniteurs. Pour un bloc, les bytecode monitorenter et monitorexit sont insérés. Pour une méthode, l'attribut ACC_SYNCHRONIZED est vérifié. Le verrouillage et le déverrouillage sont entièrement gérés par la JVM.
Lock (ReentrantLock) : Implémentation purement Java reposant sur un état atomique et une file d'attente de threads liée. Utilise intensivement CAS + spin.
| Critère | synchronized | Lock |
|---|---|---|
| Niveau | Mot-clé / JVM | API Java |
| Libération | Automatique | Manuelle (unlock() dans finally) |
| Fairness | Toujours non-équitable | Choix équitable ou non |
| Interruption | Bloquant sans échappatoire | lockInterruptibly() disponible |
| Conditions | Une seule file de conditions | Plusieurs objets Condition |
| Mode | Exclusif uniquement | Exclusif et partagé (ReadWriteLock) |
27. ConcurrentHashMap : supériorité sur Hashtable
Hashtable synchronise chaque opération sur l'objet entier, créant un goulot d'étranglement en concurrence élevée. ConcurrentHashMap réduit la granularité du verrouillage :
- Java 7 : Verrouillage par segment (
Segmenthéritant deReentrantLock). Chaque segment contrôle une portion du tableau. - Java 8 : Abandon des segments au profit d'un verrouillage par nœud (premier élément d'un compartiment) avec
synchronized+ CAS. La structure est identique à HashMap (tableau + chaîne + arbre rouge-noir).
28. Le mot-clé volatile
volatile garantit la visibilité : toute modification d'une variable volatile est immédiatement visible aux autres threads. Il empêche également le réordonnancement d'instructions autour de l'accès à la variable. En revanche, il ne fournit pas d'atomicité pour les opérations composées (ex. : i++).
29. volatile vs synchronized
| Critère | volatile | synchronized |
|---|---|---|
| Portée | Variables uniquement | Variables, méthodes, blocs |
| Visibilité | Oui | Oui |
| Atomicité | Non | Oui |
| Blocage | Aucun | Possible (attente du verrou) |
| Optimisation JIT | Empêchée sur la variable | Autorisée |
30. Processus de chargement des classes
- Chargement : Lecture du bytecode, création du
Classen mémoire. - Vérification : Validation du format des octets, de la cohérence sémantique et de la validité du bytecode.
- Préparation : Allocation mémoire pour les variables statiques et initialisation aux valeurs par défaut.
- Résolution : Conversion des références symboliques en références directes.
- Initialisation : Exécution des blocs
staticet affectation des valeurs initiales des variables statiques.
31. Les chargeurs de classes (ClassLoaders)
- Bootstrap ClassLoader : Charge les classes fondamentales du JDK (
java.lang,java.util…). Écrit en C++. - Extension ClassLoader : Charge les bibliothèques des extensions Java.
- Application ClassLoader : Charge les classes du classpath de l'application.
- ClassLoaders personnalisés : Sous-classe de
ClassLoaderpour des besoins spécifiques (chargement distant, chiffrement…).
Un classe est chargée à la première utilisation : instanciation, accès à un champ statique, appel de méthode statique, réflexion ou initialisation d'une sous-classe.
32. Stratégie d'allocation mémoire et collecte
- Eden : Les nouveaux objets sont créés ici. Le Minor GC y est fréquent et rapide.
- Survivor : Les objets survivants au Minor GC sont déplacés d'un espace Survivor à l'autre.
- Old Generation : Les objets ayant survécu à plusieurs cycles de Minor GC y sont promus. Le Major GC (Full GC) y est moins fréquent mais plus coûteux.
- Les objets volumineux peuvent être directement alloués dans la old generation.
33. Diagnostic des deadlocks
public class ExempleDeadlock {
private static final Object VERROU_A = new Object();
private static final Object VERROU_B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (VERROU_A) {
System.out.println(Thread.currentThread().getName() + " détient VERROU_A");
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
synchronized (VERROU_B) {
System.out.println(Thread.currentThread().getName() + " détient VERROU_B");
}
}
}, "Thread-Alpha");
Thread t2 = new Thread(() -> {
synchronized (VERROU_B) {
System.out.println(Thread.currentThread().getName() + " détient VERROU_B");
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
synchronized (VERROU_A) {
System.out.println(Thread.currentThread().getName() + " détient VERROU_A");
}
}
}, "Thread-Beta");
t1.start();
t2.start();
}
}
Diagnostic :
- Identifier le PID du processus Java avec
jps. - Exécuter
jstack <PID>pour obtenir un instantané de tous les threads. Le rapport indique explicitement les threads impliqués dans un deadlock avec les verrous qu'ils détiennent et ceux qu'ils attendent.