Project Oxygen & Ideo-LabIDEO LAB Dashboard 2026

☕ Java – La Plateforme "Write Once, Run Anywhere"

Guide complet IDEO-Lab : JVM, SDK/JRE, OOP, Collections & Spring Boot.

1.1 Facile

La JVM & le Bytecode

"Write Once, Run Anywhere" (WORA). .java -> .class.

JVM Bytecode
1.2 Facile

Pourquoi Java ?

Portabilité (JVM), Écosystème (Enterprise), OOP, Garbage Collector.

Portabilité Garbage Collector
1.3 Facile

JDK vs JRE & Installation

SDK (Compiler javac) vs JRE (Exécuter java). OpenJDK.

JDK SDKMAN
1.4 Moyen

Build Tools (Maven/Gradle)

pom.xml (Maven) ou build.gradle (Gradle) pour les dépendances.

Maven Gradle
2.1 Facile

Types Primitifs & final

int, double, boolean, char. final (immuable).

int double
2.2 Facile

Types Objets & null

String, Integer (Wrappers). Le danger de null.

String null
2.3 Facile

Structures de Contrôle

if/else, for, while, switch, for-each.

for switch
2.4 Facile

Méthodes & main

public static void main(String[] args). static vs instance.

main static
3.1 Facile

OOP : Classes & Objets

class User { ... }, User u = new User();, Constructeurs.

class new
3.2 Moyen

OOP : Héritage

class Admin extends User, super(), @Override.

extends @Override
3.3 Facile

OOP : Encapsulation

public, private, protected. Getters & Setters.

private get/set
3.4 Moyen

OOP : Abstraction

abstract class vs interface. implements.

abstract interface
4.1 Moyen

Collections Framework

List, Map, Set (ArrayList, HashMap).

List Map
4.2 Moyen

Gestion des Erreurs

try-catch-finally, Exceptions (Checked vs Unchecked).

try/catch Exception
4.3 Moyen

Generics (Génériques)

List, Map. Sécurité de type.

Generics <T>
4.4 Avancé

Concurrence

Thread, Runnable, synchronized, java.util.concurrent.

Thread synchronized
5.1 Moyen

Écosystème : Spring Boot

Framework N°1 (Web, Microservices). Injection de dépendances.

Spring Boot Web
5.2 Facile

Cheat-sheet (Syntaxe)

Syntaxe de base (class, main, List, Map).

cheat Syntaxe
5.3 Moyen

Logiciels utilisant Java

Elasticsearch, Kafka, Minecraft, Applications Android.

Elasticsearch Kafka Android
5.4 Facile

Liens Utiles

Documentation officielle, Baeldung, SDKMAN, Temurin.

Documentation Baeldung
Chapitre 0 — Panorama & cartes mentales (Java au centre : JVM, écosystème, stacks, choix selon contexte)

Objectif : 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)
TermeSignification
JDKKit de dev : javac + outils + runtime
JRERuntime : JVM + libs (souvent inclus dans le JDK moderne)
JVMMachine virtuelle : exécute le bytecode + gère mémoire/threads
BytecodeFormat intermédiaire portable (.class)
WORAWrite Once, Run Anywhere (via JVM)
Carte mentale (format “bulles”)
NiveauÉlémentsExemples
CentreJDK/JRE, JVM, bytecode, libs standardGC, JIT, collections, concurrency
CadresJava SE / Jakarta EE / SpringServlet, JPA, DI, MVC
ÉcosystèmeBuild, test, libs, observabilityMaven/Gradle, JUnit, logging, metrics
PlateformesBackend, data, cloud, mobile, desktopREST, Kafka, Android, tooling
OpsDé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
MondeForceQuand c’est un bon choix
Java SEFondation + perf + stabilitéPartout (base obligatoire)
Jakarta EESpecs enterprise standardiséesCorporate/legacy, standards imposés
SpringProductivité + écosystème completAPI, microservices, time-to-market

✅ En pratique : beaucoup de stacks modernes = Java SE + Spring Boot.

