Différence entre public, protected, package-private et private en Java

Java possède quatre niveaux de visibilité pour les classes, méthodes et champs. Bien les comprendre est fondamental pour écrire du code encapsulé, testable et maintenable.

Les quatre modificateurs

ModificateurMême classeMême packageSous-classePartout
public
protected
package-private (pas de mot-clé)
private

public — visible partout

Accessible depuis n'importe quelle classe, n'importe quel package. C'est la visibilité des APIs publiques : System.out, List, String.valueOf.

package com.example.api;

public class Calculator {
    public int add(int a, int b) { return a + b; }
}

Règle pratique : seul ce qui fait partie du contrat public de votre bibliothèque doit être public. Tout le reste devrait être plus restreint.

protected — pour les héritiers

Accessible depuis la classe, toute classe du même package, et toute sous-classe — même dans un autre package.

package com.example.shapes;

public class Shape {
    protected double area; // accessible aux sous-classes

    protected double computeArea() { return 0; } // override possible
}
package com.other.shapes;

import com.example.shapes.Shape;

public class Circle extends Shape {
    public Circle(double r) {
        this.area = Math.PI * r * r; // ✅ accès à area hérité
    }

    @Override
    protected double computeArea() { return area; } // ✅ override
}

Usage typique : méthodes d'un framework que les utilisateurs peuvent redéfinir mais pas appeler directement depuis l'extérieur.

package-private — le défaut silencieux

Si vous n'écrivez aucun modificateur, Java applique la visibilité package-private : la classe ou le membre est accessible uniquement depuis le même package.

package com.example.service;

class InternalHelper {   // pas de modificateur = package-private
    static void validate() { ... }
}

public class OrderService {
    public void process() {
        InternalHelper.validate(); // ✅ même package
    }
}

Parfait pour cacher des détails d'implémentation sans pour autant enterrer tout le code dans une seule classe. La visibilité package-private est souvent sous-utilisée — privilégiez-la systématiquement aux classes public qui n'ont pas vraiment besoin de l'être.

private — visible dans la classe uniquement

Le plus restrictif : seuls la classe elle-même et ses classes imbriquées y ont accès.

public class Wallet {
    private double solde;

    private boolean montantValide(double m) { return m > 0; }

    public void crediter(double montant) {
        if (montantValide(montant)) solde += montant;
    }
}

Règle : tout champ qui n'est pas destiné à être modifié de l'extérieur doit être private. C'est la base de l'encapsulation.

Particularités pour les classes

Au niveau top-level, seuls public et package-private sont permis — une classe top-level ne peut être ni private ni protected.

// ✅ Légal
public class A { }
class B { } // package-private

// ❌ Erreur de compilation
private class C { }

Les classes internes, elles, peuvent porter n'importe quel modificateur :

public class Outer {
    private static class InnerHelper { } // classe interne privée
    protected class Tool { }
    static class Nested { }
}

Modules Java (depuis Java 9)

Les modules introduisent une cinquième couche au-dessus des packages. Même un type public n'est pas accessible depuis un autre module si le package qui le contient n'est pas exported dans module-info.java :

module com.example.shapes {
    exports com.example.shapes.api;     // accessible
    // com.example.shapes.internal reste interne
}

Exemple d'architecture bien encapsulée

package com.example.order;

public class OrderService {
    private final OrderRepository repo;
    private final Validator validator; // classe interne au package

    public OrderService(OrderRepository repo) {
        this.repo = repo;
        this.validator = new Validator();
    }

    public Order create(Cart cart) {
        validator.validate(cart); // détail d'implémentation
        Order order = new Order(cart);
        repo.save(order);
        return order;
    }
}

// Package-private : pas exposée hors du package
class Validator {
    void validate(Cart cart) { /* ... */ }
}

Le client externe voit OrderService et create() uniquement. Tout le reste (Validator, détails internes) est caché.

Bonnes pratiques

  1. Principe du moindre privilège : choisir la visibilité la plus restrictive qui fonctionne. Si private suffit, pas besoin de public.
  2. Pas de public sur les champs non finaux — sauf constantes (public static final).
  3. Préférer package-private aux classes publiques pour tout ce qui n'est pas API publique.
  4. Éviter protected sur les champs — il casse l'encapsulation entre sous-classes sans vraiment la restreindre.
  5. Pour les tests, garder la classe testée public si nécessaire, mais privilégier des méthodes package-private pour accéder à l'état interne depuis un test dans le même package.

Un code Java bien encapsulé commence par une visibilité choisie avec soin, pas par le réflexe d'écrire public partout.