08 - Encapsulation et Modificateurs d'accès
Apprenez comment protéger et contrôler l'accès aux données de vos classes.
L'encapsulation est un concept consistant à cacher les détails internes d'une classe et de contrôler les accès et modifications de ses données. Cela évite des bugs et rend votre code plus facilement maintenable.
Le problème sans encapsulation
public class Joueur {
public String nom;
public int vie;
public int vieMax;
}
public class Main {
public static void main(String[] args) {
Joueur joueur = new Joueur();
joueur.vie = 100;
joueur.vieMax = 100;
// Oups ! Quelqu’un peut casser les règles
joueur.vie = 500; // Vie au-delà du maximum !
joueur.vie = -50; // Vie négative !
joueur.nom = ""; // Nom vide !
}
}Sans protection, n'importe qui peut définir des valeurs invalides !
Modificateurs d'accès
Java a des mots clés qui contrôlent qui peut accéder aux membres de votre classe :
| Modifieur | Classe | Package | Sous-classe | Global |
|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
| (aucun) | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
Pour l'instant, concentrons-nous sur :
public- Tout le monde peut y accéderprivate- Seulement cette classe peut y accéder
Rendre des propriétés privées
public class Joueur {
private String nom;
private int vie;
private int vieMax;
public Joueur(String nom, int vieMax) {
this.name = nom;
this.health = vieMax;
this.maxHealth = vieMax;
}
}Maintenant vous ne pouvez plus accéder aux propriétés directement :
Joueur joueur = new Joueur("Alice", 100);
joueur.health = 500; // ❌ Erreur ! La vie est privéeGetters et Setters
Pour accéder des propriétés privées, créez des méthodes getter et setter :
public class Joueur {
private String nom;
private int vie;
private int vieMax;
public Joueur(String nom, int vieMax) {
this.nom = nom;
this.vie = vieMax;
this.vieMax = vieMax;
}
// Getter – retourne la valeur
public int getVie() {
return vie;
}
// Setter – définit la valeur, avec validation
public void setVie(int vie) {
if (vie < 0) {
this.vie = 0;
} else if (vie > vieMax) {
this.vie = vieMax;
} else {
this.vie = vie;
}
}
public String getNom() {
return nom;
}
public int getVieMax() {
return vieMax;
}
}Maintenant vous pouvez interagir de manière sécurisée avec l'objet :
Joueur joueur = new Joueur("Alice", 100);
joueur.setVie(150); // Automatiquement limitée à 100
System.out.println(joueur.getVie()); // 100
joueur.setVie(-20); // Automatiquement définie à 0
System.out.println(joueur.getVie()); // 0Suivez les conventions de nommage Java :
- Getter :
get+ nom de propriété (avec une majuscule) - Setter:
set+ nom de propriété (avec une majuscule) - Boolean:
is+ nom de propriété (avec une majuscule)
private int vie;
public int getVie() { }
public void setVie(int vie) { }
private boolean enVie;
public boolean isEnVie() { }
public void setEnVie(boolean enVie) { }
private String nom;
public String getNom() { }
public void setNom(String nom) { }Bénéfices de l'encapsulation
1. Validation
public class Objet {
private int durabilite;
private int durabiliteMax;
public void setDurabilite(int durabilite) {
if (durabilite < 0) {
this.durabilite = 0;
} else if (durabilite > durabiliteMax) {
this.durabilite = durabiliteMax;
} else {
this.durabilite = durabilite;
}
}
public boolean isCasse() {
return durabilite <= 0;
}
}2. Propriétés en lecture seule
Parfois vous ne voulez pas de Setter :
public class Monstre {
private String id; // Should never change
private int vie;
public Monstre(String id, int vie) {
this.id = id;
this.vie = vie;
}
// Getter only - no setter!
public String getId() {
return id;
}
public int getVie() {
return vie;
}
public void setVie(int vie) {
this.vie = vie;
}
}3. Propriétés calculées
Les Getters ne sont pas obligés de retourner une propriété directement :
public class Joueur {
private int vie;
private int vieMax;
public int getVie() {
return vie;
}
// Computed property
public double getPourcentageDeVie() {
return (vie * 100.0) / vieMax;
}
// Computed property
public boolean isVieBasse() {
return getPourcentageDeVie() < 25;
}
}Exemples pratiques
Objet avec durabilité
public class Tool {
private String name;
private int durability;
private int maxDurability;
private boolean broken;
public Tool(String name, int maxDurability) {
this.name = name;
this.durability = maxDurability;
this.maxDurability = maxDurability;
this.broken = false;
}
public void use() {
if (broken) {
System.out.println(name + " is broken!");
return;
}
durability--;
System.out.println(name + " used. Durability: " + durability);
if (durability <= 0) {
broken = true;
System.out.println(name + " broke!");
}
}
public void repair() {
durability = maxDurability;
broken = false;
System.out.println(name + " repaired!");
}
// Getters
public String getName() {
return name;
}
public int getDurability() {
return durability;
}
public boolean isBroken() {
return broken;
}
public double getDurabilityPercentage() {
return (durability * 100.0) / maxDurability;
}
}Exemple compte bancaire
public class PlayerWallet {
private int gold;
private int silver;
public PlayerWallet() {
this.gold = 0;
this.silver = 0;
}
public void addGold(int amount) {
if (amount > 0) {
gold += amount;
System.out.println("Added " + amount + " gold");
}
}
public boolean spendGold(int amount) {
if (amount > gold) {
System.out.println("Not enough gold!");
return false;
}
gold -= amount;
System.out.println("Spent " + amount + " gold");
return true;
}
public int getGold() {
return gold;
}
public int getTotalValue() {
// 1 gold = 100 silver
return gold * 100 + silver;
}
}Système de bloc protégé
public class ProtectedBlock {
private int x, y, z;
private String type;
private String owner;
private boolean locked;
public ProtectedBlock(int x, int y, int z, String type, String owner) {
this.x = x;
this.y = y;
this.z = z;
this.type = type;
this.owner = owner;
this.locked = true;
}
public boolean canBreak(String playerName) {
if (!locked) {
return true;
}
return playerName.equals(owner);
}
public void unlock(String playerName) {
if (playerName.equals(owner)) {
locked = false;
System.out.println("Block unlocked");
} else {
System.out.println("You don't own this block!");
}
}
// Getters only - position and owner shouldn't change
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public String getOwner() {
return owner;
}
public boolean isLocked() {
return locked;
}
}Quand utiliser Privé vs Public
Mettez tout en privé par défaut ! Ne mettez des choses en public que si elles ont besoin d'être accédées depuis l'extérieur
Privé :
- Les données internes (vie, position, inventaire)
- Les méthodes d'aides qui ne sont utilisées que dans la classe
- Tout ce qui nécessite une validation
Public :
- Les méthodes qui définissent le comportement de la classe
- Les constructeurs
- Les méthodes que les autres classes doivent appeler
public class Example {
// Privé - données internes
private int internalCounter;
private String secretKey;
// Public - fait partie de l'interface
public void doSomething() {
// Utilise une méthode d'aide privée
validateData();
}
// Privé - aide interne
private void validateData() {
// ...
}
}Le mot clé final
final signifie que la variable ne peut pas être modifiée après avoir été définie :
public class Player {
private final String id; // Ne peut pas changer après création
private String name; // Peut changer
private int health; // Peut changer
public Player(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
// Pas de setId() - la variable est final !
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}Le mot clé static
Membres statiques
Une classe peut définir deux types de membres :
-
Membres d'instance — possédés par chaque objet (chacun à sa propre copie).
-
Membres statiques — possédés par la classe (une seule copie partagée pour tout le type).
Résumé simplement : les membres d'instance appartiennent aux objets ; les membres statiques appartiennent à la classe elle-même et sont partagés par tous les objets du même type.
Déclaration
/* (access modifier) */ static ... memberName; Exemple
class Data {
public int x; // Membre d'instance
public static int y = 1000; // Membre statique
// Membre d'instance :
// peut accéder aux membres statiques et non-statiques
public void foo() {
x = 100; // OK - pareil que this.x = 100;
y = 100; // OK - pareil que Data.y = 200;
}
// Membre statique :
// ne peut pas accéder aux variables non-statiques
public static void bar() {
x = 100; // Erreur : la variable non-statique x ne peut pas être référencée dans un contexte statique
y = 100; // OK
}
}Accéder aux membres statiques
Data data = new Data();
data.x = 1000; // OK
data.y = 1000; // Passable - pas vraiment recommandé ; il est mieux d'utiliser Data.y
Data.y = 1000; // OK - bonne pratique
Data.x = 1000; // Erreur : impossible d'accéder à une variable d'instance dans un contexte statiqueChamps statiques
Un champ statique représente une donnée membre possédée par le type classe plutôt que par l'objet. Les champs statiques sont aussi stockés dans un endroit spécifique de la mémoire qui est partagé entre toutes les instances d'objet qui sont créées.
Il est déclaré comme suit :
/* (access modifier) (optional) */ static /* final/volatile (optional) */ fieldName;Prenons le même exemple de classe Data et ajoutons ce constructeur :
public Data() {
y++; // souvenez vous que c'est la même chose que Data.y++;
}// Chaque instance de Data aura une copie privée du membre d'instance x
// Cependant, il y aura un pointage au même endroit dans la mémoire pour le membre y
Data d1 = new Data(); // y = 1001
d1.x = 5;
Data d2 = new Data(); // y = 1002
d2.x = 25;
Data d3 = new Data(); // y = 1003
// ... et ainsi de suiteMéthodes statiques
Les méthodes statiques représentent essentiellement une fonction membre d'un type classe
Souvenez-vous de la fonction (méthode instanciée) foo et (méthode statique) bar dans la classe Data
Il est possible d'accéder à ces méthodes par :
Data d1 = new Data();
d1.foo(); // Méthode instanciée : accessible seulement par un objet
Data.bar(); // Méthode statique : accessible sans objetInitialiseur statique
Utilisez un bloc d'initialisation statique pour exécuter la logique d'initialisation lorsque la classe est chargée pour la première fois
class OtherData {
private static int a = 12;
private static int b;
private static String msg;
static {
msg = "Initialisation..."
System.out.println(msg);
b = 4;
// ... initialisation complexe qui ne peut pas être faite en une seule expression
}
}Exercices pratiques
-
Créez une classe
BankAccount(compte bancaire) :- Propriétés privées :
accountNumber(numéro de compte),balance(solde) - Le constructeur définit un numéro de compte
- Méthodes :
deposit()pour déposer de l'argent,withdraw()pour en retirer, etgetBalance()pour voir le solde - Validation : on ne peut pas retirer plus que ce qu'il y a sur le compte
- Le numéro de compte doit être en lecture seule
- Propriétés privées :
-
Créez une classe
Door(porte) :- Propriétés privées :
isLocked(état de verrouillage de la porte),keyCode(code de la porte) - Le constructeur pour définir le code de la porte
- Méthodes :
lock()pour verrouiller,unlock(String code)pour déverrouiller,isLocked()pour voir si la porte est verrouillée unlock(String code)ne marche que si le code est correct- Le code de la porte doit être privé (ne l'exposez pas !)
- Propriétés privées :
-
Créez une classe
PlayerStats(statistiques du joueur) :- Propriétés privées :
strength(force),defence(défense),speed(vitesse) - Le constructeur définit toutes les statistiques
- Il faut un Getter pour chaque statistique
- Méthodes :
getPowerLevel()qui renvoie force + défense + vitesse - Les statistiques ne doivent pas être négatives ni dépasser 100
- Propriétés privées :
-
Refactorisez une classe : prenez une des classes des leçons précédentes et ajoutez-y une encapsulation appropriée :
- Mettez toutes les propriétés en privé
- Ajoutez les Getter et Setter appropriés
- Ajoutez de la validation là où c'est nécessaire