🚀 Cours Java - Paradigmes de Programmation

Maîtrisez les différentes approches de programmation en Java

Cours du 13 au 17 octobre 2025

📚 Introduction aux Paradigmes de Programmation

🎯 Qu'est-ce qu'un paradigme de programmation ?

Un paradigme de programmation est une approche ou un style de programmation qui définit comment organiser et structurer le code.

Les cinq paradigmes principaux

⚙️ Procédural

Organise le code en séquences d'instructions et de fonctions.

  • Instructions séquentielles
  • Fonctions réutilisables
  • Variables globales/locales

🎭 Orienté Objet

Organise le code autour d'objets combinant données et comportements.

  • Classes et objets
  • Encapsulation
  • Héritage et polymorphisme

🔄 Fonctionnel

Traite le calcul comme l'évaluation de fonctions mathématiques.

  • Fonctions pures
  • Immutabilité
  • Expressions lambda

⚡ Réactif

Gestion de flux de données asynchrones et propagation automatique des changements.

  • Flux d'événements
  • Programmation asynchrone
  • Pattern Observable/Observer

🧩 Logique

Décrit des problèmes via des règles et faits logiques.

  • Prédicats et règles
  • Déduction automatique
  • Programmation déclarative

Architecture Java

// Java → Bytecode → JVM Java Source (.java) ↓ compilation Bytecode (.class) ↓ exécution Java Virtual Machine (JVM) + Java Runtime Environment (JRE) + Java Development Kit (JDK)
💡 JDK disponibles :
  • Oracle JDK : Version commerciale avec support à long terme
  • OpenJDK : Version open source utilisée par de nombreuses distributions

🔤 Les Bases de Java

Types de Variables

Catégorie Type Taille Description
Entiers byte 1 octet -128 à 127
short 2 octets -32,768 à 32,767
int 4 octets ~-2 milliards à 2 milliards
long 8 octets Très grands nombres
Booléen boolean 1 octet true ou false
Caractère char 2 octets Un caractère Unicode
Décimaux float 4 octets Précision simple
double 8 octets Précision double

Structures de Contrôle

Conditions

// Si condition ET condition if (true && true) { // Code à exécuter } // Sinon si condition OU condition else if (true || false) { // Code alternatif } // Sinon else { // Code par défaut }

Boucles

// Boucle Tant que while (condition) { // Code répété tant que condition est vraie } // Boucle Do-While (exécutée au moins une fois) do { // Code exécuté } while (condition); // Boucle Pour for (int i = 0; i < 10; i++) { // Code répété 10 fois }

Tableaux

// Déclaration et initialisation int[] monTableau = new int[5]; // Tableau de 5 entiers // Initialisation avec valeurs int[] nombres = {1, 2, 3, 4, 5}; // Parcours d'un tableau for (int i = 0; i < monTableau.length; i++) { System.out.println(monTableau[i]); }

⚙️ Programmation Procédurale

📖 Définition :

La programmation procédurale organise le code en séquences d'instructions et en fonctions réutilisables. C'est l'approche la plus directe et linéaire.

Fonctions

Une fonction est un bloc d'instructions réutilisable qu'on peut appeler plusieurs fois dans un programme. Elle peut :
  • Accepter des paramètres
  • Retourner une valeur d'un certain type
  • Ou ne rien retourner (void)
// Syntaxe générale d'une fonction TypeDeRetour nomDeLaFonction(TypeParametre1 parametre1, TypeParametre2 parametre2) { // Corps de la fonction return valeurDeTypeTypeDeRetour; } // Fonction sans retour void fonctionSansRetour(TypeParametre parametre) { // Corps de la fonction }

Exemple : Rendu de Monnaie