Les grands axes de l’écosystème
AxeObjectifExemples typiques
BackendAPIs, services, microservicesREST, DI, transactions, auth
DataStockage, traitement, pipelinesORM, SQL, caching, batch
CloudDéploiement, scalabilité, opscontainers, health, autoscaling
MobileApps Androidclients API, offline, auth
DesktopOutils & apps desktopIDEs, clients, tooling
Big DataStreaming/batchKafka, pipelines data
ObservabilityMonitoring, traces, logsmetrics, health, profiling
SecurityProtection & complianceauthN/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/alerts
Ce 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)
ContexteRecommandationPourquoi
Projet standardOpenJDK distribution stable (ex: Temurin)Compat + stabilité + patchs
Entreprise réguléeVendor avec support longSLA + compliance + patching
High-scaleFocus tuning + observabilityGC, 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
  1. Quel type d’application ? (API, batch, streaming, mobile…)
  2. Quel niveau de contraintes ? (sécurité, audit, conformité)
  3. Quel niveau de charge ? (QPS, latence p95/p99, pics)
  4. Quel mode de déploiement ? (VM, containers, K8s)
  5. Quel niveau de legacy ? (Java 8, app server, monolithe)
  6. 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)
BesoinChoix typiqueNotes
API REST rapideSpring Boot WebTime-to-market
Data transactionnelleSpring Data JPA + DBTransactions, migrations
Events & asyncKafka + consumersDécouplage, throughput
Recherche full-textElasticsearchIndexation séparée
Auth/rolesSpring SecurityCentraliser règles
Prod monitoringActuator + metrics/logsHealth 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)
QuestionOuiNon
API publique à sécuriser ?Security + rate limit + auditSecurity simple
Latence critique (p99) ?Perf + profiling + cachesStack standard
Événements / async requis ?Kafka + consumersREST sync
Recherche full-text ?Search engineDB queries
Conformité forte ?Policies + support + auditLight 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)
BriqueChoix typiquePourquoi
RuntimeJDK LTSSupport long + stabilité
FrameworkSpring BootProd-ready + rapide
DataJPA + DBTransactions + productivité
SecuritySecurityCentralisation règles
ObsActuator + logsExploitation prod

✅ Ensuite seulement : Kafka, Elasticsearch, caches avancés, etc.

1.1 La JVM & le Bytecode – Architecture Interne
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.2 Pourquoi Java ? – Analyse Complète & Enterprise
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.

1.3 JDK vs JRE & Installation – OpenJDK, SDKMAN, PATH
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 Hello
Résumé opérationnel
BesoinInstallerPourquoi
Lancer un jar en prodJDK (recommandé)Outils + diagnostics + compat
Compiler / devJDKjavac + tooling
CI/CD buildJDKMaven/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)
ContexteChoixJustification
Nouveau projet SpringJDK 21 LTSÉquilibre features / support
Legacy stableJDK 17 LTSCompat + migration progressive
Très ancien SIJDK 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 version
Installer 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 javac
Bonnes 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 .sdkmanrc par projet (version locale)
Version locale par projet (optionnel mais pro)
# In project directory
                            sdk env init

                            # Edit .sdkmanrc, then:
                            sdk env
Approche 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 -version
Option B : Chocolatey (si présent)
choco search temurin
                            choco install temurin21
                            java -version
Configurer 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.

VariableValeurBut
JAVA_HOMEPath du JDKRéférence unique
PATH%JAVA_HOME%\binAccè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 -v
Erreurs classiques
ErreurCause probableFix
javac not foundJRE only / PATH incorrectInstall JDK + fix PATH
Unsupported class file major versionRuntime too oldUpgrade runtime or target
java points to wrong JDKMultiple installsFix 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 -version at startup logs
  • Memory flags only when needed (measure first)
  • Use JRE image for runtime, JDK for build
