MFormations
Modern Java Engineering

Chapitre 2

Chapitre 02 : Java et les Objets

Chapitre 02 : Java et les Objets

Cours : Java et les Objets

1. OOP : Encapsulation, Inheritance, Polymorphism

1.1 Les 4 Piliers de la POO

Java est un langage orienté objet pur (sauf les primitifs). Les 4 piliers sont :

  1. Encapsulation : Cacher l'état interne, exposer via des méthodes
  2. Héritage : Réutiliser et étendre des classes existantes
  3. Polymorphisme : Même interface, comportements différents
  4. Abstraction : Cacher la complexité, exposer l'essentiel

1.2 Encapsulation

public class CompteBancaire {
    // Attributs privés (encapsulation)
    private String titulaire;
    private double solde;
    
    // Constructeur
    public CompteBancaire(String titulaire, double soldeInitial) {
        this.titulaire = titulaire;
        this.solde = soldeInitial;
    }
    
    // Getters
    public String getTitulaire() { return titulaire; }
    public double getSolde() { return solde; }
    
    // Méthodes métier (pas de setter direct pour solde)
    public void deposer(double montant) {
        if (montant <= 0) {
            throw new IllegalArgumentException("Montant positif requis");
        }
        solde += montant;
    }
    
    public void retirer(double montant) {
        if (montant <= 0) throw new IllegalArgumentException("Montant positif requis");
        if (montant > solde) throw new IllegalStateException("Solde insuffisant");
        solde -= montant;
    }
}

Règles d'encapsulation :

  • Champs en private
  • Getters/Setters si nécessaire (préférer des méthodes métier)
  • Visibilité la plus restrictive possible
  • public pour l'API, private pour l'implémentation

1.3 Héritage

// Classe mère
public class Animal {
    protected String nom;
    
    public Animal(String nom) {
        this.nom = nom;
    }
    
    public void manger() {
        System.out.println(nom + " mange");
    }
    
    public void dormir() {
        System.out.println(nom + " dort");
    }
}

// Classe fille
public class Chien extends Animal {
    private String race;
    
    public Chien(String nom, String race) {
        super(nom);  // Appel au constructeur parent
        this.race = race;
    }
    
    // Surcharge (override) de méthode
    @Override
    public void manger() {
        System.out.println(nom + " mange des croquettes");
    }
    
    // Méthode spécifique
    public void aboyer() {
        System.out.println(nom + " aboie !");
    }
}

