1. Qu’est-ce qu’une classe interne (inner class) ?
En Java, une classe peut être déclarée non seulement au niveau supérieur (dans un fichier), mais aussi à l’intérieur d’une autre classe. Une telle classe est appelée imbriquée (nested class). Si cette classe est déclarée sans le modificateur static, on l’appelle une classe interne (ou non statique, « non-static inner class », ou simplement « inner class »).
Une classe interne est une classe déclarée à l’intérieur d’une autre classe et liée à une instance de cette classe externe. Elle peut accéder à tous les champs et méthodes de la classe externe, même s’ils sont private. C’est comme si l’objet avait son « assistant secret » à qui tout est permis !
C’est un peu comme une maison avec des pièces. La maison — c’est la classe externe, et la pièce — la classe interne. La pièce ne peut pas exister sans la maison, mais elle a accès à ses ressources : lumière, chauffage, meubles. Si la maison disparaît, la pièce disparaît aussi. Les classes internes en Java fonctionnent exactement de la même manière !
Pourquoi utiliser les classes internes ?
- Lien logique : lorsqu’une classe n’est utile que pour fonctionner avec une autre classe et n’a pas de sens en dehors d’elle.
- Encapsulation : permet de cacher les détails d’implémentation sans encombrer l’espace de noms du package.
- Accès aux membres privés : une classe interne peut accéder aux champs et méthodes privés de la classe externe.
- Compacité : réduit le nombre de classes « parasites » au niveau du package.
Syntaxe de déclaration d’une classe interne
Déclarer une classe interne est très simple : elle est déclarée à l’intérieur du corps d’une autre classe, sans le modificateur static.
class Outer {
// champs et méthodes de la classe externe
class Inner {
// champs et méthodes de la classe interne
void printHello() {
System.out.println("Hello from Inner!");
}
}
}
Ici, Inner est une classe interne par rapport à Outer.
Illustration : visualisation
Outer
│
├─ champs/méthodes
│
└─ Inner (classe interne)
└─ ses propres champs/méthodes
2. Comment créer une instance d’une classe interne
Voici l’un des pièges les plus fréquents pour les débutants ! Une instance de classe interne est toujours liée à un objet précis de la classe externe.
Exemple :
Outer outer = new Outer(); // on crée l’objet de la classe externe
Outer.Inner inner = outer.new Inner(); // on crée la classe interne via l’objet externe !
inner.printHello(); // Hello from Inner!
Tenter de faire simplement new Inner() entraînera une erreur de compilation, car Java ne sait pas à quel objet de la classe externe associer cette classe interne.
Pourquoi ?
La classe interne peut accéder aux champs et méthodes de la classe externe. Elle doit donc « savoir » avec quel objet de la classe externe elle travaille.
3. Exemple d’utilisation d’une classe interne
Voyons un exemple où une classe interne est vraiment utile.
Exemple 1 : Modèle « Sac à dos et objets »
Supposons que nous ayons une classe Backpack qui peut stocker des objets (Item). Nous voulons cependant que la classe Item soit accessible uniquement à l’intérieur de Backpack, car en dehors du sac à dos ces objets ne nous intéressent pas.
public class Backpack {
private String owner;
public Backpack(String owner) {
this.owner = owner;
}
// Classe interne — l’objet ne peut exister que dans le sac à dos !
class Item {
private String name;
public Item(String name) {
this.name = name;
}
public void printInfo() {
// Magique ! On voit le champ private de la classe externe
System.out.println(owner + " a l’objet : " + name);
}
}
}
Utilisation :
Backpack bp = new Backpack("John");
Backpack.Item item = bp.new Item("Manuel Java");
item.printInfo(); // John a l’objet : Manuel Java
Remarquez : Item peut accéder au champ owner, même s’il est private !
Exemple 2 : Itérateur pour sa propre collection
En Java, les collections implémentent souvent un itérateur en tant que classe interne. Essayons d’écrire une simple collection avec un itérateur interne.
public class IntList {
private int[] data = new int[10];
private int size = 0;
public void add(int value) {
data[size++] = value;
}
// Classe interne — itérateur !
class Iterator {
private int index = 0;
public boolean hasNext() {
return index < size;
}
public int next() {
return data[index++];
}
}
}
Utilisation :
IntList list = new IntList();
list.add(10);
list.add(20);
IntList.Iterator it = list.new Iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
4. Caractéristiques des classes internes
Accès aux membres de la classe externe
Une classe interne peut accéder à n’importe quels champs et méthodes de la classe externe (même s’ils sont private). C’est pratique, mais cela demande de la prudence : si la classe externe change, la classe interne peut soudain « cesser de fonctionner ».
class Outer {
private int secret = 42;
class Inner {
void showSecret() {
System.out.println("Secret: " + secret);
}
}
}
Une classe interne ne peut pas contenir de membres statiques
Une classe interne (non-static inner) ne peut pas contenir de champs ou méthodes statiques, à l’exception des constantes static final (par exemple, public static final int MY_CONST = 123;). Cela vient du fait qu’une classe interne est toujours « rattachée » à une instance de la classe externe.
Si vous devez déclarer une classe imbriquée statique, utilisez le modificateur static (nous en parlerons dans la leçon suivante).
Visibilité d’une classe interne
Une classe interne peut être private, protected, public ou avoir une visibilité de niveau package (package-private).
public class Outer {
private class Inner { /* ... */ }
}
5. Points utiles
Quand utiliser les classes internes
Les classes internes ne sont pas décoratives : elles servent à organiser logiquement le code. Utilisez-les lorsque :
- La classe n’est nécessaire qu’à un seul endroit (par exemple, un itérateur auxiliaire, un gestionnaire, une partie d’une structure complexe).
- La classe est étroitement liée à la classe externe et n’a pas de sens en dehors d’elle.
- Vous souhaitez cacher les détails d’implémentation aux autres classes du package.
N’utilisez pas les classes internes juste pour faire « tendance » ! Il vaut parfois mieux placer la classe au niveau supérieur si elle peut être utile ailleurs.
Classe interne et this
À l’intérieur d’une classe interne, on peut accéder aux champs et méthodes de la classe externe à l’aide de OuterClassName.this.
class Car {
private String model = "Tesla";
class Engine {
void printModel() {
// On se réfère explicitement à l’objet externe
System.out.println("Modèle: " + Car.this.model);
}
}
}
Cette syntaxe est particulièrement utile si les classes interne et externe ont des champs portant les mêmes noms.
Quand il NE faut PAS utiliser les classes internes
Ne les utilisez pas si :
- La classe interne n’accède pas aux champs/méthodes de la classe externe
- La classe peut être utile ailleurs dans le programme
- La classe externe devient trop volumineuse et complexe
Dans ces cas, il vaut mieux :
- Rendre la classe imbriquée statique (static class)
- Placer la classe dans un fichier séparé
- Utiliser des méthodes ordinaires au lieu d’une classe entière
6. Erreurs typiques avec les classes internes
Erreur n° 1 : tentative de créer une classe interne sans objet de la classe externe.
Si vous écrivez new Inner(), le compilateur émettra l’erreur : "No enclosing instance of type Outer is accessible". Créez toujours la classe interne via un objet de la classe externe : outer.new Inner().
Erreur n° 2 : tentative de déclarer des champs ou méthodes statiques dans une classe interne.
Une classe interne ne peut pas contenir de membres statiques (sauf les constantes). Si nécessaire, utilisez une classe imbriquée statique, c’est-à-dire déclarez la classe avec le modificateur static.
Erreur n° 3 : surutilisation des classes internes.
Si la classe interne n’utilise pas les champs/méthodes de la classe externe, elle devrait probablement être static ou bien être sortie à l’extérieur. Ne créez pas de classes internes « au cas où ».
Erreur n° 4 : confusion lors de l’accès aux champs de la classe externe.
Si les classes interne et externe ont des champs portant les mêmes noms, utilisez OuterClassName.this.field pour faire explicitement référence au champ de la classe externe.
GO TO FULL VERSION