1.4 Build Tools – Maven & Gradle (Dependencies, Lifecycle, Reproducible Builds)

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:tree
Structure 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 tasks
Structure 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èreMavenGradle
Standard enterpriseExcellentTrès bon
Configuration simpleStable (XML)DSL (plus flexible)
Builds complexesPossibleExcellent
Performance gros buildsBonneTrès bonne
ToolchainsOuiOui (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.

2.1 Types Primitifs & final – Valeurs, Mémoire, Wrappers, Pièges

Java 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.

TypeTailleUsage typique
byte8 bitsFlux/binaire, buffers
short16 bitsRare (compat/binaire)
int32 bitsIndex, compteurs, ID
long64 bitsTimestamps, big counters
float32 bitsRare (perf/legacy)
double64 bitsMesures, calculs
booleanJVM-dépendantFlags logiques
char16 bitsUTF-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.

TypeDefault (field)
Entiers0
float/double0.0
booleanfalse
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 + reference
Cas 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 unboxing
Primitives 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;      // OK
Conversions 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 corrupted
Piè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 safer
Piè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).

PrimitifWrapper
intInteger
longLong
doubleDouble
booleanBoolean
charCharacter
byteByte
shortShort
floatFloat
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 error
Sur 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 int par défaut, long pour timestamps/counters
  • Utilise double pour mesures, BigDecimal pour argent
  • Évite float (sauf cas spécifiques)
  • Évite short (rarement utile)
  • Préfère boolean explicite (noms clairs)
ORM / DTO : primitive ou wrapper ?

Si une valeur peut être “absente” : wrapper. Si elle doit toujours exister : primitive.

CasChoixRaison
Champ obligatoireprimitivejamais null
Champ optionnelwrappernull possible
Pratiques final
  • Marque en final ce qui ne doit pas changer
  • Utilise final sur 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.

2.2 Types Objets & null – Références, Wrappers, String, Optional, NPE

Tout 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 : nullNullPointerException.

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 object
Stack 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 correct
Concaté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 (Integer peut être null)
  • Utilitaires : parse, compare, constants
  • Interop (frameworks, ORM, serializers)
Autoboxing / Unboxing
Integer a = 10;  // boxing
                            int b = a;        // unboxing
Pièges wrappers

1) Unboxing + null = NPE

Integer n = null;
                            // int x = n;   // NPE at runtime

2) 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();  // NPE

Dans 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 null

3) streams / lambdas

List users = 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.

Optional opt = 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 null quand 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
Optional o = 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ère Optional ou exceptions contrôlées
  • Évite == sur objets : préfère equals / 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
SituationRisqueFix
role.equals("X")NPE"X".equals(role)
Wrapper -> primitiveNPEDefault / requireNonNull
Collections nullNPEemptyList/emptyMap
Chaining gettersNPEValidate + Optional/map

✅ Le but n’est pas “mettre des if partout” : le but est d’imposer un contrat, et de rendre l’absence explicite.

2.3 Structures de Contrôle – Conditions, Boucles, switch, Patterns & Pièges

Les 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.List names = java.util.List.of("Alice", "Bob");

                            for (String name : names) {
                            System.out.println(name);
                            }
Map iteration
java.util.Map map = 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.List list = 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
ErreurImpactFix
Oublier break (switch classique)Comportement imprévuAjouter break ou utiliser switch expression
while sans mise à jourBoucle infinieIncrément / condition fiable
Modifier une liste en for-eachExceptionIterator / copy / filter
if/else imbriquésLisibilité faibleFail-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.

2.4 Méthodes & mainstatic vs instance, signatures, overloading

Une 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 three
Cas 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 partout
  • private : uniquement dans la classe
  • protected : 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 (static mutable)
  • 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
QuestionOuiNon
Accède à l’état (this) ?instancestatic
Fonction pure (stateless) ?static OKinstance
Doit être mockée/testée facilement ?instancestatic (à éviter)
Utilitaire simple (parse/format) ?static OKinstance

✅ Objectif : méthodes petites, claires, testables, et un flux d’exécution prévisible.

3.1 OOP : Classes & Objets – Modèle, Constructeurs, Encapsulation, Identité

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 heap

Plusieurs 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 implementation
Pourquoi c’est critique ? (Collections)

Si tu mets des objets dans HashSet / HashMap, equals et hashCode doivent être cohérents.

java.util.Set s = 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
SujetRisqueFix
Champs publicsÉtat incohérentEncapsulation
Setters sans validationDonnées invalidesInvariants
Objets mutables partagésEffets de bordImmutabilité
Equals/hashCode non gérésCollections bugStratégie d’égalité

