☕ Java – La Plateforme "Write Once, Run Anywhere"
Guide complet IDEO-Lab : JVM, SDK/JRE, OOP, Collections & Spring Boot.
La JVM & le Bytecode
"Write Once, Run Anywhere" (WORA). .java -> .class.
Pourquoi Java ?
Portabilité (JVM), Écosystème (Enterprise), OOP, Garbage Collector.
Portabilité Garbage CollectorJDK vs JRE & Installation
SDK (Compiler javac) vs JRE (Exécuter java). OpenJDK.
Build Tools (Maven/Gradle)
pom.xml (Maven) ou build.gradle (Gradle) pour les dépendances.
Types Primitifs & final
int, double, boolean, char. final (immuable).
Types Objets & null
String, Integer (Wrappers). Le danger de null.
Structures de Contrôle
if/else, for, while, switch, for-each.
Méthodes & main
public static void main(String[] args). static vs instance.
OOP : Classes & Objets
class User { ... }, User u = new User();, Constructeurs.
OOP : Héritage
class Admin extends User, super(), @Override.
OOP : Encapsulation
public, private, protected. Getters & Setters.
OOP : Abstraction
abstract class vs interface. implements.
Collections Framework
List, Map, Set (ArrayList, HashMap).
Gestion des Erreurs
try-catch-finally, Exceptions (Checked vs Unchecked).
Generics (Génériques)
List, Map. Sécurité de type.
Concurrence
Thread, Runnable, synchronized, java.util.concurrent.
Écosystème : Spring Boot
Framework N°1 (Web, Microservices). Injection de dépendances.
Spring Boot WebCheat-sheet (Syntaxe)
Syntaxe de base (class, main, List, Map).
Logiciels utilisant Java
Elasticsearch, Kafka, Minecraft, Applications Android.
Elasticsearch Kafka AndroidLiens Utiles
Documentation officielle, Baeldung, SDKMAN, Temurin.
Documentation BaeldungObjectif : te donner une vision “carte mentale” de Java en 2026 : le centre (JDK/JRE, JVM, bytecode), puis les continents (Java SE, Jakarta EE, Spring), puis les grands axes (backend, data, cloud, mobile, desktop, big data, observability, security), et enfin une méthode simple pour choisir une stack selon contexte.
Java “au centre” : la mécanique
- Source :
.java - Compilation :
javac→.class(bytecode) - Exécution : JVM (
java) charge les classes et exécute - Runtime : GC, JIT, threads, classloading, sécurité
- APIs standard : Java SE (
java.lang,java.util,java.time, etc.)
Définitions (courtes et propres)
| Terme | Signification |
|---|---|
| JDK | Kit de dev : javac + outils + runtime |
| JRE | Runtime : JVM + libs (souvent inclus dans le JDK moderne) |
| JVM | Machine virtuelle : exécute le bytecode + gère mémoire/threads |
| Bytecode | Format intermédiaire portable (.class) |
| WORA | Write Once, Run Anywhere (via JVM) |
Carte mentale (format “bulles”)
| Niveau | Éléments | Exemples |
|---|---|---|
| Centre | JDK/JRE, JVM, bytecode, libs standard | GC, JIT, collections, concurrency |
| Cadres | Java SE / Jakarta EE / Spring | Servlet, JPA, DI, MVC |
| Écosystème | Build, test, libs, observability | Maven/Gradle, JUnit, logging, metrics |
| Plateformes | Backend, data, cloud, mobile, desktop | REST, Kafka, Android, tooling |
| Ops | Déploiement, monitoring, sécurité | containers, health checks, auth |
✅ Message clé : Java n’est pas “juste un langage” : c’est une plateforme runtime + un écosystème de production.
Java SE
Java SE = le socle : langage + JVM + bibliothèques standard. Tout le reste se base dessus.
- Collections, I/O, HTTP client, concurrency, crypto, time API
- Outils :
javac,java,jcmd,jstack…
Jakarta EE (ex Java EE)
Jakarta EE = spécifications enterprise (servlets, JPA, CDI, etc.) souvent utilisées avec des app servers.
- Approche “standardisée” via specs
- Souvent présent en environnements corporate/legacy/regulés
- Déploiement typique : serveur d’applications + WAR/EAR (selon)
Spring world
Spring = écosystème pragmatique orienté productivité. Spring Boot simplifie : auto-config, starters, packaging, observabilité, intégrations.
- DI/IoC (core), Web (MVC), Data (repositories), Security, Actuator
- Approche “application” plutôt que “app server lourd” (souvent)
- Microservices-friendly (configuration, health, metrics)
Comparaison rapide
| Monde | Force | Quand c’est un bon choix |
|---|---|---|
| Java SE | Fondation + perf + stabilité | Partout (base obligatoire) |
| Jakarta EE | Specs enterprise standardisées | Corporate/legacy, standards imposés |
| Spring | Productivité + écosystème complet | API, microservices, time-to-market |
✅ En pratique : beaucoup de stacks modernes = Java SE + Spring Boot.
Les grands axes de l’écosystème
| Axe | Objectif | Exemples typiques |
|---|---|---|
| Backend | APIs, services, microservices | REST, DI, transactions, auth |
| Data | Stockage, traitement, pipelines | ORM, SQL, caching, batch |
| Cloud | Déploiement, scalabilité, ops | containers, health, autoscaling |
| Mobile | Apps Android | clients API, offline, auth |
| Desktop | Outils & apps desktop | IDEs, clients, tooling |
| Big Data | Streaming/batch | Kafka, pipelines data |
| Observability | Monitoring, traces, logs | metrics, health, profiling |
| Security | Protection & compliance | authN/authZ, secrets, audit |
✅ Tu peux “entrer” dans Java par n’importe quel axe, mais le socle (JVM/SE) reste identique.
Un schéma d’architecture “typique” (concept)
// Typical modern stack:
// Client (web/mobile) -> API Gateway -> Spring Boot services -> DB/Cache
// Async: services -> Kafka -> consumers -> search index / analytics
// Observability: metrics/logs/traces -> dashboards/alertsCe que ça implique côté compétences
- Code : Java SE + patterns (OOP, concurrency)
- Framework : Spring Boot + Data + Security
- Ops : config, scaling, monitoring, incident response
- Data : transactions, indexes, caches, streaming
OpenJDK + distributions (vendors)
Le cœur de Java est OpenJDK (open-source). Plusieurs vendors fournissent des distributions (builds) et parfois du support, des patchs, des politiques LTS.
Ce qui change selon vendor
- Cycle de support (durée, patchs)
- Packaging (install, images container)
- Support entreprise (SLA, security patches)
- Options/diagnostics (selon outils inclus)
✅ Côté dev, la plupart du temps : compat très forte. Côté prod : support et politique LTS comptent.
Règles simples de sélection (prod)
| Contexte | Recommandation | Pourquoi |
|---|---|---|
| Projet standard | OpenJDK distribution stable (ex: Temurin) | Compat + stabilité + patchs |
| Entreprise régulée | Vendor avec support long | SLA + compliance + patching |
| High-scale | Focus tuning + observability | GC, heap, profiling, perf |
Règle de version
Privilégie une version LTS moderne (ex: 17/21) plutôt qu’un vieux runtime, sauf contrainte legacy.
Méthode simple : 6 questions
- Quel type d’application ? (API, batch, streaming, mobile…)
- Quel niveau de contraintes ? (sécurité, audit, conformité)
- Quel niveau de charge ? (QPS, latence p95/p99, pics)
- Quel mode de déploiement ? (VM, containers, K8s)
- Quel niveau de legacy ? (Java 8, app server, monolithe)
- Quelle équipe ? (taille, expertise, time-to-market)
Décisions qui en découlent
- Monolithe modulaire vs microservices
- Spring Boot vs stack Jakarta EE imposée
- Sync REST vs async events (Kafka)
- Observability “minimum” vs “hardcore”
Table de choix (très pratique)
| Besoin | Choix typique | Notes |
|---|---|---|
| API REST rapide | Spring Boot Web | Time-to-market |
| Data transactionnelle | Spring Data JPA + DB | Transactions, migrations |
| Events & async | Kafka + consumers | Découplage, throughput |
| Recherche full-text | Elasticsearch | Indexation séparée |
| Auth/roles | Spring Security | Centraliser règles |
| Prod monitoring | Actuator + metrics/logs | Health checks |
✅ L’idée : une stack minimale mais complète, puis tu ajoutes les briques selon les besoins réels.
Archétype 1 : Startup (time-to-market)
- Objectif : livrer vite, maintenir simple
- Choix : Spring Boot monolithe modulaire
- Data : DB + migrations, cache si besoin
- Observability : health + logs + metrics de base
Archétype 2 : Regulated (banque/defense/health)
- Objectif : audit, compliance, sécurité, traçabilité
- Choix : standards, durcissement, contrôle versions
- Security : policies strictes, secrets management, logs audit
- Ops : patching, support long, environnements verrouillés
Archétype 3 : High-scale (latence & débit)
- Objectif : p95/p99 bas, throughput élevé
- Choix : microservices si nécessaire, sinon monolithe performant
- Async : Kafka / queues, backpressure
- Perf : thread pools, GC tuning, profiling, caches
Archétype 4 : Legacy (contraintes historiques)
- Objectif : moderniser sans casser
- Choix : upgrade progressif (JDK LTS), refactor par modules
- Encapsulation : facades, adapters
- Strangler pattern : extraire progressivement des services
✅ Le bon choix n’est pas “la meilleure techno”, c’est “la meilleure trajectoire”.
Checklist stack (backend Java)
| Question | Oui | Non |
|---|---|---|
| API publique à sécuriser ? | Security + rate limit + audit | Security simple |
| Latence critique (p99) ? | Perf + profiling + caches | Stack standard |
| Événements / async requis ? | Kafka + consumers | REST sync |
| Recherche full-text ? | Search engine | DB queries |
| Conformité forte ? | Policies + support + audit | Light governance |
Anti-patterns fréquents
- Microservices trop tôt (complexité ops)
- Pas d’observability (aveugle en prod)
- Pas de timeouts/retries (threads bloqués)
- Versioning non maîtrisé (JDK/libs)
La formule finale (simple)
1) Commence par une stack minimale stable.
2) Ajoute les briques uniquement quand un besoin réel apparaît (scalabilité, compliance, search, async).
3) Instrumente tôt (health/metrics/logs).
4) Fixe les versions (JDK, dépendances) et automatise les tests.
Mini “template stack” (par défaut)
| Brique | Choix typique | Pourquoi |
|---|---|---|
| Runtime | JDK LTS | Support long + stabilité |
| Framework | Spring Boot | Prod-ready + rapide |
| Data | JPA + DB | Transactions + productivité |
| Security | Security | Centralisation règles |
| Obs | Actuator + logs | Exploitation prod |
✅ Ensuite seulement : Kafka, Elasticsearch, caches avancés, etc.
1️⃣ Java : philosophie & promesse
Java est un langage orienté objet, fortement typé et compilé. Sa promesse fondatrice est le principe :
WORA – Write Once, Run Anywhere
Contrairement aux langages compilés natifs (C/C++), Java ne compile pas directement en code machine. Il génère un Bytecode intermédiaire portable.
2️⃣ Processus de compilation
Étape 1 : Compilation via javac Étape 2 : Exécution via java (JVM)
Hello.java
|
| javac
v
Hello.class (Bytecode portable)
|
| JVM
v
Code natif spécifique à l’OS
3️⃣ Pourquoi le Bytecode ?
- Portabilité multi-OS
- Isolation mémoire
- Sécurité sandbox
- Optimisation JIT dynamique
- Instrumentation runtime possible
4️⃣ Structure interne de la JVM
- Class Loader – Charge les classes
- Bytecode Verifier – Vérifie la sécurité
- Runtime Data Areas – Heap / Stack / Metaspace
- Execution Engine – Interpréteur + JIT
- Garbage Collector – Gestion mémoire
5️⃣ Mémoire JVM (Architecture simplifiée)
+----------------------------+
| Heap Memory |
| (Objets, Instances) |
+----------------------------+
| Metaspace |
| (Metadata des classes) |
+----------------------------+
| Stack Threads |
| (Frames, variables locales)|
+----------------------------+
| Program Counter Register |
+----------------------------+
⚠️ Chaque thread possède sa propre Stack. Le Heap est partagé entre tous les threads.
6️⃣ Interpréteur vs JIT Compiler
La JVM démarre en mode interprété puis identifie les “Hot Spots” (code fréquemment exécuté).
Ces sections sont compilées en natif via le JIT (Just-In-Time Compiler).
- Optimisation dynamique
- Inlining
- Loop unrolling
- Escape analysis
7️⃣ Garbage Collection (GC)
Java automatise la gestion mémoire via un Garbage Collector.
- Young Generation
- Old Generation
- Minor GC
- Major GC
- G1 / ZGC / Shenandoah
8️⃣ Exemple minimal
public class Hello {
public static void main(String[] args) {
System.out.println("Hello JVM");
}
}
Compilation : javac Hello.java
Exécution : java Hello
🎯 Résumé clé
- Java compile en Bytecode portable
- La JVM exécute et optimise dynamiquement
- La gestion mémoire est automatique
- La performance repose sur JIT + GC
- WORA est rendu possible par l’abstraction JVM
1️⃣ Portabilité structurelle
Java repose sur une séparation stricte :
- Langage
- Bytecode
- Machine virtuelle (JVM)
Cette abstraction garantit :
- Indépendance OS
- Déploiement homogène
- Migration simplifiée
- Isolation du hardware
2️⃣ Modularité & packaging
- JAR / WAR packaging
- Modules (JPMS)
- Classpath maîtrisé
3️⃣ Architecture propre
Java favorise les architectures structurées :
- Clean Architecture
- Hexagonal Architecture
- Layered Architecture
- Domain Driven Design
4️⃣ Déploiement Cloud-ready
- Docker compatible
- Kubernetes ready
- 12-Factor Apps
- Microservices natifs
1️⃣ Garbage Collector
Java supprime la gestion manuelle mémoire.
- Minor GC
- Major GC
- G1
- ZGC
- Shenandoah
2️⃣ Sécurité mémoire
- Pas de pointeurs directs
- Vérification Bytecode
- Isolation sandbox
- Typage strict
3️⃣ Concurrency maîtrisée
- Thread model robuste
- Executors
- Fork/Join
- Virtual Threads (Loom)
4️⃣ Outils d'analyse mémoire
- Heap dump
- Thread dump
- JFR
- JMX
1️⃣ JIT Compiler
- Hotspot detection
- Inlining automatique
- Loop optimizations
- Escape analysis
2️⃣ Optimisation adaptative
La JVM observe le comportement réel et optimise dynamiquement.
3️⃣ Scalabilité
- Thread pools
- Reactive programming
- Backpressure
- Event-driven architecture
4️⃣ Benchmarks
Java rivalise avec C++ sur de nombreux workloads backend.
1️⃣ Frameworks
- Spring Boot
- Spring Security
- Spring Data
- Hibernate
2️⃣ Data
- JPA
- Transactions ACID
- Connection pooling
3️⃣ Messaging & Streaming
- Kafka
- RabbitMQ
- JMS
4️⃣ DevOps
- Maven / Gradle
- CI/CD
- Monitoring
1️⃣ Standard industriel
- Banques
- Assurances
- Industrie
- Défense
2️⃣ Long Term Support
- Versions LTS
- Compatibilité ascendante
3️⃣ Observabilité avancée
- Metrics
- Tracing
- Profiling
- Monitoring GC
4️⃣ Maintenabilité long terme
Java est conçu pour durer 10 à 20 ans en production.
JRE : exécuter
Le JRE (Java Runtime Environment) contient ce qui est nécessaire pour lancer une application Java : JVM + bibliothèques standard.
- Usage : exécuter un programme existant
- Commande principale :
java - Inclut : runtime, class libraries
- N’inclut pas : compilateur, outils dev
JDK : développer
Le JDK (Java Development Kit) contient tout le JRE + les outils pour compiler, packager, diagnostiquer. En pratique : installe toujours un JDK, même si tu “ne fais que lancer” (tu éviteras des surprises tooling).
javac(compiler)jar(packager)javadoc(doc)jdb(debug)jcmd,jstack,jmap(diagnostics)
Chaîne minimale “compile & run”
# Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, Java");
}
}
# Compile (JDK)
javac Hello.java
# Run (JRE or JDK)
java HelloRésumé opérationnel
| Besoin | Installer | Pourquoi |
|---|---|---|
| Lancer un jar en prod | JDK (recommandé) | Outils + diagnostics + compat |
| Compiler / dev | JDK | javac + tooling |
| CI/CD build | JDK | Maven/Gradle + tests |
⚠️ Remarque : historiquement “JRE séparé” existait beaucoup. Aujourd’hui, la pratique moderne est : JDK partout (dev, CI, serveurs), pour simplifier et fiabiliser.
OpenJDK : le standard
Pour la majorité des projets, une distribution OpenJDK suffit largement : stable, maintenue, compatible avec l’écosystème.
Versions recommandées (LTS)
En entreprise, on privilégie les versions LTS (support long terme) pour réduire le risque et stabiliser la prod. Aujourd’hui, les LTS communément reconnues incluent 8, 11, 17, 21, 25.
- 17 : base solide encore très répandue
- 21 : excellent choix “modern” pour Spring Boot récent
- 25 : LTS plus récent (upgrade planifié si besoin)
Quelle distribution choisir ?
- Eclipse Temurin : très utilisé, “default safe choice”
- Amazon Corretto : solide, très répandu en environnements AWS
L’important est surtout : standardiser (une distro), geler (une LTS), et automatiser (CI/CD).
Règles simples (entreprise)
| Contexte | Choix | Justification |
|---|---|---|
| Nouveau projet Spring | JDK 21 LTS | Équilibre features / support |
| Legacy stable | JDK 17 LTS | Compat + migration progressive |
| Très ancien SI | JDK 8/11 (si contraintes) | Plan d’upgrade recommandé |
Piège classique : “ça marche en local, pas en CI”
Cause fréquente : versions Java différentes entre poste, CI, et serveurs. Solution : toolchains (Maven/Gradle) + version pinning + scripts standard.
✅ Objectif : “same JDK, same build, same result”.
SDKMAN : gérer plusieurs JDK proprement
Sur Linux/macOS, SDKMAN est la méthode la plus pratique pour installer et switcher de version Java (comme nvm/pyenv).
Installation SDKMAN
# Install SDKMAN
curl -s "https://get.sdkman.io" | bash
# Reload shell
source "$HOME/.sdkman/bin/sdkman-init.sh"
# Verify
sdk versionInstaller un JDK (ex: Temurin 21)
# List candidates (vendors + versions)
sdk list java
# Install an LTS build (example identifier)
sdk install java 21.0.4-tem
# Use it in current shell
sdk use java 21.0.4-tem
# Set default for all new shells
sdk default java 21.0.4-tem✅ Avantage : tu peux garder plusieurs JDK (17/21/25) et basculer en 2 secondes.
Check rapide après installation
java -version
javac -version
which java
which javacBonnes pratiques
- Standardise une LTS pour chaque projet
- Documente la version (README, CI)
- Évite d’installer “Java” via packages OS si tu dois switcher souvent
- Utilise
.sdkmanrcpar projet (version locale)
Version locale par projet (optionnel mais pro)
# In project directory
sdk env init
# Edit .sdkmanrc, then:
sdk envApproche recommandée (simple et robuste)
Sur Windows, le plus important est : JAVA_HOME + PATH corrects. L’installation peut se faire via package manager (recommandé) ou installateur.
Option A : Winget (rapide)
# Search
winget search temurin
# Install (example)
winget install EclipseAdoptium.Temurin.21.JDK
# Verify
java -version
javac -versionOption B : Chocolatey (si présent)
choco search temurin
choco install temurin21
java -versionConfigurer JAVA_HOME et PATH (essentiel)
JAVA_HOME doit pointer vers le dossier du JDK (ex: C:\Program Files\Java\...). Le PATH doit inclure %JAVA_HOME%\bin.
| Variable | Valeur | But |
|---|---|---|
| JAVA_HOME | Path du JDK | Référence unique |
| PATH | %JAVA_HOME%\bin | Accès à java/javac |
Check Windows
where java
where javac
java -version
javac -version⚠️ Si where java retourne plusieurs chemins : tu as plusieurs Java installés. Il faut nettoyer PATH ou standardiser.
Checklist de validation (5 minutes)
- java -version affiche la bonne version
- javac -version disponible (sinon tu n’as pas un JDK)
- JAVA_HOME défini (recommandé)
- PATH contient le bon
bin - Build OK avec Maven/Gradle
java -version
javac -version
echo $JAVA_HOME
mvn -v
gradle -vErreurs classiques
| Erreur | Cause probable | Fix |
|---|---|---|
| javac not found | JRE only / PATH incorrect | Install JDK + fix PATH |
| Unsupported class file major version | Runtime too old | Upgrade runtime or target |
| java points to wrong JDK | Multiple installs | Fix PATH / use SDKMAN |
Comprendre “major version” (ultra important)
Chaque version Java produit un bytecode avec un “major version”. Si tu compiles avec un Java récent et exécutes avec un Java plus vieux, tu auras une erreur.
Règle simple : runtime ≥ compile target.
Diagnostiquer un jar
# Inspect jar content
jar tf app.jar | head
# Check class version (example on a class file)
javap -verbose com.example.Main | grep "major"Bon réflexe CI/CD
- Pin le JDK de build (Docker image, CI config)
- Pin le runtime de prod (same distro if possible)
- Expose la version dans les logs au démarrage
Pourquoi gérer plusieurs versions ?
- Projet legacy en 17, nouveau service en 21
- CI sur 21, prod sur 21, mais migration vers 25 en préparation
- SDKs externes qui imposent une version
Maven Toolchains (approche pro)
Objectif : Maven choisit automatiquement le bon JDK pour compiler, indépendamment de ton JAVA_HOME global.
# ~/.m2/toolchains.xml (example)
jdk
21
temurin
/path/to/jdk-21
Gradle Toolchains (approche moderne)
Gradle peut télécharger et utiliser automatiquement la bonne version Java.
// build.gradle (example)
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}Pin du target bytecode (important)
# Maven compiler plugin (concept)
# Set release to ensure consistent bytecode target
mvn -DskipTests package
# In Maven config: 21 ✅ Résultat : build reproductible, migrations contrôlées, moins d’incidents prod.
Pourquoi containers = installation “zéro surprise”
Avec Docker, tu figes :
- Distribution Java
- Version exacte
- OS image + libs
- Reproductibilité CI/prod
Exemple Dockerfile (runtime)
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY app.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]Build + Run (multi-stage) (pro)
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY . /src
RUN ./mvnw -DskipTests package
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /src/target/app.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]Bonnes pratiques containers
- Pin tag (avoid "latest")
- Expose
java -versionat startup logs - Memory flags only when needed (measure first)
- Use JRE image for runtime, JDK for build
Dans un vrai projet Java, on ne compile/exécute pas “à la main”. Un build tool gère : dépendances, compilation, tests, packaging, versioning, qualité, CI/CD et reproductibilité.
Objectif d’un build tool
- Compiler (javac) avec options cohérentes
- Résoudre les dépendances (download + cache)
- Exécuter les tests (unit + integration)
- Packager (jar/war) et publier
- Garantir un build reproductible (versions, toolchains)
Notions clés à connaître
- Repository : Maven Central + repos privés
- Coordinates : groupId:artifactId:version
- Scopes : compile / test / runtime
- Plugins : compiler, surefire, shade, etc.
- Lifecycle : enchaînement de phases standard
Cycle de vie standard (vision)
clean -> supprime target/build
compile -> compile code
test -> unit tests
package -> jar/war
verify -> checks
install -> cache local
deploy -> repo distant (CI)Ce que tu dois viser en entreprise
- Build “one command” (local + CI identique)
- Version Java maîtrisée (toolchain)
- Dépendances pin / contrôlées
- Packaging standard (jar executable)
- Checks qualité (tests, coverage, static analysis)
Maven : le standard historique
Maven impose une structure claire et un cycle de vie strict. Très répandu en entreprise. Configuration via pom.xml (XML).
Commandes Maven essentielles
mvn -v
mvn clean test
mvn clean package
mvn -DskipTests package
mvn dependency:treeStructure projet (convention)
src/main/java
src/main/resources
src/test/java
target/pom.xml minimal (Spring Boot)
4.0.0 com.example demo 1.0.0 jar 21 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test
Quand Maven est un bon choix ?
- Organisation “enterprise” standard
- Projet multi-modules stable
- Équipes habituées XML + conventions
Gradle : moderne, flexible
Gradle est plus scriptable, souvent plus rapide sur gros builds, et permet une configuration avancée. DSL Groovy ou Kotlin.
Commandes Gradle essentielles
gradle -v
gradle clean test
gradle clean build
gradle dependencies
gradle tasksStructure projet
src/main/java
src/main/resources
src/test/java
build/build.gradle minimal (Spring Boot)
plugins {
id "org.springframework.boot" version "3.3.0"
id "io.spring.dependency-management" version "1.1.6"
id "java"
}
group = "com.example"
version = "1.0.0"
java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
testImplementation "org.springframework.boot:spring-boot-starter-test"
}Quand Gradle est un bon choix ?
- Builds complexes / custom
- Gros monorepos / performance build
- Toolchains intégrées (très propre)
Résolution de dépendances : ce qui se passe vraiment
Quand tu ajoutes une dépendance, tu récupères aussi ses transitives. C’est puissant, mais ça peut provoquer :
- Conflits de versions
- Inflation des libs
- Vulnérabilités indirectes
Réflexes pro
- Inspecter l’arbre :
dependency:tree/dependencies - Éviter les versions “au hasard”
- Centraliser via BOM / dependency management
BOM (Bill of Materials) : standard Spring
Une BOM fixe des versions cohérentes pour éviter le “dependency hell”.
org.springframework.boot
spring-boot-dependencies
3.3.0
pom
import
Conflits typiques
- Logging multiple (slf4j/logback/log4j)
- Jackson versions
- Netty versions
JAR vs WAR
- JAR : standard moderne (Spring Boot executable jar)
- WAR : déploiement legacy sur app server (Tomcat external)
Executable JAR (Spring Boot)
Spring Boot peut embarquer le serveur et produire un jar autonome :
java -jar app.jar
Build artifact
- Maven :
target/app.jar - Gradle :
build/libs/app.jar
Profils & environnements
Le build tool ne sert pas qu’à compiler : il structure les environnements.
- Dev vs Prod flags
- Resources packaging
- Build metadata
- Version stamping (git commit)
Bon pattern
# CI build
mvn -DskipTests package
# Prod run
java -jar app.jar✅ Objectif : le jar est identique entre environnements, seuls les paramètres changent.
Reproductibilité : le nerf de la guerre
- Même JDK en local et en CI
- Mêmes versions de dépendances
- Build “clean” régulier
- Cache repo contrôlé
Toolchains : standard pro
Évite les builds “au hasard” en imposant Java 17/21.
- Maven Toolchains
- Gradle Toolchains
Lock files
Gradle supporte la verification/locking plus facilement pour figer un graphe de deps.
Pipeline CI minimal (concept)
Steps:
1) checkout
2) setup JDK (pin exact version)
3) cache dependencies
4) build + tests
5) package artifact
6) publish (repo / docker)Qualité
- Tests unitaires + intégration
- Coverage
- Static analysis
- Vuln scan deps
✅ Un build tool devient le “chef d’orchestre” de ton industrialisation.
Tableau de décision rapide
| Critère | Maven | Gradle |
|---|---|---|
| Standard enterprise | Excellent | Très bon |
| Configuration simple | Stable (XML) | DSL (plus flexible) |
| Builds complexes | Possible | Excellent |
| Performance gros builds | Bonne | Très bonne |
| Toolchains | Oui | Oui (très clean) |
Règle terrain
Si ton écosystème est déjà Maven : reste Maven. Si tu pars sur un build moderne et complexe : Gradle est souvent plus confortable.
Checklist “build pro” (quel que soit l’outil)
- Pin une version Java (LTS)
- Utilise une BOM
- Inspecte
dependency tree - Build en un seul command
- CI = local
- Artifact unique (jar)
- Tests + quality gates
✅ Le vrai sujet n’est pas Maven vs Gradle. Le vrai sujet : reproducible builds + dependency control + CI/CD.
final – Valeurs, Mémoire, Wrappers, PiègesJava distingue deux mondes : primitifs (valeurs “brutes”) et objets. Les primitifs sont utilisés pour la performance, la simplicité et un contrôle explicite sur les tailles. Le mot-clé final renforce la robustesse du code en empêchant la réassignation.
Les 8 types primitifs
Les primitifs sont des valeurs (pas des objets). Ils n’ont pas de méthodes, pas de null, et représentent des tailles fixes.
| Type | Taille | Usage typique |
|---|---|---|
byte | 8 bits | Flux/binaire, buffers |
short | 16 bits | Rare (compat/binaire) |
int | 32 bits | Index, compteurs, ID |
long | 64 bits | Timestamps, big counters |
float | 32 bits | Rare (perf/legacy) |
double | 64 bits | Mesures, calculs |
boolean | JVM-dépendant | Flags logiques |
char | 16 bits | UTF-16 code unit |
⚠️ char n’est pas “un caractère Unicode complet” dans tous les cas : Java stocke des unités UTF-16 (certains symboles utilisent des paires).
Exemples (déclarations + suffixes)
// Integers
byte b = 1;
short s = 10;
int i = 100;
long l = 1000L; // "L" mandatory for long literals
// Floating point
float f = 3.14f; // "f" mandatory for float literals
double d = 3.14159;
// Boolean
boolean ok = true;
// Char (single quotes)
char c = 'A';Valeurs par défaut (champs uniquement)
Les variables locales doivent être initialisées avant usage. Les champs (fields) ont des valeurs par défaut.
| Type | Default (field) |
|---|---|
| Entiers | 0 |
float/double | 0.0 |
boolean | false |
char | '\u0000' |
✅ Bon réflexe : initialise explicitement, même si Java “peut” mettre un default.
Primitifs vs objets : impact mémoire
Un primitif est une valeur directe. Un objet implique une allocation (header + alignement + référence + GC).
- Primitif : rapide, compact, pas de
null - Objet : flexible, peut être
null, a des méthodes
Stack vs Heap (simplifié)
- Variables locales primitives : généralement sur la Stack (frame)
- Objets : sur le Heap, référencés depuis la Stack
int x = 10; // value
Integer y = 10; // object + referenceCas concret : nullability
Un primitif ne peut jamais être null. Un wrapper peut l’être. Cela a un gros impact en ORM (JPA), DTO, parsing, etc.
int a = 0; // always a value
Integer b = null; // possible
// Pitfall:
Integer n = null;
// int z = n; // NullPointerException via unboxingPrimitives in collections
Les collections Java stockent des objets, donc les primitifs sont “boxés”. Sur gros volumes, ça peut exploser la mémoire.
List<Integer>= beaucoup d’objets- Pour performance : structures spécialisées (ou arrays)
Conversions implicites (widening)
Java autorise des conversions “vers plus grand” sans cast explicite.
int i = 42;
long l = i; // OK
double d = l; // OKConversions risquées (narrowing)
Convertir vers plus petit exige un cast et peut tronquer / overflow.
long big = 3_000_000_000L;
int small = (int) big; // overflow -> value corruptedPiège #1 : division entière
int a = 5 / 2; // 2 (not 2.5)
double b = 5 / 2; // 2.0 (still integer division)
double c = 5 / 2.0; // 2.5 (OK)Piège #2 : float/double et précision
Les flottants sont binaires, donc certains décimaux ne sont pas représentables exactement. Pour la monnaie : utilise plutôt BigDecimal.
double x = 0.1 + 0.2; // not exactly 0.3
// For money: BigDecimal is saferPiège #3 : char et Unicode
Certains symboles (emoji, etc.) utilisent deux char (surrogates). Pour manipuler du texte : préfère String + APIs Unicode.
Wrappers (classes) associés
Chaque primitif a une classe wrapper. Utile pour collections, generics, nullability, utilitaires (parse).
| Primitif | Wrapper |
|---|---|
int | Integer |
long | Long |
double | Double |
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
float | Float |
Parsing (très utilisé)
int a = Integer.parseInt("123");
long b = Long.parseLong("999");
boolean ok = Boolean.parseBoolean("true");Autoboxing / Unboxing
Java peut convertir automatiquement entre primitif et wrapper. Pratique, mais peut coûter en performance et créer des bugs.
Integer x = 10; // boxing
int y = x; // unboxing
// Pitfall:
Integer n = null;
// int z = n; // NPE (unboxing)Comparaison : == vs equals
== compare les références (objets), pas la valeur. Sur wrappers : utilise equals ou unboxing contrôlé.
Integer a = 128;
Integer b = 128;
// a == b may be false (different objects)
boolean r1 = (a == b); // risky
boolean r2 = a.equals(b); // correct✅ Règle : primitives -> == OK ; wrappers -> equals.
final : ce que ça garantit vraiment
final signifie : assignation unique. Cela ne veut pas toujours dire “immutable object”.
Sur primitive
final int X = 10;
// X = 20; // compile errorSur référence objet
final StringBuilder sb = new StringBuilder("a");
// sb = new StringBuilder("b"); // forbidden
sb.append("x"); // allowed (object mutates)✅ final bloque la réassignation, pas la mutation interne.
final sur méthodes et classes
final a aussi un rôle d’architecture :
- final class : interdit l’héritage
- final method : interdit l’override
final class TokenService {
// cannot be extended
}
class Base {
final void doWork() { }
// subclasses cannot override doWork()
}Pourquoi c’est utile ?
- Garantir un contrat
- Éviter un héritage dangereux
- Rendre le code plus prédictible
- Faciliter certaines optimisations
Règles simples (backend pro)
- Utilise
intpar défaut,longpour timestamps/counters - Utilise
doublepour mesures,BigDecimalpour argent - Évite
float(sauf cas spécifiques) - Évite
short(rarement utile) - Préfère
booleanexplicite (noms clairs)
ORM / DTO : primitive ou wrapper ?
Si une valeur peut être “absente” : wrapper. Si elle doit toujours exister : primitive.
| Cas | Choix | Raison |
|---|---|---|
| Champ obligatoire | primitive | jamais null |
| Champ optionnel | wrapper | null possible |
Pratiques final
- Marque en
finalce qui ne doit pas changer - Utilise
finalsur les champs injectés (robustesse) - Favorise l’immutabilité (objets immuables quand possible)
- Évite l’état mutable partagé
Mini checklist anti-bugs
- Pas de
==sur wrappers - Attention à
null+ unboxing - Attention à division entière
- Argent = BigDecimal
- IDs / timestamps = long
✅ Cette section “primitives + final” est la base d’un code Java fiable, lisible et performant.
null – Références, Wrappers, String, Optional, NPETout ce qui n’est pas un primitif est un objet. Une variable “objet” ne contient pas l’objet lui-même, mais une référence (un pointeur logique) vers un objet sur le Heap. Le principal risque en Java vient d’une référence qui ne pointe vers rien : null → NullPointerException.
Objet = instance, Référence = variable
En Java, une variable de type objet contient une référence. Plusieurs variables peuvent référencer le même objet.
// Reference variables
String a = "Hello";
String b = a; // b references the same object as a
// Changing 'a' does not mutate the original object (String is immutable)
a = "World"; // a now points to a different objectStack vs Heap (simplifié)
- Références (locales) : Stack (frame du thread)
- Objets : Heap (géré par le GC)
Quand aucune référence ne pointe vers un objet → il devient éligible au GC.
Comparaison d’objets : == vs equals
== compare les références (même objet ou non). equals compare la valeur logique (si implémenté).
String s1 = new String("abc");
String s2 = new String("abc");
boolean r1 = (s1 == s2); // false (different objects)
boolean r2 = s1.equals(s2); // true (same value)✅ Règle : sur objets, utilise equals (ou Objects.equals), pas ==.
String : l’objet le plus important
String est immuable : toute “modification” crée un nouvel objet. Cette immutabilité est une force : thread-safety, cache, sécurité.
String pool (littéraux)
Les littéraux "abc" sont souvent internés dans le pool, ce qui peut influencer ==. Ne t’appuie jamais dessus : utilise toujours equals.
String a = "abc";
String b = "abc";
boolean r = (a == b); // often true due to pool (but don't rely on it)
boolean v = a.equals(b); // always correctConcaténation : piège performance
Dans une boucle, + sur des Strings peut créer énormément d’objets. Utilise StringBuilder.
// Bad in loops
String s = "";
for (int i = 0; i < 10000; i++) {
s = s + i;
}
// Better
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String out = sb.toString();Comparaison safe (null-safe)
String role = null;
// Preferred: constant first to avoid NPE
if ("ADMIN".equals(role)) {
// ...
}Wrappers : primitives “en objet”
Les wrappers existent pour :
- Génériques (Collections :
List<Integer>) - Nullability (
Integerpeut êtrenull) - Utilitaires : parse, compare, constants
- Interop (frameworks, ORM, serializers)
Autoboxing / Unboxing
Integer a = 10; // boxing
int b = a; // unboxingPièges wrappers
1) Unboxing + null = NPE
Integer n = null;
// int x = n; // NPE at runtime2) Comparaison
Integer x = 128;
Integer y = 128;
boolean r1 = (x == y); // risky
boolean r2 = x.equals(y); // correct✅ Sur wrappers : equals (ou compareTo), jamais ==.
Qu’est-ce que null ?
null signifie “aucune référence”. La variable ne pointe vers aucun objet.
NPE : l’erreur la plus connue
Appeler une méthode sur null déclenche NullPointerException.
String name = null;
int len = name.length(); // NPEDans un backend, les sources classiques de null :
- Paramètres HTTP absents
- Valeurs DB optionnelles
- Mapping DTO incomplet
- Retours “pas trouvé” mal gérés
- Unboxing de wrapper null
NPE “masquées” (pièges fréquents)
1) equals dans le mauvais sens
String role = null;
if (role.equals("ADMIN")) { // NPE
}2) chain calls
User u = findUser();
String city = u.getAddress().getCity(); // NPE if any is null3) streams / lambdas
Listusers = null; // users.stream() -> NPE
✅ Règle : “null can appear anywhere” si tu n’imposes pas un contrat.
Stratégies simples et efficaces
1) Defensive check
if (value != null) {
// use value
}2) Constant-first equals
if ("ADMIN".equals(role)) {
// safe
}3) Default values (fallback)
String city = (inputCity != null) ? inputCity : "Unknown";
4) Fail-fast (reject invalid)
Objects.requireNonNull(userId, "userId required");
Objets utilitaires “null-safe”
Objects.equals
boolean ok = Objects.equals(a, b); // safe even if null
String.valueOf
String s = String.valueOf(x); // "null" if x is null
Collections.emptyList
List- items = (raw != null) ? raw : Collections.emptyList();
✅ Le vrai but : supprimer les “null surprises” par contrat et defaults.
Optional : représenter l’absence proprement
Optional est une abstraction “value present / absent”. Elle évite les NPE et force à gérer le cas vide.
Optionalopt = findUserById(id); User u = opt.orElseThrow(); // fail-fast User u2 = opt.orElse(defaultUser); opt.ifPresent(v -> log(v.getId()));
Usage recommandé
- Retour de méthodes de service / repository
- API internes (contrats clairs)
- Éviter de retourner
nullquand possible
Anti-patterns avec Optional
- Ne pas l’utiliser partout (fields/entities = souvent mauvais)
- Ne pas faire
opt.get()sans check - Ne pas l’utiliser comme “boîte magique” qui cache des null
Optionalo = Optional.ofNullable(input); // Bad: calling get without guard // String v = o.get(); String v = o.orElse("default");
✅ Optional est excellent pour les retours, moins pour les champs persistés.
Règles pro “backend Java”
- Évite de retourner
null: préfèreOptionalou exceptions contrôlées - Évite
==sur objets : préfèreequals/Objects.equals - Évite “String concat” en boucle : utilise
StringBuilder - Pour collections : préfère
emptyList()ànull - Documente le contrat (nullable vs non-null)
DTO/API : contrat clair
Un contrat clair réduit les bugs :
- Entrées invalides rejetées tôt
- Valeurs optionnelles explicites
- Defaults définis côté API
Checklist anti-NPE
| Situation | Risque | Fix |
|---|---|---|
role.equals("X") | NPE | "X".equals(role) |
| Wrapper -> primitive | NPE | Default / requireNonNull |
| Collections null | NPE | emptyList/emptyMap |
| Chaining getters | NPE | Validate + Optional/map |
✅ Le but n’est pas “mettre des if partout” : le but est d’imposer un contrat, et de rendre l’absence explicite.
switch, Patterns & PiègesLes structures de contrôle déterminent le flux d’exécution : choisir (if/switch), répéter (for/while/do-while/for-each), et sortir (break/continue/return). En backend, ces choix ont un impact direct sur la lisibilité, la robustesse, et la performance.
if / else : décider clairement
if est la structure la plus flexible : conditions simples, combinées, règles métiers, validations, filtres.
int age = 20;
if (age < 18) {
System.out.println("Minor");
} else if (age >= 65) {
System.out.println("Senior");
} else {
System.out.println("Adult");
}Conditions composées
Utilise && et || (short-circuit). La seconde partie n’est évaluée que si nécessaire.
String role = null;
// Safe: second condition only evaluated if role != null
if (role != null && role.equals("ADMIN")) {
// ...
}
// Safer pattern: constant-first equals
if ("ADMIN".equals(role)) {
// ...
}Ternary operator ?: (simple only)
Très utile pour assigner une valeur rapidement. À éviter si la logique devient complexe (lisibilité).
int score = 75;
String level = (score >= 60) ? "PASS" : "FAIL";Fail-fast (backend pro)
En validation d’entrée, préfère “retourner/lever tôt” plutôt que d’imbriquer.
void process(String userId) {
if (userId == null || userId.isBlank()) {
throw new IllegalArgumentException("userId required");
}
// business logic continues
}switch classique : attention au break
Sans break, Java “tombe” dans le case suivant (fall-through). Cela peut être voulu, mais c’est une source de bugs.
int code = 200;
switch (code) {
case 200:
System.out.println("OK");
break;
case 404:
System.out.println("NOT_FOUND");
break;
default:
System.out.println("OTHER");
}switch sur String
String method = "GET";
switch (method) {
case "GET":
System.out.println("Read");
break;
case "POST":
System.out.println("Create");
break;
default:
System.out.println("Other");
}switch expression (moderne)
Les versions modernes de Java permettent un style expression, très propre pour mapper des valeurs.
int http = 404;
String label = switch (http) {
case 200 -> "OK";
case 404 -> "NOT_FOUND";
case 500 -> "SERVER_ERROR";
default -> "OTHER";
};Quand préférer switch ?
- Mapping valeur → action
- Enums (très propre)
- Code plus lisible que 10 if/else
✅ Pour un backend : switch + enum = code stable et maintenable.
while : répéter tant qu’une condition est vraie
Utile quand tu ne connais pas le nombre d’itérations à l’avance.
int i = 0;
while (i < 5) {
System.out.println(i);
i++;
}do / while : au moins une fois
int tries = 0;
do {
tries++;
} while (tries < 3);for classique : index / bornes
Le meilleur choix quand tu as un index (tableau, bornes, pas fixe).
for (int j = 0; j < 5; j++) {
System.out.println(j);
}Itérer un tableau (index)
int[] arr = {10, 20, 30};
for (int k = 0; k < arr.length; k++) {
System.out.println("Index " + k + " value=" + arr[k]);
}⚠️ Attention à arr.length (pas size()).
for-each : le plus lisible pour les collections
Utilise-le quand tu n’as pas besoin de l’index.
java.util.Listnames = java.util.List.of("Alice", "Bob"); for (String name : names) { System.out.println(name); }
Map iteration
java.util.Mapmap = java.util.Map.of("a", 1, "b", 2); for (java.util.Map.Entry e : map.entrySet()) { System.out.println(e.getKey() + "=" + e.getValue()); }
Piège : modifier une collection pendant l’itération
Modifier une collection pendant un for-each peut déclencher une exception. Approche correcte : iterator, ou collect des éléments à supprimer.
java.util.Listlist = new java.util.ArrayList<>(); list.add("a"); list.add("b"); // Use iterator for removal java.util.Iterator it = list.iterator(); while (it.hasNext()) { String v = it.next(); if ("a".equals(v)) { it.remove(); } }
✅ Règle : “iterate” et “mutate” doivent être contrôlés.
break : sortir d’une boucle ou d’un switch
for (int i = 0; i < 10; i++) {
if (i == 3) {
break;
}
}continue : sauter une itération
for (int i = 0; i < 5; i++) {
if (i == 2) {
continue;
}
System.out.println(i);
}return : sortir d’une méthode (fail-fast)
Très utilisé en backend pour arrêter tôt en cas d’entrée invalide.
String normalize(String s) {
if (s == null || s.isBlank()) {
return "";
}
return s.trim();
}Nested loops : labels (rare, mais existe)
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break outer;
}
}
}Pièges fréquents
| Erreur | Impact | Fix |
|---|---|---|
Oublier break (switch classique) | Comportement imprévu | Ajouter break ou utiliser switch expression |
| while sans mise à jour | Boucle infinie | Incrément / condition fiable |
| Modifier une liste en for-each | Exception | Iterator / copy / filter |
| if/else imbriqués | Lisibilité faible | Fail-fast + early return |
Style backend recommandé
- Validation en début de méthode (fail-fast)
- Switch pour mapping simple
- For-each pour lisibilité sur collections
- For classique si index nécessaire
Patterns “lisibles” (mini exemples)
1) Guard clauses
void handle(String token) {
if (token == null || token.isBlank()) {
throw new IllegalArgumentException("token required");
}
// continue safely
}2) Mapping via switch expression
String toLabel(int code) {
return switch (code) {
case 200 -> "OK";
case 404 -> "NOT_FOUND";
default -> "OTHER";
};
}3) Loop with clear boundaries
for (int i = 0; i < items.size(); i++) {
// explicit index use
}✅ Le vrai objectif : un flux d’exécution clair, sans surprises, et facile à maintenir.
main – static vs instance, signatures, overloadingUne méthode est une fonction attachée à une classe. Java distingue : méthodes d’instance (il faut un objet) et méthodes static (liées à la classe). Le point d’entrée d’un programme Java est la méthode main.
Le point d’entrée : public static void main(String[] args)
Toute application Java “classique” démarre par main. La JVM cherche cette signature exacte pour lancer le programme.
public class App {
public static void main(String[] args) {
System.out.println("Starting...");
}
}Rôle de args
args contient les arguments passés en ligne de commande.
public static void main(String[] args) {
System.out.println("Args count=" + args.length);
if (args.length > 0) {
System.out.println("First=" + args[0]);
}
}Compilation / exécution (rappel)
javac App.java
java App one two threeCas Spring Boot (important)
En Spring Boot, main existe aussi, mais sert à démarrer le container Spring.
// Conceptual example
public class MySpringApp {
public static void main(String[] args) {
// SpringApplication.run(MySpringApp.class, args);
}
}✅ Même en framework, main reste la porte d’entrée.
Méthode d’instance : nécessite new
Une méthode d’instance s’applique à un objet concret. Elle peut accéder à l’état via this.
public class Calculator {
private int factor = 2;
public int multiply(int x) {
return x * this.factor;
}
}
Calculator c = new Calculator();
int r = c.multiply(10);✅ Instance = logique métier + état (souvent services, entities, components).
Méthode static : liée à la classe
Une méthode static n’a pas besoin d’objet : appelable via ClassName.method(). Elle ne peut pas accéder directement à this (pas de contexte d’instance).
public class MathUtil {
public static int add(int a, int b) {
return a + b;
}
}
int s = MathUtil.add(10, 5);Quand utiliser static ?
- Fonctions utilitaires pures (stateless)
- Factories (parfois)
- Constants (fields
static final)
⚠️ En backend moderne, trop de static peut compliquer le test et l’injection.
Signature d’une méthode
Une signature inclut : nom + types des paramètres. Le type de retour ne fait pas partie de la signature (important pour overloading).
public int sum(int a, int b) {
return a + b;
}Paramètres : pass-by-value (toujours)
Java est toujours “pass-by-value”. Pour un objet, la valeur copiée est la référence (pas l’objet).
static void changeInt(int x) {
x = 99;
}
static void changeObj(StringBuilder sb) {
sb.append("X"); // mutates the object
}
int a = 1;
changeInt(a); // a still 1
StringBuilder s = new StringBuilder("A");
changeObj(s); // s is now "AX"Varargs (nombre variable d’arguments)
Varargs = syntaxe pratique quand on ne connaît pas le nombre de paramètres.
static int sumAll(int... values) {
int s = 0;
for (int v : values) {
s += v;
}
return s;
}
int r = sumAll(1, 2, 3, 4);Visibility : public/private
public: accessible partoutprivate: uniquement dans la classeprotected: package + héritage- (default) : package only
✅ En backend : expose peu (public), encapsule beaucoup (private).
Type de retour
void = pas de retour. Sinon, return est obligatoire sur tous les chemins.
static void log(String msg) {
System.out.println(msg);
}
static int max(int a, int b) {
return (a > b) ? a : b;
}Early return (lisibilité)
static String normalize(String s) {
if (s == null || s.isBlank()) {
return "";
}
return s.trim();
}Exceptions : signaler l’erreur
Une méthode peut échouer : Java utilise des exceptions pour remonter l’erreur. En backend : fail-fast + messages clairs = debugging plus rapide.
static int parsePort(String s) {
if (s == null || s.isBlank()) {
throw new IllegalArgumentException("port required");
}
return Integer.parseInt(s);
}Checked vs unchecked (concept)
- Unchecked : RuntimeException (souvent en backend)
- Checked : doit être gérée (try/catch ou throws)
✅ Bon pattern : exceptions métier explicites + mapping HTTP propre (ControllerAdvice).
Overloading : même nom, paramètres différents
Overloading = plusieurs méthodes même nom mais signatures différentes. Très utilisé dans les APIs (constructeurs, utilitaires).
static int add(int a, int b) {
return a + b;
}
static double add(double a, double b) {
return a + b;
}
static int add(int a, int b, int c) {
return a + b + c;
}Pièges : ambiguïtés & conversions
Avec autoboxing / widening, certaines combinaisons peuvent devenir ambiguës. Garde les signatures simples et intentionnelles.
static void f(long x) { }
static void f(Integer x) { }
// f(1) will choose f(long) (widening int -> long)
// but mixing many overloads can be confusing✅ Overloading est utile, mais abusé il rend le code illisible.
Règles “backend propre”
- Nommer clairement : verbe + intention (ex:
computePrice) - Limiter la taille (méthodes courtes, lisibles)
- Limiter le nombre de paramètres (idéal ≤ 3)
- Éviter l’état global (
staticmutable) - Valider tôt (fail-fast)
Refactor : trop de paramètres ?
Si tu as 6-8 paramètres : crée un objet (DTO/command) ou utilise builder.
static class CreateUserCommand {
final String email;
final String role;
final int age;
CreateUserCommand(String email, String role, int age) {
this.email = email;
this.role = role;
this.age = age;
}
}Testabilité : instance > static
Les méthodes d’instance s’intègrent mieux aux frameworks (Spring) : injection, mocks, configuration, aspects.
- Services : instance methods
- Utilities pures : static OK
- Évite “static everywhere”
Mini checklist
| Question | Oui | Non |
|---|---|---|
Accède à l’état (this) ? | instance | static |
| Fonction pure (stateless) ? | static OK | instance |
| Doit être mockée/testée facilement ? | instance | static (à éviter) |
| Utilitaire simple (parse/format) ? | static OK | instance |
✅ Objectif : méthodes petites, claires, testables, et un flux d’exécution prévisible.
En Java, l’OOP repose sur une idée simple : une Classe décrit un modèle (structure + comportement), et un Objet est une instance concrète créée en mémoire via new. L’objectif : rendre le code maintenable, testable et robuste sur la durée.
Classe : le “plan”
Une classe définit :
- Champs (état) : variables internes
- Méthodes (comportement) : actions
- Constructeurs : initialisation à la création
- Visibilités : public/private/protected
Une classe est un type : on peut déclarer des variables de ce type.
Objet : l’instance en mémoire
Un objet est une instance concrète sur le Heap. Une variable de type objet contient une référence.
User u = new User(...);
// 'u' is a reference to an object on the heapPlusieurs références peuvent pointer vers le même objet.
User a = new User("a@x.com", 30);
User b = a; // b references the same object
boolean same = (a == b); // true (same reference)Champs (state) : private par défaut
En OOP, l’état doit être protégé. Le pattern standard : champs privés + méthodes publiques pour contrôler l’accès.
public class User {
private String email;
private int age;
public String getEmail() { return email; }
public int getAge() { return age; }
public void setEmail(String email) { this.email = email; }
public void setAge(int age) { this.age = age; }
}✅ “Encapsulation” = éviter que le code externe modifie l’objet n’importe comment.
Encapsulation = règles métier
Plutôt que de laisser un setter libre, on impose des invariants via des méthodes.
public class User {
private int age;
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("age must be >= 0");
}
this.age = age;
}
}Conception “backend”
- État cohérent (invariants)
- Validation “fail-fast”
- Moins de bugs au runtime
Constructeur : initialiser l’objet
Un constructeur est une “méthode spéciale” appelée par new. Il sert à construire un objet valide dès le départ.
public class User {
private final String email;
private final int age;
public User(String email, int age) {
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("email required");
}
if (age < 0) {
throw new IllegalArgumentException("age must be >= 0");
}
this.email = email;
this.age = age;
}
}✅ Ici : objet immuable (fields final) et validation “fail-fast”.
this : l’objet courant
this pointe sur l’instance courante. Très utilisé pour :
- Distinguer champ vs paramètre
- Appeler un autre constructeur (
this(...)) - Fluent APIs (retourner
this)
public class User {
private String email;
private int age;
public User(String email) {
this(email, 0); // calls the other constructor
}
public User(String email, int age) {
this.email = email;
this.age = age;
}
}new : ce qui se passe (simplifié)
- Allocation mémoire sur le Heap
- Initialisation des champs (defaults)
- Appel du constructeur
- Retour d’une référence vers l’objet
User u = new User("alice@mail.com", 30);La référence (u) vit sur la Stack (frame), l’objet sur le Heap.
Alias & effets de bord
Si deux variables pointent vers le même objet, une mutation via l’une impacte l’autre.
public class Box {
public int value;
}
Box a = new Box();
a.value = 10;
Box b = a; // alias
b.value = 99;
System.out.println(a.value); // 99✅ Pour éviter : immutabilité, copies défensives, design “no shared mutable state”.
Pourquoi l’immutabilité est puissante ?
- Thread-safe naturellement
- Moins de bugs (pas d’état modifié “en douce”)
- Meilleur raisonnement / debugging
- Objets réutilisables, caches plus simples
En backend (microservices), l’immutabilité réduit les incidents.
Exemple : classe immuable
public final class User {
private final String email;
private final int age;
public User(String email, int age) {
this.email = email;
this.age = age;
}
public String getEmail() { return email; }
public int getAge() { return age; }
}✅ Champs final + pas de setters = état stable.
Identité vs égalité
Deux objets peuvent être “différents” en mémoire mais égaux en valeur.
==: même référence ?equals: même valeur logique ?hashCode: nécessaire pour HashMap/HashSet
User a = new User("a@x.com", 30);
User b = new User("a@x.com", 30);
boolean r1 = (a == b); // false
// boolean r2 = a.equals(b); // depends on equals implementationPourquoi c’est critique ? (Collections)
Si tu mets des objets dans HashSet / HashMap, equals et hashCode doivent être cohérents.
java.util.Sets = new java.util.HashSet<>(); s.add("a"); s.add("a"); // duplicate removed because equals/hashCode are consistent
✅ Sur tes entités/DTOs : définir une stratégie d’égalité claire (souvent basée sur un id).
Règles simples (code pro)
- Champs
private, accès contrôlé - Validation dans les constructeurs (objets toujours valides)
- Préférer l’immutabilité quand possible
- Éviter setters “free-for-all”
- Nommer les méthodes avec intention métier
DTO vs Domain Object
- DTO : transport (API), souvent plus “plat”
- Domain : règles métier, invariants, comportements
Checklist anti-bugs
| Sujet | Risque | Fix |
|---|---|---|
| Champs publics | État incohérent | Encapsulation |
| Setters sans validation | Données invalides | Invariants |
| Objets mutables partagés | Effets de bord | Immutabilité |
| Equals/hashCode non gérés | Collections bug | Stratégie d’égalité |
✅ Une bonne OOP Java = objets cohérents + contrats clairs + état maîtrisé.
extends, super, override, castingL’héritage permet de réutiliser et spécialiser une classe existante (Child extends Parent). Le polymorphisme permet de manipuler un objet enfant via une référence parent, tout en exécutant le bon comportement au runtime (dynamic dispatch). En pratique : puissant, mais à utiliser avec discipline (composition souvent préférable).
Définition : “is-a”
Admin extends User signifie : Admin est un User (relation “is-a”). L’enfant hérite des champs/méthodes accessibles du parent et peut ajouter/adapter.
- L’enfant récupère les méthodes
public/protecteddu parent - Les champs
privatedu parent restent privés (access via méthodes) - Java ne supporte pas l’héritage multiple de classes (mais interfaces oui)
Exemple simple : spécialisation
public class User {
private final String email;
private final int age;
public User(String email, int age) {
this.email = email;
this.age = age;
}
public String getEmail() { return email; }
public void present() {
System.out.println("I am " + email);
}
}
public class Admin extends User {
private final String role;
public Admin(String email, int age, String role) {
super(email, age);
this.role = role;
}
public String getRole() { return role; }
}Accès & visibilité (important)
Ce qu’un enfant peut “voir” dépend des modificateurs :
| Modif | Accessible depuis enfant ? | Note |
|---|---|---|
private | Non | Accessible via getters/methods |
protected | Oui | Accès dans package + subclasses |
| (default) | Oui (package) | Pas hors package |
public | Oui | Accessible partout |
✅ Règle pratique : garde les champs private, expose via méthodes. L’héritage ne doit pas casser l’encapsulation.
super(...) : initialiser le parent
Lorsqu’un parent n’a pas de constructeur sans argument, l’enfant doit appeler explicitement super(...) pour initialiser la partie “User”. Cet appel doit être la première instruction du constructeur enfant.
public class Admin extends User {
private final String role;
public Admin(String email, int age, String role) {
super(email, age); // must be first
this.role = role;
}
}super.method() : réutiliser le comportement parent
@Override
public void present() {
super.present();
System.out.println("Role=" + role);
}Ordre d’initialisation (simplifié)
- Construction du parent (via
super) - Initialisation champs enfant
- Exécution du corps du constructeur enfant
Piège : appeler une méthode override depuis un constructeur
Si le parent appelle une méthode (override) dans son constructeur, l’enfant peut être exécuté alors que ses champs ne sont pas encore initialisés (comportements surprenants). Bonne règle : évite l’appel à des méthodes overridable depuis les constructeurs.
class Base {
Base() { init(); } // risky
void init() { }
}
class Child extends Base {
private String x;
Child() {
x = "ready";
}
@Override
void init() {
// x might be null here
System.out.println(x);
}
}Override : redéfinir une méthode
L’enfant peut redéfinir une méthode du parent pour spécialiser le comportement. Utilise @Override : ça protège contre les erreurs de signature.
public class User {
public void present() {
System.out.println("I am a user");
}
}
public class Admin extends User {
@Override
public void present() {
System.out.println("I am an admin");
}
}Polymorphisme : référence parent, objet enfant
Le type de la référence peut être le parent, mais l’objet réel est l’enfant. Au runtime, Java appelle la méthode de l’objet réel (dynamic dispatch).
Exemple concret
User u1 = new User();
User u2 = new Admin();
u1.present(); // "I am a user"
u2.present(); // "I am an admin" (dynamic dispatch)Listes : intérêt majeur du polymorphisme
java.util.Listlist = new java.util.ArrayList<>(); list.add(new User()); list.add(new Admin()); for (User u : list) { u.present(); // correct behavior for each concrete type }
✅ C’est le cœur des architectures extensibles : on manipule l’abstraction (parent), et on injecte des implémentations (enfants).
Upcast (safe)
Transformer une référence enfant en référence parent est sûr et implicite.
Admin a = new Admin("a@x.com", 30, "root");
User u = a; // upcast implicitDowncast (risky)
Transformer une référence parent vers un enfant nécessite un cast et peut échouer si l’objet n’est pas réellement de ce type (ClassCastException).
User u = new User();
// Admin a = (Admin) u; // ClassCastException at runtimeinstanceof : vérifier avant cast
User u = new Admin("a@x.com", 30, "root");
if (u instanceof Admin) {
Admin a = (Admin) u;
System.out.println(a.getRole());
}Pattern matching (moderne)
if (u instanceof Admin a) {
System.out.println(a.getRole());
}✅ Règle backend : si tu fais beaucoup de instanceof, ton design manque d’abstraction (souvent mieux avec interfaces / polymorphisme réel).
Classes abstraites : base commune
Une classe abstraite ne peut pas être instanciée. Elle sert de socle avec logique partagée. Elle peut imposer des méthodes à implémenter.
public abstract class BaseUser {
protected final String email;
protected BaseUser(String email) {
this.email = email;
}
public abstract void present();
}✅ Utile quand tu as une logique commune forte + un contrat à imposer.
final : bloquer l’héritage ou l’override
final class empêche l’héritage (sécurité, design). final method empêche la redéfinition.
public final class Token {
private final String value;
public Token(String value) { this.value = value; }
}
// Cannot extend Token
class Service {
public final void audit() {
// cannot be overridden
}
}✅ Beaucoup de libs utilisent final pour protéger les invariants.
Pièges fréquents
| Piège | Impact | Réflexe |
|---|---|---|
| Abus d’héritage (“deep hierarchy”) | Complexité, couplage | Préférer composition |
Override sans @Override | Bug silencieux | Toujours annoter |
| Downcast sans check | Crash runtime | instanceof ou refactor |
| Appel override en constructeur | Champs non init | Éviter |
| Confusion overloading/override | Comportement inattendu | Signatures claires |
Overloading vs Override (à ne pas confondre)
Overloading = même nom, paramètres différents (compile-time). Override = même signature (runtime polymorphism).
class A {
void f(int x) { }
void f(String s) { } // overloading
}
class B extends A {
@Override
void f(int x) { } // override
}✅ Si tu changes les paramètres, tu n’overrides pas : tu overloads.
Règles “pro” (backend)
- Utilise l’héritage quand la relation “is-a” est évidente
- Hiérarchies courtes (1–2 niveaux) : lisible et stable
- Expose des abstractions (interfaces/parents), implémente via classes concrètes
- Évite les casts : préfère le polymorphisme
- Validation/invariants protégés (final/constructors)
Composition vs héritage
Souvent, on préfère “has-a” (composition) : un service utilise un composant plutôt que de l’étendre. Cela réduit le couplage et augmente la flexibilité.
Checklist rapide
| Question | Oui | Non |
|---|---|---|
| Relation “is-a” évidente ? | Héritage possible | Composition |
Beaucoup de instanceof ? | Refactor design | OK |
| Besoin d’un socle commun | Abstract class | Interface + composition |
| Invariants critiques | final / encapsulation | Risque bug |
✅ Objectif : un polymorphisme “utile” (extensibilité), sans hiérarchies fragiles.
L’encapsulation consiste à cacher l’état interne (fields) et à n’exposer qu’une API contrôlée (méthodes) pour lire/modifier cet état. Objectifs : empêcher les états incohérents, protéger les invariants, réduire le couplage, et rendre le code plus maintenable/testable.
Modificateurs d’accès : qui peut voir quoi ?
Java contrôle l’accès via 4 niveaux. Le bon choix réduit les bugs et protège le domaine métier.
| Modif | Visibilité | Usage typique |
|---|---|---|
public | Partout | API exposée (contrat stable) |
protected | Package + subclasses | Extension contrôlée (héritage) |
| (default) | Package uniquement | Modules internes (même package) |
private | Classe uniquement | État interne + helpers |
✅ Règle de base : fields = private, expose via méthodes publiques intentionnelles.
Pourquoi “fields publics” = anti-pattern
Un field public :
- Expose l’état interne (couplage fort)
- Permet des valeurs invalides (invariants cassés)
- Rend le refactor risqué (API “figée”)
- Rend les logs/contrôles/side-effects impossibles à centraliser
// Bad: internal state exposed
public class User {
public int age; // anyone can set age to -999
}// Better: state protected + API controlled
public class User {
private int age;
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}Getters/Setters : accès contrôlé
Le but n’est pas de générer des getters/setters “pour tout”. Le but est d’exposer une API propre : lecture/écriture uniquement quand c’est nécessaire.
public class Person {
private String email;
private int age;
public String getEmail() { return email; }
public int getAge() { return age; }
public void setEmail(String email) { this.email = email; }
public void setAge(int age) { this.age = age; }
}Setters : oui, mais avec intention
- Setter “brut” = risque d’état incohérent
- Préférer méthodes métier :
activate(),changeEmail(), etc.
API métier plutôt que setters
Au lieu de setStatus() ou setRole() arbitraires, on expose une intention claire.
public class Account {
private boolean active;
public boolean isActive() { return active; }
public void activate() { this.active = true; }
public void deactivate() { this.active = false; }
}✅ Avantage : tu maîtrises les transitions d’état et tu peux ajouter audit/log/validation facilement.
Invariants : l’objet doit rester valide
Un invariant est une règle qui doit toujours être vraie (ex: âge positif, email non vide, solde cohérent). L’encapsulation sert à forcer ces règles.
Validation dans un setter
public class Person {
private int age;
public int getAge() { return age; }
public void setAge(int age) {
if (age < 0 || age > 130) {
throw new IllegalArgumentException("invalid age");
}
this.age = age;
}
}✅ “Fail-fast” : mieux vaut rejeter tôt que laisser un objet invalide circuler.
Validation dans le constructeur (encore mieux)
Pour les objets “domain”, on préfère souvent créer un objet valide dès le départ, puis limiter les mutations.
public class User {
private final String email;
public User(String email) {
if (email == null || email.isBlank()) {
throw new IllegalArgumentException("email required");
}
this.email = email;
}
public String getEmail() { return email; }
}Encapsulation = point central de la logique
- Validation
- Normalisation (trim/lowercase)
- Audit / logs
- Contrôle d’accès (permissions)
Immutabilité : l’encapsulation maximale
Un objet immuable ne change jamais après construction : champs final, pas de setters, API stable.
- Thread-safe par défaut
- Moins de bugs d’état
- Meilleure testabilité
public final class Money {
private final java.math.BigDecimal amount;
private final String currency;
public Money(java.math.BigDecimal amount, String currency) {
if (amount == null || currency == null || currency.isBlank()) {
throw new IllegalArgumentException("invalid money");
}
this.amount = amount;
this.currency = currency;
}
public java.math.BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}“Immuable” ne veut pas dire “pas d’évolution”
Un objet immuable peut exposer des méthodes qui retournent une nouvelle instance.
public final class Counter {
private final int value;
public Counter(int value) { this.value = value; }
public int getValue() { return value; }
public Counter increment() {
return new Counter(this.value + 1);
}
}✅ Pattern fréquent en systèmes distribués : pas d’état mutable partagé.
Encapsulation des collections : gros piège
Si tu exposes une liste interne, tu exposes ton état interne. Le code externe peut modifier la liste et casser tes invariants.
public class Team {
private final java.util.List members = new java.util.ArrayList<>();
// Bad: exposes internal list
public java.util.List getMembers() {
return members;
}
} ✅ Problème : quelqu’un peut faire team.getMembers().clear().
Solution : copie défensive / vue non modifiable
public class Team {
private final java.util.List members = new java.util.ArrayList<>();
public java.util.List getMembers() {
return java.util.Collections.unmodifiableList(members);
}
public void addMember(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name required");
}
members.add(name);
}
} ✅ Encapsulation = on expose des opérations métier (addMember) plutôt que l’état brut.
Encapsulation dans un backend (domain)
En backend, l’encapsulation sert à faire respecter la cohérence métier. Exemples :
- Ne pas autoriser un statut illégal
- Ne pas autoriser des montants négatifs
- Centraliser la normalisation (email lower-case)
- Limiter les transitions d’état
public class Order {
private String status;
public String getStatus() { return status; }
public void markPaid() {
if ("PAID".equals(status)) return;
if (!"CREATED".equals(status)) {
throw new IllegalStateException("invalid transition");
}
status = "PAID";
}
}Encapsulation + tests
Une API métier claire rend le test naturel : tu testes des comportements, pas des assignations de champs.
- Moins de tests fragiles
- Plus de tests “business”
- Refactor plus simple
Encapsulation + sécurité
Un objet qui n’accepte pas des valeurs invalides réduit les risques : injection logique, corruption de données, état incohérent.
Pièges fréquents
| Piège | Impact | Fix |
|---|---|---|
| Fields publics | État incohérent | private + API |
| Setters “libres” partout | Transitions illégales | Méthodes métier |
| Pas de validation | Données invalides | Fail-fast |
| Exposer une collection interne | Corruption d’état | unmodifiable / copy |
| API trop large | Couplage fort | Expose le minimum |
Checklist “Encapsulation propre”
- Fields
privatepar défaut - Expose seulement ce qui est nécessaire
- Validation centralisée (constructeur ou méthodes)
- Transitions d’état contrôlées
- Collections protégées (unmodifiable/copy)
- Immutabilité quand possible
- API orientée métier (pas “setEverything”)
✅ Encapsulation = tu protèges ton domaine, pas juste “mettre private”.
abstract class vs interface (extends/implements)L’abstraction sert à programmer contre un contrat plutôt que contre une implémentation. On définit ce qui doit exister (méthodes / comportements), et on laisse les classes concrètes fournir le code. En backend, c’est le socle de la testabilité (mocks), de l’extensibilité, et des architectures propres (services, adapters).
Pourquoi abstraire ?
Sans abstraction, ton code dépend directement des classes concrètes → couplage fort. Avec abstraction, ton code dépend d’un contrat → couplage faible.
- Extensibilité : ajouter une implémentation sans casser le reste
- Testabilité : mock/fake d’une interface
- Lisibilité : intention claire (PaymentGateway, UserRepository…)
- Architecture : séparation domain / infra / adapters
Exemple : programmer contre une abstraction
public interface EmailSender {
void send(String to, String subject, String body);
}
public class NotificationService {
private final EmailSender emailSender;
public NotificationService(EmailSender emailSender) {
this.emailSender = emailSender;
}
public void notifyUser(String email) {
emailSender.send(email, "Welcome", "Hello");
}
}Résultat
Tu peux brancher n’importe quelle implémentation :
- SMTP réel
- API externe
- Fake en tests (capture des emails)
public class FakeEmailSender implements EmailSender {
public final java.util.List sent = new java.util.ArrayList<>();
@Override
public void send(String to, String subject, String body) {
sent.add(to + "|" + subject);
}
} ✅ Abstraction = tu changes “comment c’est fait” sans changer “ce qui est attendu”.
abstract class : base partiellement implémentée
Une classe abstraite :
- Ne peut pas être instanciée (
newinterdit) - Peut contenir état (fields) et code partagé
- Peut imposer des méthodes abstraites (
abstract) - Utilise
extends(héritage de classe : 1 seule)
public abstract class Animal {
protected final String name;
protected Animal(String name) {
this.name = name;
}
public void breathe() {
System.out.println(name + " breathes...");
}
public abstract void makeNoise();
}
public class Dog extends Animal {
public Dog(String name) { super(name); }
@Override
public void makeNoise() {
System.out.println("Woof!");
}
}Quand utiliser une classe abstraite ?
- Tu as une logique commune forte à partager
- Tu as un état commun (fields) à factoriser
- Tu veux imposer un “template method” (squelette d’algorithme)
Pattern : Template Method (squelette)
public abstract class FileImporter {
public final void importFile(String path) {
validate(path);
String raw = read(path);
parse(raw);
persist();
}
protected void validate(String path) { }
protected abstract String read(String path);
protected abstract void parse(String raw);
protected abstract void persist();
}✅ Une classe abstraite est idéale pour “forcer le workflow” + partager du code.
interface : contrat (multi-implémentation)
Une interface définit un contrat. Une classe peut en implémenter plusieurs : implements A, B.
- Pas d’héritage de state (pas de champs d’instance)
- Excellent pour la composition et le couplage faible
- Parfait pour les APIs, services, ports/adapters
public interface Clickable {
void onClick();
}
public interface Draggable {
void onDrag();
}
public class Button implements Clickable, Draggable {
@Override public void onClick() { System.out.println("click"); }
@Override public void onDrag() { System.out.println("drag"); }
}Default methods (utile, mais à utiliser avec discipline)
Une interface peut fournir une implémentation via default. Pratique pour faire évoluer un contrat sans casser toutes les implémentations.
public interface LoggerAware {
default void logInfo(String msg) {
System.out.println("[INFO] " + msg);
}
}
public class Job implements LoggerAware {
public void run() {
logInfo("job started");
}
}Interfaces = base de l’injection (Spring)
- Interface = contrat du service
- Implémentation = composant concret
- En test : fake/mocks simples
Tableau de décision (simple et fiable)
| Besoin | Choix | Pourquoi |
|---|---|---|
| Partager du code + état commun | abstract class | Fields + impl partagée |
| Contrat + multi-implémentations | interface | Couplage faible |
| Besoin d’implémenter plusieurs “capacités” | interface | Multiple interfaces |
| Imposer un workflow / squelette | abstract class | Template method |
| Architecture ports/adapters | interface | Testabilité / flexibilité |
Règle terrain (backend)
Par défaut, en backend moderne : interface pour les contrats, composition pour assembler, et abstract class uniquement quand tu as un vrai besoin de code partagé/état commun.
Mnemonic
- Interface = “what” (contrat)
- Abstract class = “what + some how” (contrat + base)
Pattern : Repository (contrat)
public interface UserRepository {
java.util.Optional findById(long id);
void save(User user);
} Implémentations possibles :
- JPA/Hibernate
- In-memory (tests)
- API distante
Pattern : Strategy (multi-comportements)
public interface PricingStrategy {
java.math.BigDecimal compute(java.math.BigDecimal base);
}Pattern : Adapter
Tu encapsules une librairie externe derrière un contrat interne.
public interface PaymentGateway {
void charge(String customerId, int cents);
}
public class StripeGateway implements PaymentGateway {
@Override
public void charge(String customerId, int cents) {
// call stripe SDK
}
}Tests : Fake implementation
public class FakeGateway implements PaymentGateway {
public int calls = 0;
@Override
public void charge(String customerId, int cents) {
calls++;
}
}Pièges fréquents
| Piège | Impact | Fix |
|---|---|---|
| Utiliser héritage partout | Couplage, hiérarchies fragiles | Préférer interface + composition |
| Interface “fourre-tout” | Implémentations difficiles | Découper (ISP) |
Trop de default methods | Contrat flou / comportement implicite | Utiliser avec parcimonie |
| Abstract class utilisée comme “util” | Design artificiel | Static utils ou composition |
ISP (Interface Segregation Principle)
Une interface doit rester petite et cohérente. Si une classe implémente des méthodes qu’elle n’utilise pas, c’est un signal.
// Better: two small interfaces
public interface Readable {
String read();
}
public interface Writable {
void write(String data);
}✅ Découper = implémentations plus simples + code plus flexible.
Best practices (backend Java)
- Contrats =
interface(services, gateways, repositories) - Implémentations = classes concrètes, injectées (DI)
- Abstract class seulement si : état/code commun réel
- Interfaces petites (ISP), orientées intention
- Nomme les interfaces clairement (ex:
PaymentGateway)
Architecture : ports/adapters
- Port = interface (contrat du domaine)
- Adapter = impl (infra : DB, HTTP, MQ)
- Domain dépend du port, jamais de l’adapter
Checklist rapide
| Question | Réponse |
|---|---|
| Ai-je besoin de partager de l’état + du code ? | Abstract class possible |
| Ai-je besoin de plusieurs implémentations ? | Interface |
| Est-ce une capacité réutilisable ? | Interface (petite) |
| Ai-je beaucoup de “casts/instanceof” ? | Contrat à revoir |
✅ Objectif : abstractions simples, contracts clairs, implémentations interchangeables.
List, Set, Map, Complexité, Pièges & PatternsLe Collections Framework (java.util) fournit des structures standard pour stocker/manipuler des objets. Règle pro : typer tes variables par l’interface (List, Map, Set) et choisir l’implémentation (ArrayList, HashMap, HashSet, etc.) selon les besoins : ordre, doublons, recherche, performance, concurrence.
Les 3 familles
List<E>: ordonnée, indexée, doublons autorisésSet<E>: pas de doublons (unicité), ordre selon implémentationMap<K,V>: dictionnaire clé/valeur, clés uniques
Règle d’or : interface en type, classe en instanciation
java.util.Lista = new java.util.ArrayList<>(); java.util.Set b = new java.util.HashSet<>(); java.util.Map c = new java.util.HashMap<>();
✅ Avantage : tu peux changer l’implémentation sans casser l’API (couplage faible).
Choisir rapidement (règles terrain)
| Besoin | Structure | Implémentation typique |
|---|---|---|
| Ordre + accès par index | List | ArrayList |
| Unicité (pas de doublons) | Set | HashSet |
| Lookup clé → valeur | Map | HashMap |
| Ordre d’insertion à préserver | Set/Map | LinkedHashSet/LinkedHashMap |
| Tri naturel / comparator | Set/Map | TreeSet/TreeMap |
⚠️ Les collections stockent des objets : primitives → wrappers (boxing).
List<E> : ordonnée, doublons OK
Le bon choix quand tu veux préserver l’ordre et accéder par index (get(i)). En backend : résultats de requêtes, DTO lists, pagination, batch processing.
java.util.Listnames = new java.util.ArrayList<>(); names.add("Alice"); names.add("Bob"); names.add("Alice"); // duplicates allowed String first = names.get(0); int size = names.size();
ArrayList vs LinkedList (règle simple)
ArrayList: meilleur par défaut (cache-friendly)LinkedList: rarement nécessaire (souvent moins performant)
Opérations courantes
java.util.Listlist = new java.util.ArrayList<>(); list.add("a"); list.add("b"); boolean hasA = list.contains("a"); // linear scan list.remove("b"); // remove first occurrence list.set(0, "x"); // replace by index
Création rapide (immuable)
java.util.Listfixed = java.util.List.of("a", "b"); // fixed.add("c"); // UnsupportedOperationException
✅ List.of est pratique pour des constantes / configs.
Set<E> : unicité (pas de doublons)
Un Set empêche les doublons selon equals/hashCode. En backend : déduplication, tags, permissions, IDs uniques, “seen set”.
java.util.Settags = new java.util.HashSet<>(); tags.add("Java"); tags.add("Rust"); tags.add("Java"); // ignored boolean hasJava = tags.contains("Java"); int size = tags.size();
Ordre
HashSet: pas d’ordre garantiLinkedHashSet: garde l’ordre d’insertionTreeSet: trié (comparateur)
Critique : equals / hashCode
Pour les objets custom, Set dépend de equals/hashCode. Si c’est mal défini → doublons incohérents ou lookup cassé.
static class UserId {
final long id;
UserId(long id) { this.id = id; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserId)) return false;
UserId other = (UserId) o;
return this.id == other.id;
}
@Override public int hashCode() {
return Long.hashCode(id);
}
}✅ Règle : si tu mets un objet en HashSet/HashMap key, tu dois maîtriser equals/hashCode.
Map<K,V> : clé → valeur
Map est un dictionnaire : clés uniques, valeur associée. En backend : cache local, indexation, comptage, regroupements, headers, params.
java.util.Mapages = new java.util.HashMap<>(); ages.put("Alice", 30); ages.put("Bob", 25); Integer a = ages.get("Alice"); // 30 boolean hasBob = ages.containsKey("Bob");
Itération (correcte)
for (java.util.Map.Entrye : ages.entrySet()) { System.out.println(e.getKey() + "=" + e.getValue()); }
Opérations utiles (backend)
get peut retourner null si la clé n’existe pas (ou valeur null). Utilise des patterns sûrs :
int v1 = ages.getOrDefault("Charlie", 0);
ages.putIfAbsent("Dan", 40);
ages.compute("Alice", (k, oldVal) -> oldVal == null ? 1 : oldVal + 1);Ordre & tri
HashMap: pas d’ordre garantiLinkedHashMap: ordre insertionTreeMap: trié
Complexités (raccourci utile)
Ce tableau sert de guide mental pour choisir rapidement. Les coûts peuvent varier selon taille, collisions, etc., mais c’est une bonne base.
| Structure | Accès | Recherche | Ajout | Note |
|---|---|---|---|---|
ArrayList | O(1) par index | O(n) contains | O(1) amorti | Excellent default |
HashSet | - | O(1) avg | O(1) avg | Depends hash |
HashMap | O(1) avg | O(1) avg | O(1) avg | Depends hash |
TreeSet/TreeMap | O(log n) | O(log n) | O(log n) | Sorted |
Perf backend : points critiques
- Boxing :
List<Integer>crée beaucoup d’objets - contains() sur List = O(n) (peut coûter cher)
- Hash collisions : mauvais
hashCode= perf dégradée - Memory : HashMap/HashSet ont un overhead
- Capacity : pré-dimensionner si tu sais la taille
java.util.Mapmap = new java.util.HashMap<>(1024);
✅ Sur gros volumes : penser complexité + mémoire.
Itération : for-each (lisible)
java.util.Listlist = java.util.List.of("a", "b"); for (String v : list) { System.out.println(v); }
Piège : modifier une collection pendant l’itération
Modifications pendant un for-each peuvent déclencher une exception. Utilise un iterator si tu dois supprimer.
java.util.Listxs = new java.util.ArrayList<>(); xs.add("a"); xs.add("b"); java.util.Iterator it = xs.iterator(); while (it.hasNext()) { String v = it.next(); if ("a".equals(v)) { it.remove(); } }
Tri (List)
java.util.Listnums = new java.util.ArrayList<>(); nums.add(3); nums.add(1); nums.add(2); nums.sort(java.util.Comparator.naturalOrder());
Stream (utile mais pas obligatoire)
java.util.Listout = nums.stream() .map(Object::toString) .toList();
✅ En backend, streams ok pour pipelines simples ; sinon boucle claire.
Patterns backend incontournables
1) Déduplication (Set)
java.util.Setseen = new java.util.HashSet<>(); for (String id : ids) { if (!seen.add(id)) { // duplicate } }
2) Comptage / histogram (Map)
java.util.Mapcounts = new java.util.HashMap<>(); for (String w : words) { counts.merge(w, 1, Integer::sum); }
3) Grouping (Map -> List)
java.util.Map> byType = new java.util.HashMap<>(); for (String item : items) { String type = "t"; // compute type byType.computeIfAbsent(type, k -> new java.util.ArrayList<>()).add(item); }
Checklist de choix (rapide)
| Question | Choix |
|---|---|
| J’ai besoin d’ordre + index ? | List / ArrayList |
| Je veux garantir unicité ? | Set / HashSet |
| Je veux clé → valeur ? | Map / HashMap |
| Je veux préserver insertion order ? | LinkedHashMap/LinkedHashSet |
| Je veux tri naturel ? | TreeMap/TreeSet |
✅ Collections bien choisies = code plus simple, plus rapide, plus fiable.
try-catch-finally)Java gère les erreurs via les **Exceptions**. Il n'y a pas de Result comme en Rust. Quand une erreur se produit, une Exception est "levée" (throw).
On gère les exceptions avec try-catch.
try-catch-finally
public void lireFichier(String chemin) {
FileReader reader = null;
try {
// 1. (TRY) Code risqué (peut lever une exception)
reader = new FileReader(chemin);
// ... lire le fichier ...
} catch (FileNotFoundException e) {
// 2. (CATCH) Gérer l'erreur spécifique
System.err.println("Erreur: Fichier non trouvé: " + chemin);
} catch (IOException e) {
// 3. (CATCH) Gérer une erreur plus générique
System.err.println("Erreur I/O: " + e.getMessage());
} finally {
// 4. (FINALLY) Exécuté TOUJOURS (succès ou erreur)
// (Idéal pour fermer les ressources)
if (reader != null) {
try { reader.close(); } catch (IOException e) { /* Ignoré */ }
}
}
}
Checked vs Unchecked Exceptions :
1. Checked (ex: IOException) : Le compilateur vous *force* à les gérer (avec try-catch ou en ajoutant throws à votre méthode).
2. Unchecked (ex: NullPointerException) : Erreurs de programmation. Le compilateur ne vous force pas. (Elles héritent de RuntimeException).
<T>, sécurité de type, bornes, wildcards, type erasureLes generics (<T>) permettent d’écrire des classes/méthodes réutilisables qui fonctionnent avec n’importe quel type, tout en garantissant la sécurité de type à la compilation. En backend, c’est essentiel pour les collections, les APIs, les repositories, les DTOs, et pour éviter les ClassCastException en production.
Le problème sans generics (avant Java 5)
Sans generics, une collection stocke des Object. Tu peux mélanger les types, et tu découvres le problème trop tard (runtime).
java.util.List list = new java.util.ArrayList();
list.add("Hello");
list.add(123); // mixed types
String a = (String) list.get(0); // ok
String b = (String) list.get(1); // ClassCastException at runtime⚠️ Le pire en prod : bug tardif, stacktrace, et données potentiellement corrompues.
La solution avec generics : sécurité à la compilation
Le compilateur empêche les insertions invalides et supprime le besoin de cast.
java.util.Listlist = new java.util.ArrayList<>(); list.add("Hello"); // list.add(123); // compilation error String s = list.get(0); // no cast
Bénéfices clés
- Moins de bugs runtime (
ClassCastException) - API plus claire (intention du type)
- Refactor plus sûr
- Meilleure auto-complétion IDE
Variables de type : T, K, V…
T: Type (générique)E: Element (collections)K/V: Key/Value (maps)R: Return type (souvent)
java.util.Lista = new java.util.ArrayList<>(); java.util.Map b = new java.util.HashMap<>(); java.util.Set c = new java.util.HashSet<>();
Diamond operator <>
Le compilateur déduit le type à droite.
java.util.Listxs = new java.util.ArrayList<>();
Génériques imbriqués
java.util.Map> stats = new java.util.HashMap<>(); stats.put("a", java.util.List.of(1, 2, 3));
Important : generics = compile-time
La sécurité de type est vérifiée à la compilation. À l’exécution, les infos génériques sont en grande partie effacées (type erasure).
✅ Résultat : tu gagnes en sûreté, sans coût runtime direct sur les structures de base.
Créer une classe générique
Une classe générique s’écrit avec une variable de type.
public class Box{ private T value; public Box(T value) { this.value = value; } public T get() { return value; } public void set(T value) { this.value = value; } }
Boxa = new Box<>("hello"); String s = a.get();
Exemple backend : Result / Response wrapper
Pattern fréquent : wrapper de résultat standardisé (data + erreurs).
public class Result{ private final T data; private final String error; private Result(T data, String error) { this.data = data; this.error = error; } public static Result ok(T data) { return new Result<>(data, null); } public static Result fail(String error) { return new Result<>(null, error); } public boolean isOk() { return error == null; } public T getData() { return data; } public String getError() { return error; } }
✅ Tu factorises la gestion d’erreur sans perdre la sécurité de type.
Créer une méthode générique
Une méthode générique déclare son type avec <T> avant le retour.
public staticT first(java.util.List list) { return list.get(0); }
String s = first(java.util.List.of("a", "b"));
Integer n = first(java.util.List.of(1, 2, 3));Method generic + multiple params
public staticjava.util.Map singletonMap(K k, V v) { java.util.Map m = new java.util.HashMap<>(); m.put(k, v); return m; }
Utilité en pratique
- Helpers réutilisables
- Mapping / conversion type-safe
- Factories
Borne supérieure : <T extends X>
Tu limites T à des types qui héritent de X (ou implémentent une interface). Ça permet d’utiliser les méthodes de X sur T.
public staticdouble sum(java.util.List xs) { double s = 0.0; for (T x : xs) { s += x.doubleValue(); } return s; }
✅ Ici, T est garanti d’être un Number, donc doubleValue() existe.
Borne sur interface (ex: Comparable)
public static> T max(T a, T b) { return (a.compareTo(b) >= 0) ? a : b; }
Borne multiple
Un type peut être borné par une classe + des interfaces.
public static> T pick(T a, T b) { return (a.compareTo(b) >= 0) ? a : b; }
Pourquoi les wildcards ? existent
Les generics en Java sont invariants : List<Integer> n’est pas un List<Number>. Les wildcards servent à exprimer “je veux accepter une famille de types”.
// Not allowed (invariance):
// java.util.List xs = java.util.List.of(1, 2, 3); ? extends (producer)
“Je lis des éléments” : la liste produit des Number (ou sous-types).
public static double sumNumbers(java.util.List : extends Number> xs) {
double s = 0.0;
for (Number n : xs) { s += n.doubleValue();
return s;
}
? super (consumer)
“Je veux ajouter des éléments” : la structure consomme des Integer.
public static void addOnes
(java.util.List super Integer> xs) {
xs.add(1);
xs.add(1);
// Reading gives Object (safe only):
Object v = xs.get(0);
}Règle mémo : PECS
- Producer →
? extends - Consumer →
? super
✅ En backend : très utile pour APIs utilitaires et librairies.
Type erasure : où passent les types au runtime ?
Java implémente les generics via effacement de type : à l’exécution, List<String> et List<Integer> sont tous deux un List.
- Impossible de faire
new T() - Impossible de faire
new T[]directement - Impossible de tester
if (x instanceof List<String>)
// Not allowed:
// public class Box { T v = new T(); } Pièges fréquents
| Piège | Impact | Fix |
|---|---|---|
Raw types (List sans <T>) | Perte de sûreté | Toujours typer |
| Cast “à l’aveugle” | ClassCastException | Garder generics partout |
Confusion ? extends / ? super | API impossible à utiliser | PECS |
| Comparer types génériques runtime | Impossible | Passer Class<T> si besoin |
Solution runtime : passer un Class<T> (si besoin)
public staticT newInstance(Class cls) { try { return cls.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("cannot instantiate", e); } }
synchronized, JMM, java.util.concurrent, pools, pitfallsLa concurrence permet d’exécuter plusieurs tâches “en parallèle” (ou de façon entrelacée) via des threads. En backend, elle sert surtout à exploiter les CPUs, améliorer le throughput, et gérer l’I/O (réseau, DB, files). Le défi : éviter les bugs subtils (race conditions, deadlocks, visibilité mémoire) tout en restant performant.
Concurrence ≠ parallélisme (mais lié)
- Concurrence : plusieurs tâches progressent (interleaving possible)
- Parallélisme : plusieurs tâches tournent réellement en même temps (multi-core)
Les 3 problèmes classiques
- Race condition : deux threads modifient une donnée sans coordination
- Visibilité mémoire : un thread ne “voit” pas la mise à jour d’un autre
- Ordre d’exécution : réordonnancements CPU/JIT (Java Memory Model)
✅ La plupart des bugs concurrency ne se reproduisent pas facilement : ils “apparaissent” en prod.
Priorité backend : throughput + stabilité
- Ne pas créer un thread par requête (mauvais scaling)
- Utiliser des pools + backpressure
- Préférer primitives de
java.util.concurrentà du lock “manuel” - Éviter l’état mutable partagé
Règle terrain
Si tu peux éviter de partager un état mutable entre threads, fais-le. Sinon : synchronisation explicite, atomics ou collections concurrentes.
Créer un thread (baseline)
On préfère implémenter Runnable (ou Callable) plutôt qu’hériter de Thread.
Runnable task = () -> {
System.out.println("Running in a worker thread");
try { Thread.sleep(300); } catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore interrupt flag
}
};
Thread t = new Thread(task);
t.start();
System.out.println("Main continues");join() : attendre la fin
Thread t = new Thread(() -> { /* work */ });
t.start();
try {
t.join(); // wait
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}Pourquoi “Thread direct” est rarement une bonne idée
- Création coûteuse (stack, scheduling)
- Pas de contrôle de débit (risque de saturation)
- Pas de gestion unifiée (timeouts, queue, monitoring)
Callable<T> : tâche qui retourne une valeur
java.util.concurrent.Callablec = () -> 42; // Typically executed by an ExecutorService
✅ Pour du code pro : threads → pools (Executors).
synchronized : exclusion mutuelle + visibilité
synchronized crée un verrou (monitor) : un seul thread dans la section critique. Bonus : garantit aussi une forme de happens-before (visibilité mémoire).
public class Counter {
private int c = 0;
public synchronized void inc() {
c++;
}
public synchronized int get() {
return c;
}
}Bloc synchronisé sur un lock dédié
public class SafeBox {
private final Object lock = new Object();
private int v;
public void set(int x) {
synchronized (lock) {
v = x;
}
}
public int get() {
synchronized (lock) {
return v;
}
}
}Éviter : synchroniser sur this ou un objet public
Si tu synchronises sur un lock exposé, un code externe peut interférer (blocages imprévus). Préfère un lock privé dédié.
wait/notify (concept) – rarement nécessaire aujourd’hui
Utilisés pour coordonner des threads sur un monitor, mais les outils modernes (BlockingQueue, CountDownLatch, Semaphore) sont plus sûrs.
// prefer java.util.concurrent primitives over wait/notify
✅ En prod : vise des primitives de haut niveau plutôt que de réinventer un scheduler.
Java Memory Model (JMM) : la visibilité n’est pas automatique
Sans synchronisation, un thread peut lire une valeur “ancienne” (cache CPU, reorderings). C’est contre-intuitif : même un booléen peut poser problème.
volatile : visibilité (pas atomicité)
public class StopFlag {
private volatile boolean stop = false;
public void requestStop() { stop = true; }
public void runLoop() {
while (!stop) {
// work
}
}
}✅ volatile est parfait pour un flag, config “read-mostly”, publication simple.
Piège : volatile ne rend pas ++ atomique
count++ = read + add + write (3 opérations). Même en volatile : race possible.
public class BadCounter {
private volatile int c = 0;
public void inc() { c++; } // NOT atomic
public int get() { return c; }
}Solution : atomics ou locks
public class GoodCounter {
private final java.util.concurrent.atomic.AtomicInteger c =
new java.util.concurrent.atomic.AtomicInteger(0);
public void inc() { c.incrementAndGet(); }
public int get() { return c.get(); }
}Primitives modernes (haute fiabilité)
| Outil | Usage | Pourquoi |
|---|---|---|
Atomic* | compteurs, flags | lock-free / simple |
Lock/ReentrantLock | locks avancés | tryLock, fairness |
Semaphore | limiter concurrence | throttling |
CountDownLatch | attendre N événements | coordination simple |
BlockingQueue | producer/consumer | backpressure |
ConcurrentHashMap | map concurrente | scale multi-thread |
✅ Beaucoup de problèmes “classiques” se résolvent en choisissant la bonne primitive.
Exemples rapides
1) Throttling avec Semaphore
java.util.concurrent.Semaphore sem = new java.util.concurrent.Semaphore(10);
void callExternal() {
try {
sem.acquire();
// do the call
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
sem.release();
}
}2) Producer/Consumer avec BlockingQueue
java.util.concurrent.BlockingQueueq = new java.util.concurrent.ArrayBlockingQueue<>(100); void producer() throws InterruptedException { q.put("job"); } String consumer() throws InterruptedException { return q.take(); }
ExecutorService : exécuter sans gérer les threads à la main
Le pool réutilise des threads, limite la création, et permet de piloter le débit.
java.util.concurrent.ExecutorService pool =
java.util.concurrent.Executors.newFixedThreadPool(8);
java.util.concurrent.Future f =
pool.submit(() -> {
// heavy work
return 123;
});
try {
Integer r = f.get(); // waits
} catch (Exception e) {
throw new RuntimeException("task failed", e);
} finally {
pool.shutdown();
} Timeouts (important)
try {
Integer r = f.get(200, java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (java.util.concurrent.TimeoutException e) {
// handle timeout
}Bon réflexe : shutdown propre
Un pool non stoppé garde des threads vivants → leak / app qui ne s’arrête pas.
pool.shutdown();
try {
if (!pool.awaitTermination(1, java.util.concurrent.TimeUnit.SECONDS)) {
pool.shutdownNow();
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}ScheduledExecutor (tâches périodiques)
java.util.concurrent.ScheduledExecutorService sched =
java.util.concurrent.Executors.newScheduledThreadPool(1);
sched.scheduleAtFixedRate(() -> {
// periodic job
}, 0, 1, java.util.concurrent.TimeUnit.SECONDS);✅ En prod : pools + queue + timeouts = stabilité.
Deadlocks : quand tout s’arrête
Deadlock typique : Thread A tient lock1 et attend lock2, pendant que Thread B tient lock2 et attend lock1.
final Object lock1 = new Object();
final Object lock2 = new Object();
void a() {
synchronized (lock1) {
synchronized (lock2) { }
}
}
void b() {
synchronized (lock2) {
synchronized (lock1) { }
}
}✅ Fix : ordre unique d’acquisition des locks (toujours lock1 puis lock2).
Pièges fréquents (prod)
| Piège | Impact | Fix |
|---|---|---|
| État mutable partagé | Race / corruption | Immutabilité / confinement |
| Volatile utilisé pour des opérations non atomiques | Résultats faux | Atomic / lock |
| Créer trop de threads | Context switching / OOM | Pool + queue |
| Pas de timeouts | Threads bloqués | Timeouts + cancel |
| Locks dans ordre variable | Deadlock | Lock ordering |
Checklist “concurrency safe”
- Limiter l’état partagé
- Préférer
java.util.concurrentàsynchronizedmanuel - Utiliser pools + backpressure
- Ajouter timeouts + interruption
- Surveiller (threads, queue size, latency)
Spring Boot est le standard de facto pour construire des backends Java modernes (API REST, microservices), car il fournit : auto-configuration, starter dependencies, serveur embarqué, Injection de Dépendances (IoC/DI) et une stack complète pour produire du “prod-ready” : observabilité (Actuator), sécurité, données, configuration, packaging, tests.
Le cœur : IoC / DI (Inversion of Control)
Au lieu d’instancier toi-même des objets (new partout), tu laisses Spring créer et assembler les composants (beans). Résultat : couplage faible, testabilité, composants interchangeables.
- Bean : objet géré par le container Spring
- Injection : fournir les dépendances (souvent via constructeur)
- Scan : détection via annotations
Annotations de base
@SpringBootApplication: bootstrap + scan + auto-config@Component,@Service,@Repository: beans@Configuration+@Bean: config explicite
Injection par constructeur (best practice)
Favorise l’injection par constructeur : dépendances explicites, champs final, plus simple à tester (mocks).
@Service
public class UserService {
public String getName(long id) {
return "User-" + id;
}
}
@RestController
@RequestMapping("/api/v1")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/{id}")
public String getUser(@PathVariable long id) {
return userService.getName(id);
}
}✅ Évite @Autowired sur champs : moins testable, moins clair.
API REST : contrôleurs & routing
Spring MVC (ou Spring Web) permet de décrire des endpoints via annotations. Tu exposes des routes HTTP, tu reçois des paramètres, tu retournes un payload.
@RestController: JSON par défaut@GetMapping,@PostMapping…@PathVariable,@RequestParam,@RequestBody
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@GetMapping("/{id}")
public java.util.Map get(@PathVariable long id) {
return java.util.Map.of("id", id, "status", "CREATED");
}
} Validation des inputs (pattern essentiel)
En prod, tu dois valider les entrées : types, champs requis, formats. Pattern standard : DTO + validations + erreurs HTTP propres.
public class CreateUserRequest {
public String email;
public int age;
}
@PostMapping
public java.util.Map create(@RequestBody CreateUserRequest req) {
if (req.email == null || req.email.isBlank()) {
throw new IllegalArgumentException("email required");
}
return java.util.Map.of("ok", true);
} ✅ Ensuite : handler global pour mapper exceptions → HTTP 400/500 (voir modal erreurs).
Configuration : application.properties/application.yml
Spring Boot charge automatiquement la config. Elle dépend du profil actif (dev, prod…).
# application.properties
server.port=8080
app.featureX.enabled=trueProfiles (environnements)
application-dev.properties: local/devapplication-prod.properties: prod
# activate profile (example)
# SPRING_PROFILES_ACTIVE=prodBinder de config (pattern)
Pattern courant : “regrouper” les propriétés sous un préfixe et les exposer via une classe.
@Configuration
public class AppConfig {
@Bean
public java.util.Random random() {
return new java.util.Random();
}
}✅ Objectif : configuration centralisée, claire, et modifiable sans toucher au code métier.
Data layer : Spring Data JPA (standard)
Spring Boot + JPA/Hibernate permet de gérer des entités et des repositories. Pattern : Entity + Repository + Service.
@Entity
public class UserEntity {
@Id
private Long id;
private String email;
public Long getId() { return id; }
public String getEmail() { return email; }
}public interface UserRepository
extends org.springframework.data.repository.CrudRepository {
} Transactions
En backend, les transactions sont cruciales pour la cohérence. Annotation typique : @Transactional sur service.
@Service
public class UserAppService {
private final UserRepository repo;
public UserAppService(UserRepository repo) {
this.repo = repo;
}
@org.springframework.transaction.annotation.Transactional
public void updateEmail(Long id, String email) {
UserEntity u = repo.findById(id)
.orElseThrow(() -> new RuntimeException("not found"));
// u.setEmail(email); // example
repo.save(u);
}
}✅ Transactions = cohérence + rollback en cas d’erreur.
Spring Security : protection des APIs
Spring Security gère authentification + autorisations : basic auth, session, tokens, filters, règles par endpoint.
- Authentication : qui est l’utilisateur ?
- Authorization : a-t-il le droit ?
- Filters : pipeline de sécurité HTTP
✅ En microservices : souvent tokens (JWT/OAuth2) + gateway (selon archi).
Règles d’accès (concept)
Pattern : sécuriser des routes sensibles, garder des endpoints publics. L’important : centraliser les règles plutôt que disperser du code.
// Security configuration is typically centralized
// Example intent:
// - /actuator/health : public
// - /api/v1/admin/** : restricted
// - /api/v1/** : authenticated✅ Objectif : sécurité “by default”, pas “au cas par cas”.
Actuator : endpoints d’observabilité
Actuator expose des endpoints utiles en prod : health, metrics, info, env (à sécuriser). C’est la base pour brancher monitoring/alerting.
/actuator/health: état de l’app/actuator/metrics: métriques/actuator/info: info build
✅ Indispensable en Kubernetes / ALB / autoscaling (health checks).
Logs & tracing (prod)
En microservices, tu veux :
- Logs structurés (requestId, userId)
- Metrics (latency, throughput, errors)
- Traces distribuées (corrélation inter-services)
✅ Spring Boot est conçu pour s’intégrer facilement à ces outils (selon stack).
Tests : unit vs integration
- Unit tests : services isolés (mocks)
- Integration tests : contexte Spring + DB in-memory (selon besoins)
- Tests web : controller tests, contract tests
La DI facilite le test : tu injectes un fake au lieu d’une implémentation réelle.
public class FakeSender implements EmailSender {
public int calls = 0;
@Override public void send(String to, String subject, String body) {
calls++;
}
}Packaging : jar exécutable
Spring Boot produit un jar “fat” exécutable avec serveur embarqué.
# Concept:
# build -> app.jar
# run -> java -jar app.jarChecklist prod-ready
- Config externalisée (env/profiles)
- Health checks (Actuator)
- Timeouts, limites, thread pools
- Logs/metrics/tracing
- Security (actuator endpoints compris)
✅ Spring Boot = accélérateur : “from code to production” rapidement, proprement.
Cheat-sheet “terrain” : snippets courts, prêts à copier. Idéal pour réviser vite avant un entretien ou coder un backend (API, services, jobs, microservices).
Types, variables, final, conditions
// Primitives
int i = 10;
long l = 1_000_000L;
double d = 5.5;
boolean ok = true;
char c = 'A';
// Reference types
String s = "hello";
// Constant (by convention: UPPER_SNAKE_CASE)
final String APP_NAME = "Demo";
// If / else
if (i > 0) { }
else { }
// Switch (classic)
switch (i) {
case 1: break;
default: break;
}Loops + main
// for
for (int x = 0; x < 3; x++) { }
// while
int k = 0;
while (k < 3) { k++; }
// for-each
java.util.List xs = java.util.List.of("a", "b");
for (String v : xs) { }
// main
public static void main(String[] args) {
System.out.println("ok");
} Class, fields, constructor, method
public class User {
private final String email;
private int age;
public User(String email, int age) {
this.email = email;
this.age = age;
}
public String getEmail() { return email; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}Static vs instance
public class MathUtil {
public static int add(int a, int b) { return a + b; }
}
int r = MathUtil.add(1, 2);Inheritance / Interface
class Admin extends User {
public Admin(String email, int age) { super(email, age); }
}
interface Greeter { String hi(); }
class Bot implements Greeter {
@Override public String hi() { return "hi"; }
}equals / hashCode (key in maps/sets)
static class Key {
final long id;
Key(long id) { this.id = id; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Key)) return false;
return id == ((Key) o).id;
}
@Override public int hashCode() { return Long.hashCode(id); }
}List / Set
// List (ordered, duplicates OK)
java.util.List list = new java.util.ArrayList<>();
list.add("A");
list.add("B");
String first = list.get(0);
// Set (unique)
java.util.Set set = new java.util.HashSet<>();
set.add("A");
set.add("A"); // ignored
boolean hasA = set.contains("A"); Sort (List)
java.util.Listnums = new java.util.ArrayList<>(); nums.add(3); nums.add(1); nums.add(2); nums.sort(java.util.Comparator.naturalOrder());
Map
java.util.Mapmap = new java.util.HashMap<>(); map.put("A", 1); map.put("B", 2); int v = map.getOrDefault("C", 0); map.putIfAbsent("D", 4); for (java.util.Map.Entry e : map.entrySet()) { String k = e.getKey(); Integer val = e.getValue(); }
Group-by pattern
java.util.Map> byType = new java.util.HashMap<>(); for (String item : items) { String type = "t"; byType.computeIfAbsent(type, k -> new java.util.ArrayList<>()).add(item); }
Generic class
public class Box{ private T v; public Box(T v) { this.v = v; } public T get() { return v; } } Box b = new Box<>("hello"); String s = b.get();
Generic method
public staticT first(java.util.List xs) { return xs.get(0); }
Bounds + wildcards (PECS)
// Upper bound: T extends Number
public static double sum(java.util.List xs) {
double s = 0.0;
for (T x : xs) s += x.doubleValue();
return s;
}
// Producer extends, Consumer super
public static double sum2(java.util.List extends Number> xs) { return 0.0; }
public static void addOnes(java.util.List super Integer> xs) { xs.add(1); } try / catch / finally
try {
int x = 10 / 0;
} catch (ArithmeticException e) {
System.err.println("division by zero");
} finally {
// cleanup
}throw + wrap (keep cause)
try {
// io or db call
} catch (Exception e) {
throw new RuntimeException("operation failed", e);
}try-with-resources
try (java.io.BufferedReader br =
new java.io.BufferedReader(new java.io.FileReader(path))) {
String line = br.readLine();
} catch (java.io.IOException e) {
throw new RuntimeException("io error", e);
}Custom runtime exception
public class NotFoundException extends RuntimeException {
public NotFoundException(String msg) { super(msg); }
}Basics
java.util.Listxs = java.util.List.of(1, 2, 3, 4); java.util.List evens = xs.stream() .filter(x -> x % 2 == 0) .toList(); int sum = xs.stream() .mapToInt(Integer::intValue) .sum();
Map + distinct + sorted
java.util.Listout = java.util.List.of("b", "a", "a").stream() .distinct() .sorted() .toList();
Grouping / counting
java.util.Mapcounts = words.stream() .collect(java.util.stream.Collectors.groupingBy( w -> w, java.util.stream.Collectors.counting() ));
When to avoid streams
Si c’est une boucle complexe avec beaucoup d’état, une boucle classique est souvent plus lisible.
Concurrency essentials
// Atomic counter
java.util.concurrent.atomic.AtomicInteger c =
new java.util.concurrent.atomic.AtomicInteger(0);
c.incrementAndGet();
// ExecutorService
java.util.concurrent.ExecutorService pool =
java.util.concurrent.Executors.newFixedThreadPool(8);
java.util.concurrent.Future f = pool.submit(() -> 42);
try {
Integer r = f.get(200, java.util.concurrent.TimeUnit.MILLISECONDS);
} catch (Exception e) {
throw new RuntimeException("task failed", e);
} finally {
pool.shutdown();
} Spring Boot mini-snippets
@Service
public class UserService {
public String getName(long id) { return "User-" + id; }
}
@RestController
@RequestMapping("/api/v1")
public class UserController {
private final UserService userService;
public UserController(UserService userService) { this.userService = userService; }
@GetMapping("/users/{id}")
public String get(@PathVariable long id) {
return userService.getName(id);
}
}// Concept:
// build -> app.jar
// run -> java -jar app.jarJava et la JVM sont omniprésents car ils offrent : portabilité, performances (JIT), outillage mature, stabilité en production, et un écosystème énorme. Résultat : beaucoup de logiciels critiques (infra, data, observabilité, CI/CD) et d’applications “grand public” reposent sur Java ou tournent sur la JVM.
Pourquoi autant de logiciels sont en Java ?
- Performance stable : JIT + GC modernes
- Portabilité : même binaire sur Linux/Windows/Mac (avec JVM)
- Écosystème : librairies, frameworks, tooling, monitoring
- Prod-ready : robustesse, tuning, profiling, observabilité
- Équipe & recrutement : compétences largement disponibles
Ce que tu “hérites” quand tu utilises un outil Java
- Config JVM (heap, GC, threads)
- Logs (formats, rotation)
- Métriques (latency, GC pauses)
- Capacité à scaler horizontalement (souvent)
Table “réflexe” : logiciels JVM connus
| Logiciel | Domaine | Pourquoi c’est utilisé |
|---|---|---|
| Elasticsearch | Recherche / Analytics | Indexation + requêtes rapides + agrégations |
| Apache Kafka | Streaming / Messaging | Event bus scalable, haute perf |
| Apache Hadoop | Big Data (batch) | Traitement distribué historique |
| Android apps | Mobile | Java/Kotlin sur runtime Android |
| Minecraft (Java) | Jeu | Écosystème modding + JVM |
| IntelliJ IDEA | IDE | IDE riche construit sur plateforme JVM |
✅ Cette modal te donne surtout le “pourquoi” et comment les utiliser proprement côté backend.
Elasticsearch : recherche & logs (ELK/Elastic stack)
Elasticsearch est un moteur distribué d’indexation/recherche et d’analytics. Usages typiques : recherche full-text, logs, métriques, traces, dashboards.
- Index : documents JSON
- Query : recherche, filtres, agrégations
- Scale : shards + replicas
Exemples d’usage backend (concept)
// Patterns backend:
// 1) index a document
// 2) search by query
// 3) aggregate (count by status, top N, etc.)Points “prod” (à ne pas oublier)
- Heap JVM (ES adore la mémoire, mais éviter l’over-heap)
- GC pauses impactent la latence
- Disque et I/O : indexation = écritures intensives
- Mapping et taille des documents : influence perf + storage
- Monitoring : latence queries, refresh, merges, heap usage
Quand l’utiliser
- Recherche full-text / fuzzy
- Exploration logs à grande échelle
- Analytics rapide sur events/document
✅ Règle : Elasticsearch n’est pas une DB transactionnelle ; c’est un moteur de recherche/analytics.
Kafka : event streaming & message bus
Kafka sert à transporter des événements (logs, clics, transactions, telemetry) entre systèmes, de manière durable et scalable.
- Topic : flux d’événements
- Partition : parallélisme + ordering par clé
- Consumer group : scaling de consommation
- Retention : conservation des messages
Patterns backend
- Producer (service A) → Topic → Consumers (services B/C)
- Event-driven microservices
- Pipeline de données (ETL/ELT)
Points “prod” (latence et fiabilité)
- Choisir le bon partitioning key (ordering souhaité)
- Gérer retries + idempotence (éviter doublons)
- Backpressure côté consumers (ne pas saturer)
- Observer lag (retard de consommation)
- DLQ / dead-letter (selon archi) pour messages invalides
Exemples d’usage backend (concept)
// Producer -> publish event
// Consumer -> process event
// Key idea: decouple services, scale consumers with partitions✅ Kafka est un standard dans les stacks microservices / event-driven.
Hadoop : batch processing (historique)
Hadoop (MapReduce, HDFS) est un framework historique pour traiter de gros volumes en batch. Aujourd’hui, il reste présent dans certaines stacks “legacy” ou environnements data.
- HDFS : stockage distribué
- MapReduce : calcul batch réparti
- Écosystème : coordination, scheduling, etc.
✅ Dans beaucoup d’entreprises, tu peux encore rencontrer du Hadoop dans des pipelines existants.
Ce que ça implique côté engineering
- Jobs batch : latence (minutes/heures), pas du temps réel
- Gestion des échecs : retry, checkpoint, reprise
- Coûts : I/O disque + réseau
- Monitoring : durée jobs, volumes, erreurs, skew
Quand c’est pertinent
- Traitements lourds périodiques (batch)
- Environnements data historiques
- Analyses offline
Android : Java/Kotlin et runtime dédié
Beaucoup d’apps Android ont été écrites en Java. Aujourd’hui Kotlin est dominant sur Android, mais reste dans l’univers JVM et interopère avec Java.
Pourquoi c’est intéressant pour un backend Java
- Partage de concepts (OOP, libs, patterns)
- SDKs clients (API calls) souvent faciles à maintenir
- Formats : JSON, REST, auth tokens
Backend + mobile : points critiques
- Auth : tokens, refresh, expiration
- Réseau instable : retries, timeouts, pagination
- Versioning d’API : compat
- Monitoring : taux d’erreur par version d’app
Exemple “contract” backend (concept)
// Mobile calls:
// GET /api/v1/users/{id}
// POST /api/v1/orders
// Expect stable contracts + versioningMinecraft Java Edition
Minecraft Java Edition est emblématique : il montre que la JVM peut aussi servir à des logiciels grand public (et un écosystème de mods).
Ce que ça illustre
- Portabilité JVM
- Écosystème de plugins/modding
- Perf acceptable avec tuning + bonnes pratiques
Desktop tooling : IDEs (IntelliJ)
Certains outils dev majeurs sont écrits en Java et montrent la force de l’écosystème : UI riche, plugins, analyse statique, indexing, refactors.
Ce que ça inspire côté backend
- Outillage mature (profilers, debuggers)
- Refactors sûrs sur codebase large
- Écosystèmes plugins = architecture modulaire
Comment relier ces outils dans une architecture backend
Beaucoup de stacks modernes utilisent une combinaison :
| Brique | Rôle | Exemple |
|---|---|---|
| API service | expose REST | Spring Boot |
| Messaging | events / async | Kafka |
| Search | full-text + analytics | Elasticsearch |
| Batch | traitements offline | Hadoop (ou autre stack data) |
| Clients | frontends | Android |
Un scénario type
- Service Spring Boot écrit en DB
- Publie un event dans Kafka (“UserCreated”)
- Consumer indexe dans Elasticsearch (recherche rapide)
- Jobs batch (si besoin) consolident / agrègent
Checklist “prod” quand tu utilises des outils JVM
- Dimensionner mémoire/heap (sans over-alloc)
- Observer GC (pauses, allocation rate)
- Définir des limites (threads, queues, timeouts)
- Logs structurés + correlation id
- Monitoring : CPU, heap, latency, errors, queue lag
✅ En pratique : la JVM est une force, mais elle demande un minimum de discipline “ops”.
Résumé en 1 phrase
Java ne sert pas qu’à coder des apps : il fait tourner une partie énorme des outils qui composent l’infrastructure moderne (data, streaming, recherche, tooling).
Ressources “terrain” pour apprendre, diagnostiquer, optimiser, et rester à jour sur Java + Spring + JVM. Objectif : te donner un kit complet (docs officielles, tutorials, outils, communautés, observabilité).
Top 8 “à connaître par cœur”
| Ressource | À quoi ça sert | Quand l’utiliser |
|---|---|---|
| Baeldung | Tutoriels Java/Spring très pragmatiques | Quand tu veux un exemple clair et rapide |
| Java API Docs | Référence des classes standard (JDK) | Quand tu veux le contrat exact d’une classe |
| start.spring.io | Générateur officiel de projet Spring Boot | Quand tu démarres un nouveau service |
| Spring Reference Docs | Docs officielles Spring Boot / Spring Framework | Quand il faut comprendre “comment ça marche” |
| SDKMAN! | Gestion de versions JDK (Linux/macOS) | Quand tu jongles entre Java 8/11/17/21 |
| Eclipse Temurin | Distribution OpenJDK fiable (prod) | Quand tu veux une base JDK standard |
| Maven Central | Recherche de dépendances Java | Quand tu cherches un artifact/version |
| Spring Guides | Guides officiels “cookbook” | Quand tu veux un mini-projet de référence |
Kit “minimum viable dev Java”
- 1 JDK LTS installé (ex: Java 17 ou 21)
- 1 IDE (IntelliJ ou Eclipse)
- 1 build tool (Maven ou Gradle)
- 1 projet Spring Boot généré (start.spring.io)
- 1 routine “debug” : logs + stacktrace + tests
Conseil de navigation
Si tu dois résoudre un bug rapidement : commence par une source “pratique” (Baeldung, guides), puis confirme le comportement exact dans la doc officielle (Java API / Spring reference).
Java – documentation officielle
| Ressource | Description |
|---|---|
| Java Platform / JDK Docs | Documentation officielle de la plateforme Java (référence globale) |
| Java API Documentation | Référence des packages/classes du JDK (contrats, signatures, comportements) |
| JEPs (JDK Enhancement Proposals) | Évolutions du langage et de la JVM (features, motivations, détails) |
| JSR / spécifications (historique) | Textes de specs (utile quand tu veux comprendre les “règles” profondes) |
Quand lire les docs officielles
- Comportement exact (edge cases, exceptions levées)
- Threads / mémoire / contrats de visibilité
- Performance ou compatibilité (version JDK)
Conseil : lire “API docs” correctement
- Regarde la signature + les exceptions
- Lis le “contract” (préconditions/postconditions)
- Vérifie si la méthode est thread-safe
- Regarde les implémentations recommandées
Les pages du JDK qui sauvent du temps
java.lang(String, Object, Math)java.util(Collections, Optional)java.util.concurrent(Executors, atomics, locks)java.time(dates modernes)java.nio(I/O performant)
Astuce : pour un bug en prod, la doc te dit souvent “dans quelles conditions” un appel échoue.
Spring Boot & Spring ecosystem
| Ressource | Description |
|---|---|
| start.spring.io | Spring Initializr : générer un projet clean avec starters |
| Spring Boot Reference Documentation | Auto-configuration, starters, properties, Actuator, packaging |
| Spring Framework Reference | Core container, DI/IoC, AOP, Spring MVC, transactions |
| Spring Guides | Recettes officielles : REST, data, security, messaging |
Microservices (en pratique)
- Config externalisée + profils
- Observabilité (health, metrics, logs)
- Time-outs et retries (ne pas saturer)
- Versioning d’API
Ressources “pro” pour patterns d’architecture
| Ressource | Description |
|---|---|
| Baeldung (Spring) | Tutos ciblés : security, transactions, caching, REST, testing |
| Spring Security Docs | Auth, filters, configuration, bonnes pratiques |
| Spring Actuator Docs | Endpoints de santé/métriques, prod-hardening |
Règle terrain : en Spring, quand c’est “magique”, la réponse est dans la doc reference (auto-config + conditions).
Maven / Gradle / dépendances
| Ressource | Description |
|---|---|
| Maven Central | Catalogue principal des artifacts Java |
| Maven Documentation | Cycles de build, plugins, gestion des dépendances |
| Gradle Documentation | Build logic, tasks, dependency management, performance |
| Spring Boot Dependencies (BOM) | Versions alignées (réduit les conflits de dépendances) |
Ce qui te fait gagner du temps
- Comprendre “dependency tree” (conflits de versions)
- Utiliser un BOM / dependency management
- Verrouiller les versions (prod reproducible)
Checklist “build stable”
- Version JDK fixée (toolchain si possible)
- Versions de dépendances cohérentes (BOM)
- Plugins build maîtrisés (shade, spring-boot plugin, etc.)
- CI : build + tests + static checks
Quand ça casse
- ClassNotFound / NoSuchMethod : conflit de versions
- Erreur au runtime : jar “fat” mal construit
- Diff dev/prod : JDK différent ou config différente
JVM, GC, profiling, tuning
| Ressource | Description |
|---|---|
| OpenJDK (concepts JVM) | Comprendre GC, JIT, memory model, options de runtime |
| JFR (Java Flight Recorder) | Profiling production-friendly (CPU, allocations, locks, GC) |
| JMC (Java Mission Control) | Analyse graphique des traces JFR |
| VisualVM | Monitoring/profiling “généraliste” (dev/test) |
Indicateurs à surveiller en prod
- Heap usage + allocation rate
- GC pauses (p95/p99)
- Thread count + blocked threads
- Latence requêtes + erreurs
Ressources “performance mindset”
- Comprendre les pauses GC et leur impact latence
- Observer contention locks (synchronized/locks)
- Différencier CPU-bound vs IO-bound
- Utiliser des benchmarks (JMH) en micro-bench (avec discipline)
Quand tu as un incident prod
- Corréler : trafic vs CPU vs heap vs GC vs DB latency
- Capturer un JFR sur fenêtre courte si possible
- Vérifier : thread pools saturés, timeouts, queues, backpressure
Astuce : la JVM donne énormément de signaux, mais il faut instrumenter (metrics/logs) dès le départ.
IDEs & outils de dev
| Outil | Description |
|---|---|
| IntelliJ IDEA | IDE très puissant (refactor, inspections, Spring support) |
| Eclipse IDE | Historique, très utilisé en entreprise, plugins variés |
| VS Code (Java extensions) | Plus léger, utile pour petits projets ou dev “polyglotte” |
| jenv / SDKMAN! | Gestion multi-JDK (selon OS) |
Outils “ops/dev” utiles
jcmd,jstack,jmap(diagnostic JVM)jps(process Java)- Logging framework (slf4j + impl)
Checklist IDE (gagne du temps)
- Profiling / CPU sampling intégré
- Refactor “safe” (rename, extract, inline)
- Recherche usages + call hierarchy
- Debug (breakpoints conditionnels)
- Inspections “nullability”, “dead code”, “unused”
Qualité code
- Formatter + règles de style cohérentes
- Lint / static analysis (selon stack)
- Tests automatiques exécutés en CI
Communautés & Q/A
| Lieu | Pourquoi c’est utile |
|---|---|
| Stack Overflow (Java/Spring) | Problèmes concrets, erreurs fréquentes, solutions rapides |
| GitHub (issues) | Tracker officiel des libs/frameworks (bugs, workarounds, versions) |
| Reddit (java/spring) | Discussions, retours d’expérience, tendances |
| Meetups / conférences | Bon moyen de rester à jour sur JVM/Spring, retours prod |
Quand tu cherches une réponse
- Lire l’erreur exacte + stacktrace
- Identifier versions : JDK, Spring Boot, libs
- Rechercher avec message + classe + version
Veille technique (simple, efficace)
- Suivre les releases JDK (LTS, patchs)
- Suivre releases Spring Boot (security fixes)
- Lire quelques posts “postmortems” / retours prod
- Garder un mini “upgrade plan” (versions, tests)
Checklist “je suis bloqué”
- Reproduire sur un petit projet (minimal repro)
- Réduire le scope : une classe, une config, une requête
- Vérifier compat versions (JDK + Spring + libs)
- Confirmer dans docs officielles
✅ Avec cette page, tu as un “répertoire mental” : apprendre vite, débugger vite, produire vite.
