Classes imbriquées en Java
Prérequis
Maîtriser les classes, les objets, les modificateurs d'accès et l'héritage. Comprendre les concepts de static et d'instanciation. Connaître les interfaces et les classes abstraites.
Objectifs
Comprendre les différents types de classes imbriquées en Java. Savoir quand et comment utiliser les classes statiques imbriquées, les classes internes, les classes locales et les classes anonymes. Maîtriser les règles d'accès et les cas d'usage spécifiques.
1. Qu'est-ce qu'une classe imbriquée ?
Une classe imbriquée est une classe définie à l'intérieur d'une autre classe. La classe conteneur est appelée classe externe (top-level class). L'imbrication permet de regrouper logiquement des classes qui ne sont utilisées que par la classe externe, améliorant ainsi l'encapsulation et la lisibilité du code.
Chaque classe que vous avez étudiée jusqu'à présent a été stockée dans son propre fichier, avec un nom correspondant à celui de la classe. En Java, vous pouvez créer une classe dans une autre classe et les stocker ensemble. Ces classes sont des classes imbriquées.
Il existe quatre types de classes imbriquées :
- Classes imbriquées statiques — associées à la classe externe, accessibles sans instance
- Classes membres non statiques (classes internes) — nécessitent une instance de la classe externe
- Classes locales — définies dans un bloc (méthode, constructeur, boucle)
- Classes anonymes — classes locales sans nom, instanciées une seule fois
La raison la plus courante est que la classe interne n'est utilisée que par la classe de niveau supérieur. C'est une "classe d'assistance" qui n'a pas d'utilité ailleurs. Regrouper ces classes facilite la compréhension de leur connexion et la maintenance du code.
2. Classes imbriquées statiques (Static Nested Classes)
Une classe imbriquée statique est associée à sa classe externe comme le sont les méthodes et variables statiques. Elle ne peut pas accéder directement aux variables d'instance ni aux méthodes non statiques de la classe englobante — elle ne peut les utiliser que via une référence d'objet.
Les classes imbriquées statiques sont accessibles en utilisant le nom de la classe englobante :
ClasseExterne.ClasseStatique obj = new ClasseExterne.ClasseStatique();
Exemple 1 — Classe statique imbriquée
public class Test {
// Classe imbriquée statique
static class ClasseStatique {
private String message = "Message de la classe statique";
public void afficher() {
System.out.println("La classe imbriquée statique");
System.out.println("Message : " + message);
}
// Une classe statique peut avoir des méthodes statiques
public static void methodeStatique() {
System.out.println("Méthode statique dans classe imbriquée");
}
}
public static void main(String[] args) {
// Instanciation sans instance de la classe externe
Test.ClasseStatique obj = new Test.ClasseStatique();
obj.afficher();
// Accès à la méthode statique
Test.ClasseStatique.methodeStatique();
}
}
La classe imbriquée statique Message : Message de la classe statique Méthode statique dans classe imbriquée
Lorsque vous compilez l'exemple ci-dessus, deux fichiers sont produits :
Test.class— la classe externeTest$ClasseStatique.class— la classe imbriquée (le symbole$sépare les noms)
3. Classes internes (Inner Classes / Non-static Nested Classes)
Une classe interne est une classe membre non statique. Elle est associée à une instance spécifique de la classe externe. Elle a accès à toutes les variables et méthodes (même privées) de la classe externe.
Les classes internes constituent un mécanisme de sécurité en Java. Une classe ne peut normalement pas être déclarée private, mais une classe interne peut l'être — elle n'est alors accessible que depuis la classe externe.
Une classe interne peut avoir les modificateurs d'accès : private, protected, public ou package-private (par défaut).
Une classe interne est implicitement associée à un objet de sa classe externe. Elle ne peut donc pas définir de méthode static. Les champs static final (constantes) sont autorisés car ils sont déterminés à la compilation.
Exemple 2 — Classe interne (non statique)
public class Test {
private int compteur;
private static final String NOM_CLASSE = "Test";
// Classe interne (non statique)
public class ClasseInterne {
private int val;
public ClasseInterne(int val) {
this.val = val;
}
public void afficher() {
// Accès direct aux membres privés de la classe externe
compteur = 2;
System.out.println("La classe interne");
System.out.println("compteur (externe) : " + compteur);
System.out.println("val (interne) : " + val);
System.out.println("Constante externe : " + NOM_CLASSE);
}
// Impossible : méthode statique dans une classe interne
// public static void methodeStatique() { }
}
public static void main(String[] args) {
// Création : d'abord l'objet externe, puis l'objet interne
Test obj = new Test();
Test.ClasseInterne interne = obj.new ClasseInterne(42);
interne.afficher();
}
}
La classe interne compteur (externe) : 2 val (interne) : 42 Constante externe : Test
La syntaxe objetExterne.new ClasseInterne() est spécifique — elle lie l'instance interne à l'instance externe. Sans instance externe, impossible de créer une instance interne.
4. Classes locales (Local Classes)
Une classe locale est définie à l'intérieur d'un bloc — généralement un corps de méthode, mais aussi une boucle for, une clause if ou un bloc d'initialisation. Elle n'est visible que dans ce bloc.
Les classes locales ne sont membres d'aucune classe englobante. Elles ne peuvent être associées à aucun modificateur d'accès (public, private, etc.), mais peuvent être marquées final ou abstract.
Une classe locale peut accéder aux variables et paramètres du bloc englobant, à condition qu'ils soient effectivement final — c'est-à-dire qu'ils ne soient modifiés qu'une seule fois (ou jamais). Depuis Java 8, le mot-clé final n'est plus obligatoire si la variable n'est pas modifiée après son initialisation.
Exemple 3 — Classe locale dans une méthode
public class Test {
private int champExterne = 10;
public void methodeAvecClasseLocale() {
// Variables locales (effectivement finales)
int coefficient = 3;
String prefixe = "Résultat : ";
// Classe locale — définie dans la méthode
class CalculatriceLocale {
private int multiplicateur;
public CalculatriceLocale(int multiplicateur) {
this.multiplicateur = multiplicateur;
}
public int calculer(int valeur) {
// Accès au champ de la classe externe
int base = champExterne;
// Accès aux variables locales (effectivement finales)
return (base + valeur) * coefficient * multiplicateur;
}
public void afficherResultat(int valeur) {
System.out.println(prefixe + calculer(valeur));
}
}
// Instanciation dans le bloc où la classe est définie
CalculatriceLocale calc = new CalculatriceLocale(2);
calc.afficherResultat(5); // (10 + 5) * 3 * 2 = 90
}
public static void main(String[] args) {
Test obj = new Test();
obj.methodeAvecClasseLocale();
// Impossible : la classe locale n'est pas visible ici
// CalculatriceLocale calc2 = new CalculatriceLocale(1);
}
}
Résultat : 90
5. Classes anonymes (Anonymous Classes)
Une classe anonyme est une classe locale sans nom, définie et instanciée en une seule expression. Elle est utilisée lorsque vous avez besoin d'une classe simple, utilisée une seule fois, souvent pour implémenter une interface ou étendre une classe de manière ponctuelle.
Vous utilisez l'opérateur new pour créer une classe anonyme, en définissant simultanément son corps. L'instance créée hérite d'une superclasse ou implémente une interface — les mots-clés extends ou implements ne sont pas utilisés.
Exemple 4 — Classe anonyme implémentant une interface
interface Forme {
void afficher();
double surface();
}
public class Test {
public static void main(String[] args) {
String message = "Hello, ";
// Classe anonyme implémentant l'interface Forme
Forme rectangle = new Forme() {
private double longueur = 5.0;
private double largeur = 3.0;
@Override
public void afficher() {
System.out.println(message + "Je suis un rectangle");
System.out.println("Surface : " + surface());
}
@Override
public double surface() {
return longueur * largeur;
}
};
rectangle.afficher();
// Classe anonyme étendant une classe
Thread monThread = new Thread() {
@Override
public void run() {
System.out.println("Exécution dans un thread anonyme");
}
};
monThread.start();
}
}
Hello, Je suis un rectangle Surface : 15.0 Exécution dans un thread anonyme
- Une classe anonyme doit implémenter une interface ou hériter d'une classe (qui doit avoir un constructeur accessible).
- Elle doit redéfinir toutes les méthodes abstraites de l'interface ou de la superclasse.
- Elle peut accéder aux variables de la méthode englobante si elles sont effectivement
final. - Elle ne peut pas avoir de constructeur explicite (car elle n'a pas de nom).
- Elle peut avoir des blocs d'initialisation d'instance (
{ ... }).
6. Hiérarchie et règles d'accès — Tableau récapitulatif
| Type | Modificateurs autorisés | Accès aux membres externes | Instanciation | Membres statiques |
|---|---|---|---|---|
| Statique imbriquée | public, private, protected, static |
Uniquement membres statiques (via référence d'objet) | new Externe.Statique() |
Oui (méthodes et champs statiques) |
| Interne (non statique) | public, private, protected |
Tous les membres (même privés), via Externe.this |
externe.new Interne() |
Non (sauf static final) |
| Locale | final, abstract (uniquement) |
Membres externes + variables locales effectivement finales | Dans le bloc de définition uniquement | Non |
| Anonyme | Aucun (pas de nom) | Membres externes + variables effectivement finales | À la définition (new Interface() { ... }) |
Non |
7. Shadowing et accès explicite — Externe.this
Lorsqu'une classe interne déclare un champ ou une méthode portant le même nom qu'un membre de la classe externe, le membre interne masque (shadows) le membre externe. Pour accéder explicitement au membre de la classe externe, utilisez ClasseExterne.this.membre.
Exemple 5 — Résolution des conflits de noms
public class Externe {
private int valeur = 100;
private String nom = "Externe";
public class Interne {
private int valeur = 200; // Masque la variable externe
public void afficher() {
int valeur = 300; // Variable locale (masque les deux)
System.out.println("valeur locale : " + valeur); // 300
System.out.println("this.valeur : " + this.valeur); // 200
System.out.println("Externe.this.valeur : " + Externe.this.valeur); // 100
System.out.println("nom externe : " + Externe.this.nom); // Externe
}
}
public static void main(String[] args) {
Externe e = new Externe();
Externe.Interne i = e.new Interne();
i.afficher();
}
}
valeur locale : 300 this.valeur : 200 Externe.this.valeur : 100 nom externe : Externe
8. Interface imbriquée (Nested Interface)
Une interface peut également être imbriquée dans une classe ou une autre interface. Cela permet de regrouper des interfaces liées logiquement.
Exemple 6 — Interface imbriquée
class Conteneur {
// Interface imbriquée dans une classe
interface Traitable {
void traiter();
}
// Interface imbriquée avec modificateur private
private interface Interne {
void executer();
}
// Implémentation de l'interface interne
class ImplInterne implements Interne {
@Override
public void executer() {
System.out.println("Exécution de l'interface interne");
}
}
}
// Implémentation de l'interface publique
class MonTraitement implements Conteneur.Traitable {
@Override
public void traiter() {
System.out.println("Traitement effectué");
}
}
public class TestInterfaceImbriquee {
public static void main(String[] args) {
Conteneur.Traitable t = new MonTraitement();
t.traiter();
}
}
Traitement effectué
9. Exemple complet — Système de Builder avec classe interne statique
Exemple 7 — Pattern Builder avec classe statique imbriquée (Java 21)
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* Classe immuable représentant une Personne
* Utilise le pattern Builder avec une classe statique imbriquée
*/
public class Personne {
// Champs obligatoires
private final String nom;
private final String prenom;
// Champs optionnels
private final LocalDate dateNaissance;
private final String email;
private final List<String> telephone;
// Constructeur privé — seule la classe Builder peut l'appeler
private Personne(Builder builder) {
this.nom = builder.nom;
this.prenom = builder.prenom;
this.dateNaissance = builder.dateNaissance;
this.email = builder.email;
this.telephone = builder.telephone != null
? List.copyOf(builder.telephone)
: List.of();
}
// Getters
public String getNom() { return nom; }
public String getPrenom() { return prenom; }
public LocalDate getDateNaissance() { return dateNaissance; }
public String getEmail() { return email; }
public List<String> getTelephone() { return telephone; }
@Override
public String toString() {
return String.format("Personne{nom='%s', prenom='%s', dateNaissance=%s, email='%s', telephone=%s}",
nom, prenom, dateNaissance, email, telephone);
}
// ════════════════════════════════════════════════════════════
// Classe Builder — statique imbriquée
// ════════════════════════════════════════════════════════════
public static class Builder {
// Champs obligatoires
private final String nom;
private final String prenom;
// Champs optionnels (avec valeurs par défaut)
private LocalDate dateNaissance;
private String email;
private List<String> telephone;
public Builder(String nom, String prenom) {
if (nom == null || nom.isBlank()) {
throw new IllegalArgumentException("Le nom ne peut pas être vide");
}
if (prenom == null || prenom.isBlank()) {
throw new IllegalArgumentException("Le prénom ne peut pas être vide");
}
this.nom = nom;
this.prenom = prenom;
this.telephone = new ArrayList<>();
}
public Builder dateNaissance(LocalDate dateNaissance) {
this.dateNaissance = dateNaissance;
return this;
}
public Builder email(String email) {
if (email != null && email.contains("@")) {
this.email = email;
}
return this;
}
public Builder ajouterTelephone(String numero) {
if (this.telephone == null) {
this.telephone = new ArrayList<>();
}
this.telephone.add(numero);
return this;
}
public Personne build() {
return new Personne(this);
}
}
// ════════════════════════════════════════════════════════════
// Méthode main — démonstration
// ════════════════════════════════════════════════════════════
public static void main(String[] args) {
Personne personne = new Personne.Builder("El Alaoui", "Mostafa")
.dateNaissance(LocalDate.of(1985, 5, 15))
.email("mostafa@example.com")
.ajouterTelephone("06 12 34 56 78")
.ajouterTelephone("05 12 34 56 78")
.build();
System.out.println(personne);
}
}
Personne{nom='El Alaoui', prenom='Mostafa', dateNaissance=1985-05-15, email='mostafa@example.com', telephone=[06 12 34 56 78, 05 12 34 56 78]}
Ce pattern est très utilisé en Java pour construire des objets immuables avec de nombreux paramètres optionnels. La classe Builder est statique imbriquée car elle n'a pas besoin d'une instance de Personne pour exister — elle est indépendante et sert uniquement à construire des objets Personne.
10. Exercice
Système de gestion de comptes bancaires
Concevoir un système de gestion de comptes bancaires utilisant différents types de classes imbriquées.
Travail demandé
- Créer une classe
Banquecontenant :- Attribut
nom(String) etcomptes(List<Compte>) - Une classe interne
Compte(non statique) avec attributsnumero,soldeet méthodesdeposer(),retirer() - Une classe statique imbriquée
Transaction(statique) représentant une opération bancaire - Une méthode
creerCompteAvecInteret()contenant une classe localeCompteAvecInteretqui étendCompteet ajoute un attributtauxInteret - Une méthode
trierComptes()utilisant une classe anonyme implémentantComparator<Compte>
- Attribut
- Tester toutes les fonctionnalités dans un
main.
import java.util.*;
/**
* Classe Banque — démonstration des différents types de classes imbriquées
*/
public class Banque {
private String nom;
private List<Compte> comptes;
public Banque(String nom) {
this.nom = nom;
this.comptes = new ArrayList<>();
}
public String getNom() { return nom; }
// ════════════════════════════════════════════════════════════
// Classe interne (non statique) — Compte
// ════════════════════════════════════════════════════════════
public class Compte {
private String numero;
private double solde;
public Compte(String numero, double soldeInitial) {
this.numero = numero;
this.solde = soldeInitial;
comptes.add(this);
}
public String getNumero() { return numero; }
public double getSolde() { return solde; }
public void deposer(double montant) {
if (montant > 0) {
solde += montant;
System.out.printf("Dépôt de %.2f € sur le compte %s%n", montant, numero);
}
}
public boolean retirer(double montant) {
if (montant > 0 && montant <= solde) {
solde -= montant;
System.out.printf("Retrait de %.2f € du compte %s%n", montant, numero);
return true;
}
System.out.printf("Retrait impossible sur le compte %s (solde: %.2f, montant demandé: %.2f)%n",
numero, solde, montant);
return false;
}
public void afficher() {
System.out.printf("Compte %s : %.2f €%n", numero, solde);
}
// Accès à la classe externe
public String getNomBanque() {
return Banque.this.nom;
}
}
// ════════════════════════════════════════════════════════════
// Classe statique imbriquée — Transaction
// ════════════════════════════════════════════════════════════
public static class Transaction {
private String reference;
private LocalDateTime date;
private double montant;
private TypeTransaction type;
public enum TypeTransaction {
DEPOT, RETRAIT, VIREMENT
}
public Transaction(String reference, double montant, TypeTransaction type) {
this.reference = reference;
this.date = LocalDateTime.now();
this.montant = montant;
this.type = type;
}
public String getReference() { return reference; }
public LocalDateTime getDate() { return date; }
public double getMontant() { return montant; }
public TypeTransaction getType() { return type; }
public void afficher() {
System.out.printf("[%s] %s : %.2f € (ref: %s)%n",
date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")),
type, montant, reference);
}
// Méthode statique dans une classe statique
public static Transaction creerTransactionTest(String ref, double montant) {
return new Transaction(ref, montant, TypeTransaction.DEPOT);
}
}
// ════════════════════════════════════════════════════════════
// Méthode avec classe locale — CompteAvecInteret
// ════════════════════════════════════════════════════════════
public Compte creerCompteAvecInteret(String numero, double soldeInitial, double tauxInteret) {
final double taux = tauxInteret; // effectivement final
// Classe locale — étend Compte
class CompteAvecInteret extends Compte {
private double interetsGagnes = 0;
public CompteAvecInteret(String numero, double soldeInitial) {
super(numero, soldeInitial);
}
public void appliquerInterets() {
double interets = getSolde() * taux / 100;
deposer(interets);
interetsGagnes += interets;
System.out.printf("Intérêts appliqués : %.2f € (taux: %.1f%%)%n", interets, taux);
}
public double getInteretsGagnes() {
return interetsGagnes;
}
@Override
public void afficher() {
super.afficher();
System.out.printf(" Intérêts cumulés : %.2f €%n", interetsGagnes);
}
}
CompteAvecInteret compte = new CompteAvecInteret(numero, soldeInitial);
comptes.remove(compte); // Le compte a déjà été ajouté par le constructeur de Compte
comptes.add(compte); // On le réajoute (ou on pourrait modifier la logique)
return compte;
}
// ════════════════════════════════════════════════════════════
// Méthode avec classe anonyme — tri des comptes
// ════════════════════════════════════════════════════════════
public void trierComptesParSolde() {
// Classe anonyme implémentant Comparator
Comparator<Compte> comparateur = new Comparator<Compte>() {
@Override
public int compare(Compte c1, Compte c2) {
return Double.compare(c2.getSolde(), c1.getSolde()); // Ordre décroissant
}
};
comptes.sort(comparateur);
System.out.println("Comptes triés par solde décroissant");
}
public void trierComptesParNumero() {
// Version avec lambda (équivalente à une classe anonyme fonctionnelle)
comptes.sort((c1, c2) -> c1.getNumero().compareTo(c2.getNumero()));
System.out.println("Comptes triés par numéro croissant");
}
public void afficherTousLesComptes() {
System.out.printf("\n=== Banque %s ===%n", nom);
if (comptes.isEmpty()) {
System.out.println("Aucun compte");
} else {
for (Compte c : comptes) {
c.afficher();
}
}
}
// ════════════════════════════════════════════════════════════
// Main — démonstration
// ════════════════════════════════════════════════════════════
public static void main(String[] args) {
Banque banque = new Banque("Crédit du Maroc");
// 1. Création de comptes standards (classe interne)
Banque.Compte compte1 = banque.new Compte("FR76 1234 5678", 1000.0);
Banque.Compte compte2 = banque.new Compte("FR76 8765 4321", 2500.0);
compte1.deposer(500);
compte2.retirer(200);
// 2. Utilisation de la classe statique imbriquée
Banque.Transaction transaction = new Banque.Transaction(
"TXN-001", 1500.0, Banque.Transaction.TypeTransaction.VIREMENT
);
transaction.afficher();
Banque.Transaction transactionTest = Banque.Transaction.creerTransactionTest("TXN-TEST", 99.99);
transactionTest.afficher();
// 3. Création d'un compte avec intérêts (classe locale)
Banque.Compte compte3 = banque.creerCompteAvecInteret("FR76 9999 8888", 2000.0, 5.0);
// Application des intérêts (cast nécessaire pour accéder aux méthodes spécifiques)
if (compte3 instanceof Banque.CompteAvecInteret) {
// Pour accéder à la méthode spécifique, on aurait besoin de caster
// Dans une vraie implémentation, on pourrait stocker une référence spécifique
}
// 4. Tri des comptes (classe anonyme)
banque.afficherTousLesComptes();
banque.trierComptesParSolde();
banque.afficherTousLesComptes();
banque.trierComptesParNumero();
banque.afficherTousLesComptes();
}
}
- Classe interne
Compte: non statique, a accès àBanque.this.nom— chaque compte est lié à sa banque. - Classe statique
Transaction: indépendante de toute instance deBanque, peut contenir des méthodes statiques et desenum. - Classe locale
CompteAvecInteret: définie dans une méthode, étendCompte, accès à la variable localetaux(effectivement final). - Classe anonyme
Comparator: définie et instanciée en une seule expression pour le tri personnalisé. - Syntaxe d'instanciation :
banque.new Compte(...)pour une classe interne ;new Banque.Transaction(...)pour une classe statique imbriquée.
L'essentiel en bref
- Classe statique imbriquée : indépendante de l'instance externe, accès uniquement aux membres statiques. Idéale pour les builders, les utilitaires spécifiques.
- Classe interne (non statique) : liée à une instance externe, accès à tous ses membres (même privés). Utilisée pour des objets qui n'existent pas sans l'objet conteneur.
- Classe locale : définie dans un bloc, visible uniquement dans ce bloc. Utile pour des traitements ponctuels complexes.
- Classe anonyme : classe locale sans nom, définie et instanciée en une expression. Parfaite pour les callbacks, listeners, comparateurs simples.
- Règle d'accès : les classes locales et anonymes ne peuvent accéder qu'aux variables effectivement finales du bloc englobant.
- Fichiers compilés : une classe imbriquée produit un fichier
Externe$Interne.class.
Les classes imbriquées ont été introduites dans Java 1.1 (1997) pour améliorer l'encapsulation et permettre une meilleure organisation du code. Les classes anonymes ont immédiatement été adoptées pour la gestion d'événements en Swing/AWT. Java 8 a renforcé leur utilité avec les expressions lambda, qui remplacent souvent les classes anonymes fonctionnelles. Les classes internes non statiques sont essentielles pour implémenter certains patterns comme l'Iterator (où l'itérateur a besoin d'accéder à la collection parente).
Discussion (0)
Soyez le premier à laisser un commentaire !
Laisser un commentaire
Votre commentaire sera visible après modération.