Règles de l'héritage :

  • extends pour hériter d'une classe (mono-héritage)
  • super pour accéder au parent
  • @Override pour surcharger (toujours l'utiliser)
  • Constructeur parent appelé avec super()
  • protected pour les membres accessibles aux sous-classes
  • final sur une classe empêche l'héritage

1.4 Polymorphisme

// Polymorphisme : même interface, comportement différent
Animal monChien = new Chien("Rex", "Berger");
Animal monChat = new Chat("Minou", "Siamois");

monChien.manger();  // "Rex mange des croquettes"
monChat.manger();   // "Minou mange du poisson"

// Instanceof (Java 16+ : pattern matching)
if (monChien instanceof Chien chien) {
    chien.aboyer();
}

// Appel dynamique (late binding)
List<Animal> animaux = List.of(monChien, monChat);
animaux.forEach(Animal::manger);

Types de polymorphisme :

  1. Surcharge (overload) : Même nom, paramètres différents (compile-time)
  2. Surchage (override) : Redéfinition dans sous-classe (runtime)
  3. Paramétrique : Generics

1.5 Composition vs Héritage

Préférer la composition à l'héritage :

// ❌ Héritage inapproprié
public class Voiture extends Moteur { }

// ✅ Composition
public class Voiture {
    private Moteur moteur;
    private List<Roue> roues;
    
    public Voiture() {
        this.moteur = new Moteur();
        this.roues = List.of(new Roue(), new Roue(), new Roue(), new Roue());
    }
    
    public void demarrer() {
        moteur.allumer();
    }
}

2. abstract vs interface (Java 8+)

2.1 Classe Abstraite

public abstract class Forme {
    protected String couleur;
    
    public Forme(String couleur) {
        this.couleur = couleur;
    }
    
    // Méthode abstraite (sans corps)
    public abstract double calculerSurface();
    public abstract double calculerPerimetre();
    
    // Méthode concrète
    public String getCouleur() { return couleur; }
    
    // Méthode statique
    public static Forme fromCouleur(String couleur) { ... }
}

2.2 Interface (Java 8+)

public interface Dessinable {
    // Constante (implicitement public static final)
    int VERSION = 1;
    
    // Méthode abstraite
    void dessiner();
    
    // Java 8 : default method
    default void afficherInfos() {
        System.out.println("Version " + VERSION);
    }
    
    // Java 8 : static method
    static Dessinable vide() {
        return () -> System.out.println("Dessin vide");
    }
    
    // Java 9 : private method
    private void log(String msg) {
        System.out.println("[Dessin] " + msg);
    }
}

2.3 abstract vs interface : Quand utiliser ?

CritèreClasse abstraiteInterface
État (champs)Oui (tous types)Non (constantes uniquement)
ConstructeursOuiNon
Héritage multipleNon (mono-héritage)Oui (multi-implémentation)
Méthodes concrètesOuidefault/static
VisibilitéToutespublic uniquement
Relation"est un""peut faire"
// Bon usage d'une classe abstraite
public abstract class Vehicule {
    protected String immatriculation;
    public abstract void demarrer();
}

// Bon usage d'interface
public interface Roulant {
    void rouler();
}

public interface Volant {
    void voler();
}

// Une voiture volante peut implémenter les deux interfaces
public class VoitureVolante extends Vehicule implements Roulant, Volant {
    @Override public void demarrer() { ... }
    @Override public void rouler() { ... }
    @Override public void voler() { ... }
}

3. equals/hashCode

3.1 Le Contrat

Le contrat entre equals() et hashCode() est défini dans la documentation de Object :

  1. Si deux objets sont égaux selon equals(), ils doivent avoir le même hashCode()
  2. Si deux objets ont le même hashCode(), ils peuvent être égaux ou non
  3. hashCode() doit être cohérent (même valeur si l'objet n'est pas modifié)

3.2 Implémentation Correcte

public class Personne {
    private final String nom;
    private final String prenom;
    private final int age;
    
    // equals()
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Personne personne = (Personne) o;
        return age == personne.age 
            && Objects.equals(nom, personne.nom) 
            && Objects.equals(prenom, personne.prenom);
    }
    
    // hashCode()
    @Override
    public int hashCode() {
        return Objects.hash(nom, prenom, age);
    }
    
    // toString()
    @Override
    public String toString() {
        return "Personne{nom='" + nom + "', prenom='" + prenom + "', age=" + age + "}";
    }
}

3.3 Avec Lombok

import lombok.EqualsAndHashCode;
import lombok.ToString;

@EqualsAndHashCode
@ToString
public class PersonneLombok {
    private String nom;
    private String prenom;
    private int age;
}

3.4 Avec Record (Java 16+)

// equals, hashCode, toString automatiques
public record PersonneRecord(String nom, String prenom, int age) {}

3.5 Erreurs Fréquentes

// ❌ equals sans hashCode
public class Mauvais {
    private int id;
    
    @Override
    public boolean equals(Object o) {
        // hashCode() non défini → HashMap ne fonctionne pas
    }
}

// ❌ Utilisation de classe mutable dans HashSet/HashMap
Set<List<String>> set = new HashSet<>();
List<String> list = new ArrayList<>(List.of("a"));
set.add(list);
list.add("b");  // hashCode change, liste perdue dans le set

// ❌ Symétrie brisée
public class SubClass extends SuperClass {
    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        // Ne compare pas avec instance de SuperClass
    }
}

4. Records (Java 16)

4.1 Définition

Les records sont des classes transparentes pour les données. Java génère automatiquement :

  • Constructeur canonical
  • Getters (nom(), prenom(), etc.)
  • equals(), hashCode(), toString()
public record Point(int x, int y) {}

// Utilisation
var p = new Point(3, 4);
System.out.println(p.x());      // 3 (getter, pas getX())
System.out.println(p);          // Point[x=3, y=4]
System.out.println(p.equals(new Point(3, 4))); // true

4.2 Constructeur Compact

public record Personne(String nom, String email) {
    // Constructeur compact (validation)
    public Personne {
        Objects.requireNonNull(nom);
        Objects.requireNonNull(email);
        if (!email.contains("@")) {
            throw new IllegalArgumentException("Email invalide");
        }
    }
    
    // Constructeur additionnel
    public Personne(String nom) {
        this(nom, nom.toLowerCase() + "@example.com");
    }
}

4.3 Méthodes et Champs Statiques

public record Rectangle(double largeur, double hauteur) {
    // Champs statiques autorisés
    public static final Rectangle CARRE_UNITE = new Rectangle(1, 1);
    
    // Méthodes d'instance
    public double surface() { return largeur * hauteur; }
    public double perimetre() { return 2 * (largeur + hauteur); }
    