public class rendumonnaie { public static void main(String[] args) { double montant = 0.97; double[] pieces = {100, 50, 20, 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01}; int[] rendu = new int[pieces.length]; for (int i = 0; i < pieces.length; i++) { while (montant >= pieces[i]) { montant -= pieces[i]; rendu[i]++; } } System.out.println("Rendu de monnaie :"); for (int i = 0; i < pieces.length; i++) { if (rendu[i] > 0) { System.out.println(rendu[i] + " x " + pieces[i] + "€"); } } } }
⚠️ Limites de l'approche procédurale :
  • Difficile à maintenir pour de gros projets
  • Code moins réutilisable
  • Pas de notion d'encapsulation des données

🎭 Programmation Orientée Objet (POO)

📖 Définition :

La POO est un paradigme qui utilise des "objets" pour représenter des données et des comportements. Les objets sont des instances de classes qui définissent leurs caractéristiques.

Les Classes et Objets

// Définition d'une classe class NomDeLaClasse { // Attributs (propriétés) TypeAttribut1 attribut1; TypeAttribut2 attribut2; // Constructeur NomDeLaClasse(TypeAttribut1 param1, TypeAttribut2 param2) { this.attribut1 = param1; this.attribut2 = param2; } // Méthodes (comportements) TypeRetour methode1(TypeParametre parametre) { // Corps de la méthode return valeurDeTypeTypeRetour; } }

L'Héritage

💡 L'héritage permet à une classe de hériter des attributs et méthodes d'une autre classe. Utilisez le mot-clé extends.
public class Equipement { int[] adresseIp = new int[4]; String localisation; void demarrer() { System.out.println("L'équipement démarre..."); } } public class Routeur extends Equipement { String bonjour; // Hérite de tous les attributs et méthodes d'Equipement }

Classes et Méthodes Abstraites

// Classe abstraite : ne peut pas être instanciée directement abstract class ClasseAbstraite { // Méthode abstraite : pas de corps, doit être implémentée abstract void methodeAbstraite(); } class ClasseConcrète extends ClasseAbstraite { @Override void methodeAbstraite() { System.out.println("Implémentation de la méthode abstraite"); } }
✅ Avantages de la POO :
  • Encapsulation : Données et méthodes regroupées
  • Réutilisabilité : Héritage et polymorphisme
  • Maintenabilité : Code plus organisé et modulaire
  • Abstraction : Masque la complexité

🔄 Programmation Fonctionnelle

📖 Définition :

La programmation fonctionnelle traite le calcul comme l'évaluation de fonctions mathématiques. Elle privilégie l'immutabilité et les fonctions pures.

Interfaces Fonctionnelles en Java

// Les principales interfaces fonctionnelles : // Supplier → Retourne un type, attend 0 paramètre Supplier<String> supplier = () -> "Bonjour"; // Consumer → Retourne rien, attend 1 paramètre Consumer<String> consumer = str -> System.out.println(str); // Function → Retourne un type, attend 1 paramètre Function<String, Integer> function = str -> str.length(); // Predicate → Retourne un boolean, attend 1 paramètre Predicate<Integer> predicate = num -> num > 10; // BiFunction → Retourne un type, attend 2 paramètres BiFunction<Integer, Integer, Integer> biFunction = (a, b) -> a + b;

Streams et Opérations Fonctionnelles

List<Integer> listeNombres = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // map : Transforme chaque élément listeNombres.stream() .map(nombre -> nombre * nombre) .forEach(System.out::println); // filter : Garde les éléments qui respectent une condition listeNombres.stream() .filter(nombre -> nombre > 5) .forEach(System.out::println); // reduce : Agrège tous les éléments int sum = listeNombres.stream() .reduce(0, Integer::sum);
✅ Avantages de la programmation fonctionnelle :
  • Concision : Code plus court et expressif
  • Immutabilité : Pas d'effets de bord inattendus
  • Composabilité : Facilite la combinaison d'opérations
  • Parallélisation : Plus facile à paralléliser

⚡ Programmation Réactive

📖 Définition :

La programmation réactive est un paradigme de programmation orienté vers les flux de données asynchrones et la propagation automatique du changement. Elle se base sur le pattern Observable/Observer.

Le Manifeste Réactif

Les applications réactives doivent être :

🎯 Réactives

Répondent rapidement aux événements et requêtes avec une faible latence.

💪 Résilientes

Restent fonctionnelles face aux erreurs et pannes.

📈 Élastiques

S'adaptent dynamiquement à la demande en ajustant les ressources.

📨 Orientées Messages

Utilisent la communication asynchrone basée sur les messages.

Concepts Clés

Flux de Données Asynchrones

Les flux constituent l'épine dorsale de la programmation réactive. Ils permettent un flux continu de données entre composants, créant une expérience en temps réel.