✅ Une bonne OOP Java = objets cohérents + contrats clairs + état maîtrisé.

3.2 OOP : Héritage & Polymorphisme – extends, super, override, casting

L’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/protected du parent
  • Les champs private du 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 :

ModifAccessible depuis enfant ?Note
privateNonAccessible via getters/methods
protectedOuiAccès dans package + subclasses
(default)Oui (package)Pas hors package
publicOuiAccessible 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é)
  1. Construction du parent (via super)
  2. Initialisation champs enfant
  3. 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.List list = 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 implicit
Downcast (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 runtime
instanceof : 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ègeImpactRéflexe
Abus d’héritage (“deep hierarchy”)Complexité, couplagePréférer composition
Override sans @OverrideBug silencieuxToujours annoter
Downcast sans checkCrash runtimeinstanceof ou refactor
Appel override en constructeurChamps non initÉviter
Confusion overloading/overrideComportement inattenduSignatures 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
QuestionOuiNon
Relation “is-a” évidente ?Héritage possibleComposition
Beaucoup de instanceof ?Refactor designOK
Besoin d’un socle communAbstract classInterface + composition
Invariants critiquesfinal / encapsulationRisque bug

✅ Objectif : un polymorphisme “utile” (extensibilité), sans hiérarchies fragiles.

3.3 OOP : Encapsulation – Modificateurs d’accès, Get/Set, Invariants, API propre

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.

ModifVisibilitéUsage typique
publicPartoutAPI exposée (contrat stable)
protectedPackage + subclassesExtension contrôlée (héritage)
(default)Package uniquementModules internes (même package)
privateClasse 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ègeImpactFix
Fields publicsÉtat incohérentprivate + API
Setters “libres” partoutTransitions illégalesMéthodes métier
Pas de validationDonnées invalidesFail-fast
Exposer une collection interneCorruption d’étatunmodifiable / copy
API trop largeCouplage fortExpose le minimum
Checklist “Encapsulation propre”
  • Fields private par 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”.

3.4 OOP : Abstraction – 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 (new interdit)
  • 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)
BesoinChoixPourquoi
Partager du code + état communabstract classFields + impl partagée
Contrat + multi-implémentationsinterfaceCouplage faible
Besoin d’implémenter plusieurs “capacités”interfaceMultiple interfaces
Imposer un workflow / squeletteabstract classTemplate method
Architecture ports/adaptersinterfaceTestabilité / 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ègeImpactFix
Utiliser héritage partoutCouplage, hiérarchies fragilesPréférer interface + composition
Interface “fourre-tout”Implémentations difficilesDécouper (ISP)
Trop de default methodsContrat flou / comportement impliciteUtiliser avec parcimonie
Abstract class utilisée comme “util”Design artificielStatic 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
QuestionRé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.

4.1 Collections Framework – List, Set, Map, Complexité, Pièges & Patterns

Le 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és
  • Set<E> : pas de doublons (unicité), ordre selon implémentation
  • Map<K,V> : dictionnaire clé/valeur, clés uniques
Règle d’or : interface en type, classe en instanciation
java.util.List a = 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)
BesoinStructureImplémentation typique
Ordre + accès par indexListArrayList
Unicité (pas de doublons)SetHashSet
Lookup clé → valeurMapHashMap
Ordre d’insertion à préserverSet/MapLinkedHashSet/LinkedHashMap
Tri naturel / comparatorSet/MapTreeSet/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.List names = 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.List list = 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.List fixed = 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.Set tags = 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 garanti
  • LinkedHashSet : garde l’ordre d’insertion
  • TreeSet : 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.Map ages = 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.Entry e : 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 garanti
  • LinkedHashMap : ordre insertion
  • TreeMap : 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.

StructureAccèsRechercheAjoutNote
ArrayListO(1) par indexO(n) containsO(1) amortiExcellent default
HashSet-O(1) avgO(1) avgDepends hash
HashMapO(1) avgO(1) avgO(1) avgDepends hash
TreeSet/TreeMapO(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.Map map = new java.util.HashMap<>(1024);

✅ Sur gros volumes : penser complexité + mémoire.

Itération : for-each (lisible)
java.util.List list = 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.List xs = 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.List nums = 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.List out =
                            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.Set seen = new java.util.HashSet<>();
                            for (String id : ids) {
                            if (!seen.add(id)) {
                            // duplicate
                            }
                            }

2) Comptage / histogram (Map)

