Les classes imbriquées en Java

11 Sep 2019 11 Sep 2019 7349 vues ESSADDOUKI Mostafa 20 min de lecture
Introduction
1 Nouveautés de Java 11 2 Différences entre JDK, JRE et JVM 3 Structure d'un programme Java - Hello World 4 Mots clés et conventions de dénomination en Java 5 Types de données intégrés en Java 6 Les variables en Java 7 Classes enveloppe - Number, Integer, Double ... 8 Lire les entrées clavier en Java
Structures de contrôle
9 Les opérateurs en Java 10 Les structures conditionnelles en Java 11 Les boucles en Java 12 Instructions de contrôle de boucle - break, continue
Chaines de caractères
13 Les chaines en Java - API String 14 Les chaines en Java - StringBuffer et StringBuilder 15 Les expressions régulières en Java
Programmation OO
16 Objets et classes en Java 17 Modificateurs d'accès Java - public, private, protected et package 18 Méthodes et surcharge des méthodes en Java 19 les constructeurs en Java 20 L'héritage en Java 21 Classes abstraites en Java 22 Interfaces et héritage multiple en Java 23 Les classes imbriquées en Java 24 Les singletons en Java 25 Classes et méthodes génériques 26 Interface fonctionnelle et expressions Lambda en Java
Tableaux et collections
27 Les tableaux en Java 28 Classe Arrays - java.util.Arrays 29 Les listes dynamiques - java.util.ArrayList 30 Les listes chaînées en Java - java.util.LinkedList 31 HashSet en Java - java.util.HashSet 32 HashMap en Java - java.util.HashMap
Gestion des fichiers
33 Comprendre les fichiers informatiques 34 Utilisation des classes Path et Files en Java 35 Lecture et écriture dans un fichier en Java 36 Fichiers à accès aléatoire en Java
Gestion d'exceptions
37 Gestion d'exceptions en Java 38 Créez vos propres classes d'exception en Java
Programmation concurrente
39 Introduction à la programmation concurrente en Java - Multi-threads 40 classe java.lang.Thread 41 Synchronisation des threads en Java
Cours Java pour les débutants — Étape 23 sur 41

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 ?

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
Pourquoi imbriquer des classes ?

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)

Classe imbriquée statique

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 :

   
Syntaxe — Accès à une classe statique imbriquée Java
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();
    }
}
Sortie
La classe imbriquée statique
Message : Message de la classe statique
Méthode statique dans classe imbriquée
Fichiers générés à la compilation

Lorsque vous compilez l'exemple ci-dessus, deux fichiers sont produits :

  • Test.class — la classe externe
  • Test$ClasseStatique.class — la classe imbriquée (le symbole $ sépare les noms)

3. Classes internes (Inner Classes / Non-static Nested Classes)

Classe interne (Inner Class)

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).

⚠️ Pas de méthodes statiques dans une classe interne

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();
    }
}
Sortie
La classe interne
compteur (externe) : 2
val (interne) : 42
Constante externe : Test
Syntaxe d'instanciation d'une classe interne

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)

Classe locale

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.

Accès aux variables locales

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);
    }
}
Sortie
Résultat : 90

5. Classes anonymes (Anonymous Classes)

Classe anonyme

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();
    }
}
Sortie
Hello, Je suis un rectangle
Surface : 15.0
Exécution dans un thread anonyme
Règles pour les classes anonymes
  • 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();
    }
}
Sortie
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();
    }
}
Sortie
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);
    }
}
Sortie
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]}
Pattern Builder avec classe statique imbriquée

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

Niveau : Intermédiaire

Concevoir un système de gestion de comptes bancaires utilisant différents types de classes imbriquées.

Travail demandé

  1. Créer une classe Banque contenant :
    • Attribut nom (String) et comptes (List<Compte>)
    • Une classe interne Compte (non statique) avec attributs numero, solde et méthodes deposer(), retirer()
    • Une classe statique imbriquée Transaction (statique) représentant une opération bancaire
    • Une méthode creerCompteAvecInteret() contenant une classe locale CompteAvecInteret qui étend Compte et ajoute un attribut tauxInteret
    • Une méthode trierComptes() utilisant une classe anonyme implémentant Comparator<Compte>
  2. Tester toutes les fonctionnalités dans un main.

  L'essentiel en bref

Synthèse — Classes imbriquées en Java
  • 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.
Un peu d'histoire

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).

Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.