    // Méthodes statiques
    public static Rectangle carre(double cote) {
        return new Rectangle(cote, cote);
    }
}

4.4 Records et Interfaces

public interface Identifiable {
    long id();
}

public record User(long id, String name) implements Identifiable {}

4.5 Records Locaux (Java 16+)

public List<String> process(List<Personne> personnes) {
    record PersonneAgee(String nom, int age) {}
    
    return personnes.stream()
        .filter(p -> p.age() >= 65)
        .map(p -> new PersonneAgee(p.nom(), p.age()))
        .map(PersonneAgee::toString)
        .toList();
}

5. Sealed Classes

5.1 Principe

Les classes scellées (sealed) restreignent les sous-classes autorisées. Introduites en Java 15 (prévisualisation), définitives en Java 17.

public sealed class Figure permits Cercle, Rectangle, Triangle { }

// Sous-classes autorisées
public final class Cercle extends Figure { }
public final class Rectangle extends Figure { }
public final class Triangle extends Figure { }

// Ou sealed en chaîne
public sealed class FormeComplexe extends Figure permits Groupe, Composant { }
public final class Groupe extends FormeComplexe { }
public final class Composant extends FormeComplexe { }

5.2 Avec Pattern Matching

public double calculerAire(Figure figure) {
    return switch (figure) {
        case Cercle c -> Math.PI * c.rayon() * c.rayon();
        case Rectangle r -> r.largeur() * r.hauteur();
        case Triangle t -> 0.5 * t.base() * t.hauteur();
        // Pas de default nécessaire : toutes les sous-classes sont couvertes
    };
}

5.3 Sealed Records

public sealed interface Expression permits Addition, Soustraction, Nombre { }

public record Addition(Expression gauche, Expression droite) implements Expression { }
public record Soustraction(Expression gauche, Expression droite) implements Expression { }
public record Nombre(double valeur) implements Expression { }

6. Enums

6.1 Définition et Utilisation

public enum JourSemaine {
    LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI, DIMANCHE;
    
    public boolean estWeekend() {
        return this == SAMEDI || this == DIMANCHE;
    }
}

6.2 Enum avec Attributs et Méthodes

public enum StatutCommande {
    NOUVEAU("Nouvelle commande", 0),
    CONFIRME("Confirmée", 1),
    EN_PREPARATION("En préparation", 2),
    EXPEDIE("Expédiée", 3),
    LIVRE("Livrée", 4),
    ANNULE("Annulée", -1);
    
    private final String description;
    private final int code;
    
    StatutCommande(String description, int code) {
        this.description = description;
        this.code = code;
    }
    
    public String getDescription() { return description; }
    public int getCode() { return code; }
    
    public boolean estFinal() {
        return this == LIVRE || this == ANNULE;
    }
    
    public boolean peutTransitionnerVers(StatutCommande next) {
        return switch (this) {
            case NOUVEAU -> next == CONFIRME || next == ANNULE;
            case CONFIRME -> next == EN_PREPARATION || next == ANNULE;
            case EN_PREPARATION -> next == EXPEDIE || next == ANNULE;
            case EXPEDIE -> next == LIVRE;
            case LIVRE, ANNULE -> false;
        };
    }
}

6.3 Enum avec Méthodes Abstraites

public enum Operation {
    PLUS { @Override public double apply(double a, double b) { return a + b; } },
    MOINS { @Override public double apply(double a, double b) { return a - b; } },
    FOIS { @Override public double apply(double a, double b) { return a * b; } },
    DIVISE { @Override public double apply(double a, double b) { return a / b; } };
    
    public abstract double apply(double a, double b);
}

7. Generics et PECS

7.1 Classes Génériques

public class Boite<T> {
    private T contenu;
    
    public void mettre(T contenu) { this.contenu = contenu; }
    public T prendre() { return contenu; }
}

// Utilisation
Boite<String> boite = new Boite<>();
boite.mettre("Hello");
String valeur = boite.prendre();

7.2 Méthodes Génériques

public class Utils {
    public static <T> T premierOuNull(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }
    
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }
}

// Inférence de type
String max = Utils.max("abc", "xyz");  // T = String

7.3 Wildcards et PECS

PECS = Producer Extends, Consumer Super

// Producer Extends : lit des valeurs
public void afficherNoms(List<? extends Animal> animaux) {
    for (Animal a : animaux) {
        System.out.println(a.nom);
    }
    // animaux.add(new Chien()); // ❌ Ne peut pas ajouter
}

