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 :
- Encapsulation : Cacher l'état interne, exposer via des méthodes
- Héritage : Réutiliser et étendre des classes existantes
- Polymorphisme : Même interface, comportements différents
- 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
publicpour l'API,privatepour 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 :
extendspour hériter d'une classe (mono-héritage)superpour accéder au parent@Overridepour surcharger (toujours l'utiliser)- Constructeur parent appelé avec
super() protectedpour les membres accessibles aux sous-classesfinalsur 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 :
- Surcharge (overload) : Même nom, paramètres différents (compile-time)
- Surchage (override) : Redéfinition dans sous-classe (runtime)
- 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ère | Classe abstraite | Interface |
|---|---|---|
| État (champs) | Oui (tous types) | Non (constantes uniquement) |
| Constructeurs | Oui | Non |
| Héritage multiple | Non (mono-héritage) | Oui (multi-implémentation) |
| Méthodes concrètes | Oui | default/static |
| Visibilité | Toutes | public 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 :
- Si deux objets sont égaux selon
equals(), ils doivent avoir le mêmehashCode() - Si deux objets ont le même
hashCode(), ils peuvent être égaux ou non 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é
| Concept | Point clé |
|---|---|
| Encapsulation | Champs privés, méthodes publiques |
| Héritage | Mono-héritage, préférer composition |
| Polymorphisme | Late binding, @Override, instanceof |
| abstract | "est un", état, constructeurs |
| interface | "peut faire", multi-implémentation, default methods |
| equals/hashCode | Contrat : si equals, hashCode identique |
| Record | Données immuables, constructeur compact |
| Sealed Classes | Sous-classes limitées, exhaustivité |
| Enum | Constantes typées, attributs, méthodes |
| Generics | Type safety, PECS, type erasure |
| Optional | Jamais null, API fluide, pas dans les champs |