Modding d'Hytale
Bases de Java

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 :

ModifieurClassePackageSous-classeGlobal
public
protected
(aucun)
private

Pour l'instant, concentrons-nous sur :

  • public - Tout le monde peut y accéder
  • private - 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ée

Getters 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());  // 0
Nommage des Getter et Setter

Suivez 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

Règles générales

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 statique

Champs 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 suite

Mé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 objet

Initialiseur 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

  1. 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, et getBalance() 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
  2. 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 !)
  3. 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
  4. 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