  • Observable : Source qui émet des données
  • Observer : Consommateur qui s'abonne au flux
  • Opérateurs : Transforment et combinent les flux

Bibliothèques Réactives en Java

Bibliothèque Description Utilisation
RxJava Extensions Réactives pour Java Applications Android, microservices
Project Reactor Framework réactif Spring Spring WebFlux, applications web
Akka Streams Traitement de flux avec Akka Systèmes distribués, traitement de données
Reactive Streams API standard Java 9+ Spécification commune, interopérabilité

Exemple avec RxJava

import io.reactivex.subjects.Subject; import io.reactivex.subjects.ReplaySubject; public class ExempleReactif { public static void main(String[] args) { // Créer une source de données réactive Subject<Integer> source = ReplaySubject.create(); // S'abonner aux événements de la source source.subscribe(result -> System.out.println("Valeur reçue : " + result) ); // Émettre des données source.onNext(1); source.onNext(2); System.out.println("Autre tâche exécutée"); // Continuer à émettre des données source.onNext(3); source.onNext(4); } } // Sortie : // Valeur reçue : 1 // Valeur reçue : 2 // Autre tâche exécutée // Valeur reçue : 3 // Valeur reçue : 4

Opérateurs Réactifs Courants

// map : Transforme les éléments du flux observable.map(x -> x * 2) // filter : Filtre les éléments observable.filter(x -> x > 10) // flatMap : Transforme et aplatit les flux observable.flatMap(x -> Observable.just(x, x * 2)) // merge : Combine plusieurs flux Observable.merge(obs1, obs2) // zip : Combine des flux en paires Observable.zip(obs1, obs2, (a, b) -> a + b) // debounce : Limite la fréquence d'émission observable.debounce(300, TimeUnit.MILLISECONDS)

Comparaison Impératif vs Réactif

Approche Impérative

List<Integer> liste = Arrays.asList(1, 2, 3, 4, 5); for (Integer num : liste) { System.out.println("Nombre : " + num); } // Si on ajoute un élément après, il faut tout refaire liste.add(6); // Ne fonctionne pas avec une liste immuable

Approche Réactive

Subject<Integer> flux = ReplaySubject.create(); // On s'abonne une fois au flux flux.subscribe(num -> System.out.println("Nombre : " + num) ); // On peut continuer à émettre des données flux.onNext(1); flux.onNext(2); flux.onNext(3); // Et ajouter des données plus tard, l'observateur réagira automatiquement flux.onNext(4); flux.onNext(5);
✅ Avantages de la programmation réactive :
  • Asynchrone par nature : Gestion élégante des opérations non-bloquantes
  • Scalabilité : Utilisation optimale des ressources
  • Gestion d'erreurs : Propagation et traitement des erreurs intégrés
  • Composabilité : Facilite la création de flux complexes
  • Temps réel : Parfait pour les applications temps réel
⚠️ Points d'attention :
  • Courbe d'apprentissage plus élevée
  • Débogage plus complexe (flux asynchrones)
  • Consommation mémoire potentiellement plus élevée
  • Nécessite un changement de mentalité

Quand utiliser la programmation réactive ?

  • Applications avec beaucoup d'I/O (API, bases de données)
  • Interfaces utilisateur réactives (mises à jour en temps réel)
  • Systèmes nécessitant haute disponibilité et résilience
  • Traitement de flux de données en temps réel
  • Applications avec beaucoup d'événements asynchrones

🧩 Programmation Logique

📖 Définition :

La programmation logique est un paradigme déclaratif où le programme décrit des relations et des règles logiques plutôt que des séquences d'instructions. Le système déduit automatiquement les solutions.

Principe Fondamental

Programme = Théorie Logique

Exécution = Recherche de Preuve

On ne dit pas au programme "comment" résoudre le problème, mais "quoi" est le problème.

Prolog : Le Langage de Programmation Logique

Historique

  • 1972 : Création de Prolog par Alain Colmerauer et Philippe Roussel à Marseille
  • 1977 : Premier compilateur par D.H. Warren à Édimbourg
  • 1980 : Reconnaissance comme langage IA de référence
  • Aujourd'hui : Standard ISO, utilisé en IA et traitement du langage naturel

Composants de Base

1. Les Faits

Déclarations de vérités simples sur le monde

% Déclarer que Fido est un chien chien(fido). % Déclarer le genre de personnes masculin(gabriel). masculin(raphael). feminin(emma). feminin(alice). % Relations familiales parent(emma, raphael). parent(emma, alice). parent(gabriel, raphael).

2. Les Règles

Définissent des relations conditionnelles

% Tout chien est un mammifère mammifere(X) :- chien(X). % X est le grand-parent de Z si X est parent de Y et Y est parent de Z grand_parent(X, Z) :- parent(X, Y), parent(Y, Z). % X est un ancêtre de Y si X est parent de Y ancetre(X, Y) :- parent(X, Y). % OU si X est parent de quelqu'un qui est ancêtre de Y (récursion) ancetre(X, Y) :- parent(X, Z), ancetre(Z, Y).

3. Les Requêtes

Questions posées au système

% Est-ce que Fido est un chien ? ?- chien(fido). true. % Qui sont les personnes de sexe masculin ? ?- masculin(X). X = gabriel ; X = raphael. % Qui est le parent de Raphael ? ?- parent(X, raphael). X = emma ; X = gabriel. % Emma est-elle grand-parent de quelqu'un ? ?- grand_parent(emma, X).

Variables en Prolog

  • Majuscules : Variables (X, Y, Personne)
  • Minuscules : Atomes/constantes (gabriel, chien)
  • Underscore : Variable anonyme (_)

Mécanisme d'Unification et Backtracking

Unification

Processus de mise en correspondance de termes

% Prolog tente d'unifier les termes % X = 5 unifie X avec 5 % parent(emma, X) cherche tous les X où emma est parent

Backtracking

Exploration systématique des solutions possibles

% Si une tentative échoue, Prolog revient en arrière % et essaie une autre possibilité parent(jean, marie). parent(jean, pierre). parent(marie, sophie). % Requête : ?- parent(jean, X), parent(X, Y). % 1. Essaie X = marie, trouve Y = sophie ✓ % 2. Backtrack, essaie X = pierre, ne trouve pas de Y ✗

Exemple Complet : Arbre Généalogique

% Base de faits homme(gabriel). homme(raphael). homme(leo). femme(emma). femme(alice). femme(jade). parent(gabriel, leo). parent(emma, leo). parent(gabriel, alice). parent(emma, alice). parent(leo, jade). parent(alice, raphael). % Règles dérivées pere(X, Y) :- homme(X), parent(X, Y). mere(X, Y) :- femme(X), parent(X, Y). enfant(X, Y) :- parent(Y, X). frere(X, Y) :- homme(X), parent(P, X), parent(P, Y), X \= Y. soeur(X, Y) :- femme(X), parent(P, X), parent(P, Y), X \= Y. grand_pere(X, Z) :- homme(X), parent(X, Y), parent(Y, Z). grand_mere(X, Z) :- femme(X), parent(X, Y), parent(Y, Z). % Requêtes possibles : % ?- pere(gabriel, X). % Qui sont les enfants de Gabriel ? % ?- grand_mere(emma, X). % Qui sont les petits-enfants d'Emma ? % ?- frere(X, alice). % Qui sont les frères d'Alice ?

Applications de la Programmation Logique

Domaine Utilisation
Intelligence Artificielle Systèmes experts, raisonnement automatique
Traitement du Langage Analyse syntaxique, sémantique, grammaires
Bases de Données Bases de données déductives, requêtes complexes
Planification Ordonnancement, résolution de contraintes
CAO Conception assistée, vérification de circuits

Programmation Logique en Java

Bien que Java ne soit pas un langage de programmation logique, il existe des bibliothèques :

  • tuProlog : Interpréteur Prolog en Java
  • JIProlog : Moteur Prolog pour applications Java
  • Frameworks de règles : Drools, Easy Rules

Exemple avec tuProlog en Java

import alice.tuprolog.*; public class ExempleProlog { public static void main(String[] args) { try { // Créer un moteur Prolog Prolog engine = new Prolog(); // Définir des faits et règles engine.setTheory(new Theory( "parent(jean, marie). " + "parent(jean, pierre). " + "parent(marie, sophie). " + "grand_parent(X, Z) :- parent(X, Y), parent(Y, Z)." )); // Poser une requête SolveInfo info = engine.solve("grand_parent(jean, X)."); if (info.isSuccess()) { System.out.println("Jean est grand-parent de : " + info.getVarValue("X")); } } catch (Exception e) { e.printStackTrace(); } } }
✅ Avantages de la programmation logique :
  • Déclarative : Focus sur "quoi" plutôt que "comment"
  • Concise : Règles simples pour problèmes complexes
  • Puissante : Déduction automatique de solutions
  • Maintenable : Ajout/suppression de règles facile
  • Vérifiable : Preuves mathématiques possibles
⚠️ Limites :
  • Performance parfois limitée sur gros volumes
  • Combinatoire peut exploser rapidement
  • Difficile de contrôler l'ordre d'exécution
  • Courbe d'apprentissage pour penser "déclaratif"

⚖️ Comparaison Complète des Paradigmes

Tableau Comparatif Global

Critère Procédural Orienté Objet Fonctionnel Réactif Logique
Focus Séquence d'instructions Objets et interactions Transformations Flux de données Relations logiques
Organisation Fonctions Classes et objets Fonctions pures Observables/Flux Faits et règles
État Variables mutables Attributs d'objets Immutable Flux de changements Base de faits
Exécution Séquentielle Appels de méthodes Évaluation lazy Asynchrone Déduction
Complexité Simple Moyenne Moyenne Élevée Élevée
Maintenance Difficile (gros projets) Bonne Excellente Bonne Excellente
Performance Très bonne Bonne Bonne Excellente (I/O) Variable

Quand Utiliser Chaque Paradigme ?

⚙️ Procédural

Utilisez quand :
  • Scripts simples et rapides
  • Prototypes
  • Algorithmes séquentiels
  • Petits projets

🎭 Orienté Objet

Utilisez quand :
  • Modélisation du monde réel
  • Gros projets
  • Code réutilisable
  • Maintenance long terme

🔄 Fonctionnel

Utilisez quand :
  • Traitement de collections
  • Transformations de données
  • Parallélisme
  • Code sans effets de bord

⚡ Réactif

Utilisez quand :
  • Applications temps réel
  • Beaucoup d'I/O asynchrones
  • UI réactives
  • Microservices

🧩 Logique

Utilisez quand :
  • Systèmes experts
  • Résolution de contraintes
  • Traitement du langage
  • Raisonnement automatique

Combinaison des Paradigmes

💡 En Pratique :

Les meilleurs programmes modernes combinent souvent plusieurs paradigmes !

  • Java moderne : OOP + Fonctionnel (Streams) + Réactif (Spring WebFlux)
  • Architecture typique : OOP pour la structure, Fonctionnel pour les transformations, Réactif pour l'asynchrone
  • Choisissez le bon outil : Utilisez le paradigme le plus adapté à chaque problème