// Consumer Super : écrit des valeurs
public void ajouterChiens(List<? super Chien> liste) {
    liste.add(new Chien("Rex"));
    liste.add(new Chien("Max"));
    // Chien c = liste.get(0); // ❌ Ne peut pas lire (type erroné)
}

// Usage
List<Animal> zoo = new ArrayList<>();
ajouterChiens(zoo);  // List<Animal> est List<? super Chien>

List<Chien> chenil = new ArrayList<>();
ajouterChiens(chenil); // List<Chien> aussi

List<Object> objets = new ArrayList<>();
ajouterChiens(objets); // List<Object> aussi

Règle mnémotechnique PECS :

  • Si on produit (lit) des éléments → ? extends T
  • Si on consomme (écrit) des éléments → ? super T
  • Si on lit ET écrit → pas de wildcard
// PECS en action avec Collections.copy()
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));  // lit de src (extends), écrit dans dest (super)
    }
}

7.4 Type Erasure

Les generics sont implémentés via l'effacement de type (type erasure) :

// Compile-time
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

// Runtime : List<List> (erased)
// string.get(0) → cast implicite vers String

Bridge methods :

public class MyList implements Comparable<MyList> {
    @Override
    public int compareTo(MyList o) { ... }
    // Bridge method généré : compareTo(Object o) { compareTo((MyList) o); }
}

8. Optional

8.1 Création

// Optional vide
Optional<String> vide = Optional.empty();

// Optional avec valeur non-null
Optional<String> present = Optional.of("valeur");

// Optional nullable (null → empty)
Optional<String> nullable = Optional.ofNullable(getValue());

8.2 Utilisation (Never null)

// ❌ Mauvais : get() sans vérification
if (opt.isPresent()) {
    String val = opt.get();
}

// ✅ ifPresent
opt.ifPresent(System.out::println);

// ✅ ifPresentOrElse (Java 9+)
opt.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("Valeur absente")
);

// ✅ orElse : valeur par défaut
String result = opt.orElse("défaut");

// ✅ orElseGet : supplier lazy
String result = opt.orElseGet(() -> generateDefault());

// ✅ orElseThrow : exception personnalisée
String result = opt.orElseThrow(() -> new NoSuchElementException("Value missing"));

// ✅ or (Java 9+) : chaîner les Optional
Optional<String> result = opt.or(() -> fallback());

8.3 Transformation

// map : transforme si présent
Optional<Integer> longueur = opt.map(String::length);

// flatMap : évite Optional<Optional>
Optional<String> result = opt.flatMap(this::findInDatabase);

// filter : filtre si condition
Optional<String> longStr = opt.filter(s -> s.length() > 5);

// stream (Java 9+) : convertit en Stream
Stream<String> stream = opt.stream();

8.4 Optional dans les APIs

// ✅ Bon : retourner Optional
public Optional<Utilisateur> findById(Long id) {
    return Optional.ofNullable(database.get(id));
}

// ❌ Mauvais : Optional comme paramètre
public void setInfo(Optional<String> name) { ... }

// ❌ Mauvais : Optional dans les champs
public class Personne {
    private Optional<String> email;  // Jamais !
    private String email;             // null si absent
}

// ✅ Optional pour les méthodes getter
public Optional<String> getEmail() {
    return Optional.ofNullable(email);
}

8.5 Bonnes Pratiques

// ✅ Chaînage fluide
String result = findUser(id)
    .flatMap(this::findProfile)
    .map(Profile::getEmail)
    .orElse("email@default.com");

// ✅ Optional dans les Streams
List<String> emails = users.stream()
    .map(User::getEmail)
    .flatMap(Optional::stream)  // Java 9+
    .toList();

// ❌ Ne pas utiliser Optional pour :
// - Les paramètres de méthode
// - Les champs de classe
// - Les collections (retourner List vide plutôt)
// - Les primitifs (utiliser OptionalInt, OptionalLong, OptionalDouble)

// ✅ Primitifs
OptionalInt optionalInt = OptionalInt.of(42);
optionalInt.ifPresent(System.out::println);

9. Résumé

ConceptPoint clé
EncapsulationChamps privés, méthodes publiques
HéritageMono-héritage, préférer composition
PolymorphismeLate binding, @Override, instanceof
abstract"est un", état, constructeurs
interface"peut faire", multi-implémentation, default methods
equals/hashCodeContrat : si equals, hashCode identique
RecordDonnées immuables, constructeur compact
Sealed ClassesSous-classes limitées, exhaustivité
EnumConstantes typées, attributs, méthodes
GenericsType safety, PECS, type erasure
OptionalJamais null, API fluide, pas dans les champs