java.util.Map counts = 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)
QuestionChoix
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.

4.2 Gestion des Erreurs (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).

4.3 Generics – <T>, sécurité de type, bornes, wildcards, type erasure

Les 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.List list = 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.List a = 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.List xs = 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; }
                            }
Box a = 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 static  T 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 static  java.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 static  double 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ègeImpactFix
Raw types (List sans <T>)Perte de sûretéToujours typer
Cast “à l’aveugle”ClassCastExceptionGarder generics partout
Confusion ? extends / ? superAPI impossible à utiliserPECS
Comparer types génériques runtimeImpossiblePasser Class<T> si besoin
Solution runtime : passer un Class<T> (si besoin)
public static  T newInstance(Class cls) {
                            try {
                            return cls.getDeclaredConstructor().newInstance();
                            } catch (Exception e) {
                            throw new RuntimeException("cannot instantiate", e);
                            }
                            }
4.4 Concurrence – Threads, synchronized, JMM, java.util.concurrent, pools, pitfalls

La 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.Callable c = () -> 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é)
OutilUsagePourquoi
Atomic*compteurs, flagslock-free / simple
Lock/ReentrantLocklocks avancéstryLock, fairness
Semaphorelimiter concurrencethrottling
CountDownLatchattendre N événementscoordination simple
BlockingQueueproducer/consumerbackpressure
ConcurrentHashMapmap concurrentescale 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.BlockingQueue q =
                            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ègeImpactFix
État mutable partagéRace / corruptionImmutabilité / confinement
Volatile utilisé pour des opérations non atomiquesRésultats fauxAtomic / lock
Créer trop de threadsContext switching / OOMPool + queue
Pas de timeoutsThreads bloquésTimeouts + cancel
Locks dans ordre variableDeadlockLock ordering
Checklist “concurrency safe”
  • Limiter l’état partagé
  • Préférer java.util.concurrent à synchronized manuel
  • Utiliser pools + backpressure
  • Ajouter timeouts + interruption
  • Surveiller (threads, queue size, latency)
5.1 Écosystème : Spring Boot – Web, DI/IoC, Data, Security, Actuator, tests & prod-ready

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=true
Profiles (environnements)
  • application-dev.properties : local/dev
  • application-prod.properties : prod
# activate profile (example)
                            # SPRING_PROFILES_ACTIVE=prod
Binder 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.jar
Checklist 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.

5.2 Cheat-sheet (Syntaxe) – Java essentials (OOP, Collections, Generics, Errors, Streams, Concurrency, Spring)

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.List nums = new java.util.ArrayList<>();
                            nums.add(3); nums.add(1); nums.add(2);
                            nums.sort(java.util.Comparator.naturalOrder());
Map
java.util.Map map = 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 static  T 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.List xs = 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.List out =
                            java.util.List.of("b", "a", "a").stream()
                            .distinct()
                            .sorted()
                            .toList();
Grouping / counting
java.util.Map counts =
                            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.jar
5.3 Logiciels utilisant Java – Big Data, Recherche, Streaming, Mobile, Jeux, Outils & Prod systems

Java 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
LogicielDomainePourquoi c’est utilisé
ElasticsearchRecherche / AnalyticsIndexation + requêtes rapides + agrégations
Apache KafkaStreaming / MessagingEvent bus scalable, haute perf
Apache HadoopBig Data (batch)Traitement distribué historique
Android appsMobileJava/Kotlin sur runtime Android
Minecraft (Java)JeuÉcosystème modding + JVM
IntelliJ IDEAIDEIDE 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 + versioning
Minecraft 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 :

BriqueRôleExemple
API serviceexpose RESTSpring Boot
Messagingevents / asyncKafka
Searchfull-text + analyticsElasticsearch
Batchtraitements offlineHadoop (ou autre stack data)
ClientsfrontendsAndroid
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).