17. Au-delà des Classes
Table des matières
- 17.1 Interfaces
- 17.2 Types sealed, non-sealed et final
- 17.3 Enum
- 17.4 Records (Java 16+)
- 17.5 Classes Imbriquées en Java
- 17.6 Imbrication des Interfaces en Java
Ce chapitre présente plusieurs mécanismes de type (type) avancés, au-delà de celui, déjà étudié, de la classe : interfaces, enum, classes sealed / non-sealed, records et classes imbriquées.
17.1 Interfaces
Une interface en Java est un type de référence qui définit un contrat de méthodes qu’une classe accepte d’implémenter.
Une interface est implicitement abstract et ne peut pas être marquée final : comme pour les classes top-level, une interface peut déclarer une visibilité public ou default (package-private).
Une classe Java peut implémenter un nombre quelconque d’interfaces via le mot-clé implements.
Une interface peut à son tour étendre plusieurs interfaces en utilisant le mot-clé extends.
Les interfaces permettent l’abstraction, un couplage faible et l’héritage multiple de type.
17.1.1 Ce que les Interfaces Peuvent Contenir
- Méthodes abstraites (implicitement
publicetabstract) - Méthodes concrètes
- Méthodes default (contiennent du code et sont implicitement
public) - Méthodes static (déclarées
static, contiennent du code et sont implicitementpublic) - Méthodes private (Java 9+) pour la réutilisation interne
- Méthodes default (contiennent du code et sont implicitement
- Constantes → implicitement
public static finalet initialisées à la déclaration
interface Calculator {
int add(int a, int b); // abstract
default int mult(int a, int b) { // default method
return a * b;
}
static double pi() { return 3.14; } // static method
}
Warning
Puisque les méthodes abstraites des interfaces sont implicitement public, vous ne pouvez pas réduire le niveau d’accès sur une méthode d’implémentation.
17.1.2 Implémenter une Interface
class BasicCalc implements Calculator {
public int add(int a, int b) { return a + b; }
}
Note
Chaque méthode abstraite doit être implémentée, sauf si la classe est elle-même abstraite.
17.1.3 Héritage Multiple
Une classe peut implémenter plusieurs interfaces.
interface A { void a(); }
interface B { void b(); }
class C implements A, B {
public void a() {}
public void b() {}
}
17.1.4 Héritage des Interfaces et Conflits
Si deux interfaces fournissent des méthodes default avec la même signature, la classe qui implémente doit redéfinir (override) la méthode.
interface X { default void run() { } }
interface Y { default void run() { } }
class Z implements X, Y {
public void run() { } // mandatory
}
Si vous voulez tout de même accéder à une implémentation particulière de la méthode default héritée, vous pouvez utiliser la syntaxe suivante :
interface X { default int run() { return 1; } }
interface Y { default int run() { return 2; } }
class Z implements X, Y {
public int useARun(){
return Y.super.run();
}
}
17.1.5 Méthodes Default
Une méthode default (déclarée avec le mot-clé default) est une méthode qui définit une implémentation et peut être redéfinie par une classe qui implémente l interface.
- Une méthode default contient du code et est implicitement
public; - Une méthode default ne peut pas être
abstract,staticoufinal; - Une interface peut redéclarer une méthode
defaultet fournir une implémentation différente ; - Une sous-interface est autorisée à redéclarer une méthode statique d'une superinterface comme méthode
default. - Comme nous l'avons vu juste au-dessus, si deux interfaces fournissent des méthodes default avec la même signature, la classe implémentante doit redéfinir la méthode ;
- Une classe implémentante peut bien sûr s'appuyer sur l'implémentation fournie par la méthode
defaultsans la redéfinir ; - La méthode
defaultpeut être invoquée sur une instance de la classe implémentante et NON comme une méthodestaticde l interface qui la contient ; - Une classe (ou une interface) peut invoquer explicitement une méthode default d'une interface qui est directement mentionnée dans sa clause
implements(ouextends) en utilisant la syntaxeInterfaceName.super.methodName(); cela est généralement utilisé pour lever l'ambiguïté entre plusieurs méthodes default héritées ; - Cette syntaxe ne peut être utilisée que si l'interface est explicitement mentionnée dans la clause
implements(ouextends) ; elle ne peut pas être utilisée pour invoquer une méthode default provenant d'une interface héritée indirectement ; - La syntaxe
InterfaceName.super.methodName()s'applique uniquement aux méthodesdefaultet ne peut pas être utilisée pour des méthodes abstraites, des méthodes statiques, des méthodes privées d'interface ou des champs.
Example: Utilisation correcte
interface A {
default void hello() {
System.out.println("Hello from A");
}
}
class B implements A {
@Override
public void hello() {
A.super.hello(); // ✅ allowed
System.out.println("Hello from B");
}
}
Example: Utilisation incorrecte
interface A {
default void hello() {
System.out.println("Hello from A");
}
}
interface B extends A {
}
class C implements B {
public void test() {
A.super.hello(); // ❌ compilation error
}
}
Note
- Une sous-interface est autorisée à redéclarer une méthode statique d'une superinterface comme méthode
default.
Exemple :
interface Parent {
static void p() { }
}
interface Child extends Parent {
default void p() { } // VALID, static method redeclared as default
}
Note
- Une interface est autorisée à redéclarer une méthode
defaulthéritée d une superinterface et à la transformer en méthodeabstract.
Lorsque cela se produit, l implémentation default provenant de la superinterface est effectivement supprimée dans la sous-interface.
En conséquence, toute classe qui implémente la sous-interface N héritera PAS de l implémentation default originale et devra fournir sa propre implémentation.
Exemple :
interface Parent {
default void greet() {
System.out.println("Hello from Parent");
}
}
interface Child extends Parent {
void greet(); // redéclarée comme abstraite
}
class Demo implements Child {
public void greet() { // obligatoire
System.out.println("Hello from Demo");
}
}
Explication :
Parentfournit une implémentation par défaut degreet().Childredéclaregreet()sansdefault, la rendant à nouveau abstraite.Demone peut pas hériter de l implémentation par défaut deParent.- Par conséquent,
Demodoit implémenter explicitementgreet().
17.1.6 Méthodes static
- Une interface peut fournir des
static methods(via le mot-cléstatic) qui sont implicitementpublic; - Les méthodes static doivent inclure un corps de méthode et sont accessibles via le nom de l’interface ;
- Les méthodes static ne peuvent pas être
abstractoufinal;
17.1.7 Méthodes private dans les interfaces
Parmi toutes les méthodes concrètes qu’une interface peut implémenter, nous avons aussi :
- Méthodes
private: visibles uniquement à l’intérieur de l’interface déclarante et qui ne peuvent être invoquées que depuis un contextenon-static(méthodesdefaultou autresnon-static private methods). - Méthodes
private static: visibles uniquement à l’intérieur de l’interface déclarante et qui peuvent être invoquées par n’importe quelle méthode de l’interface englobante.
17.2 Types sealed, non-sealed et final
Les classes et interfaces sealed (Java 17+) restreignent quelles autres classes (ou interfaces) peuvent les étendre ou les implémenter.
Un sealed type est déclaré en plaçant le modificateur sealed juste avant le mot-clé class (ou interface), et en ajoutant, après le nom du Type, le mot-clé permits suivi de la liste des types qui peuvent l’étendre (ou l’implémenter).
public sealed class Shape permits Circle, Rectangle { }
final class Circle extends Shape { }
non-sealed class Rectangle extends Shape { }
17.2.1 Règles
- Un type sealed doit déclarer tous les sous-types autorisés.
- Un sous-type autorisé doit être final, sealed ou non-sealed ; puisque les interfaces ne peuvent pas être final, elles ne peuvent être marquées que
sealedounon-sealedlorsqu’elles étendent une interface sealed. - Si une sealed class appartient à un
namedmodule, alors toutes les classes listées dans sa permits clause doivent également appartenir à cemême module. - Si une sealed class appartient à un
unnamed module, alors toutes les classes listées dans sa permits clause doivent être déclarées dans lemême package.
17.3 Enum
Les enum définissent un ensemble fixe de valeurs constantes.
Les enum peuvent déclarer des attributs, des constructeurs et des méthodes comme des classes ordinaires mais ne peuvent pas être étendues.
La liste des valeurs de l’enum doit se terminer par un point-virgule (;) dans le cas des Enum Complexes, mais ce n’est pas obligatoire pour les Enum Simples.
17.3.1 Définition d’une Enum Simple
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } // point-virgule omis
17.3.2 Enum Complexes avec État et Comportement
enum Level {
LOW(1), MEDIUM(5), HIGH(10); // point-virgule obligatoire
private int code;
Level(int code) { this.code = code; }
public int getCode() { return code; }
}
public static void main(String[] args) {
Level.MEDIUM.getCode(); // invoking a method
}
17.3.3 Méthodes des Enum
values()– renvoie un tableau de toutes les valeurs constantes utilisables, par exemple, dans une bouclefor-eachvalueOf(String)– renvoie la constante par son nomordinal()– index (int) de la constante
17.3.4 Règles
- Les constructeurs d’enum sont implicitement
private; - Les enum peuvent contenir des méthodes
staticetinstance; - Les enum peuvent implémenter des
interfaces; - Les énumérations ne peuvent pas être étendues.
17.4 Records (Java 16+)
Un record est une classe spéciale conçue pour modéliser des données immuables : ils sont en effet implicitement final.
Vous ne pouvez pas étendre un record, mais il est permis d’implémenter une interface normale ou sealed.
Il fournit automatiquement :
- champs private final pour chaque composant ;
- constructeur avec des paramètres dans le même ordre que la déclaration du record ;
- getters (portant le nom des attributs) ;
equals(),hashCode(),toString(): il est également permis de redéfinir (override) ces méthodes ;- Les Records peuvent inclure
nested classes,interfaces,records,enumsetannotations.
public record Point(int x, int y) { }
var element = new Point(11, 22);
System.out.println(element.x);
System.out.println(element.y);
Si vous avez besoin de validation ou de transformation supplémentaire des champs fournis, vous pouvez définir un constructeur long ou un constructeur compact.
17.4.1 Résumé des Règles de Base pour les Records
Un record peut être déclaré à trois emplacements :
- Comme record top-level (directement dans un package)
- Comme record member (à l’intérieur d’une classe ou d’une interface)
- Comme record local (à l’intérieur d’une méthode)
Toutes les classes record member et local sont implicitement static.
- Un record member peut déclarer
staticde manière redondante. - Un record local ne doit pas déclarer
staticexplicitement.
Chaque classe record est implicitement final.
- Déclarer
finalexplicitement est autorisé mais redondant. - Un record ne peut pas être déclaré
abstract,sealedounon-sealed.
La superclasse directe de chaque record est java.lang.Record.
- Un record ne peut pas déclarer de clause
extends. - Un record ne peut étendre aucune autre classe.
La sérialisation des records diffère de celle des classes sérialisables ordinaires.
- Lors de la désérialisation, le constructeur canonique est invoqué.
Le corps d’un record peut contenir :
- Des constructeurs
- Des méthodes
- Des champs statiques
- Des blocs d’initialisation statiques
Le corps d’un record NE doit PAS contenir :
- Des déclarations de champs d’instance
- Des blocs d’initialisation d’instance
- Des méthodes
abstract - Des méthodes
native
17.4.2 Constructeur Long
public record Person(String name, int age) {
public Person (String name, int age){
if (age < 0) throw new IllegalArgumentException();
this.name = name;
this.age = age;
}
}
Vous pouvez aussi définir des constructeurs en surcharge (overload), à condition qu’ils délèguent finalement au constructeur canonique via this(...) :
public record Point(int x, int y) {
// Overloaded constructor (NOT canonical)
public Point(int value) {
this(value, value); // doit invoquer, comme première instruction, un autre constructeur surchargé et, en dernière instance, le constructeur canonique.
}
}
Note
- Le compilateur n’insérera pas de constructeur si vous en fournissez manuellement un avec la même liste de paramètres dans l’ordre défini ;
- Dans ce cas, vous devez définir explicitement chaque champ manuellement ;
17.4.3 Constructeur Compact
Vous pouvez définir un constructeur compact qui initialise implicitement tous les champs, tout en vous permettant d’effectuer des validations et des transformations sur des champs spécifiques.
Java exécutera le constructeur complet, initialisant tous les champs, après que le constructeur compact a terminé.
public record Person(String name, int age) {
public Person {
if (age < 0) throw new IllegalArgumentException();
name = name.toUpperCase(); // This transformation is still (at this level of initialization) on the input parameter.
// this.name = name; // ❌ Does not compile.
}
}
Warning
- Si vous essayez de modifier un attribut de Record dans un Constructeur Compact, votre code ne compilera pas
17.4.4 Pattern Matching pour les Records
Quand vous utilisez le pattern matching avec instanceof ou avec switch, un record pattern doit spécifier :
- Le type du record ;
- Un pattern pour chaque champ du record (correspondant au bon nombre de composants, et avec des types compatibles) ;
Exemple record :
Object obj = new Point(3, 5);
if (obj instanceof Point(int a, int b)) {
System.out.println(a + b); // 8
}
17.4.5 Nested Record Patterns et Matching des Records avec var et Generics
Les nested record patterns permettent de déstructurer des records qui contiennent d’autres records ou des types complexes, en extrayant récursivement des valeurs directement dans le pattern.
Ils combinent la puissance de la déconstruction des record avec le pattern matching, vous donnant une manière concise et expressive de naviguer dans des structures de données hiérarchiques.
17.4.5.1 Nested Record Pattern de Base
Si un record contient un autre record, vous pouvez déstructurer les deux en une seule fois :
record Address(String city, String country) {}
record Person(String name, Address address) {}
void printInfo(Object obj) {
switch (obj) {
case Person(String n, Address(String c, String co)) -> System.out.println(n + " lives in " + c + ", " + co);
default -> System.out.println("Unknown");
}
}
Dans l’exemple ci-dessus, le pattern Person inclut un pattern Address imbriqué.
Les deux sont appariés structurellement.
17.4.5.2 Nested Record Patterns avec var
Au lieu de spécifier des types exacts pour chaque champ, vous pouvez utiliser var dans le pattern pour laisser le compilateur inférer le type.
switch (obj) {
case Person(var name, Address(var city, var country)) -> System.out.println(name + " — " + city + ", " + country);
}
var dans les patterns fonctionne comme var dans les variables locales : cela signifie "inférer le type".
Warning
- Vous avez toujours besoin du type du record englobant (Person, Address) ;
- seuls les types des champs peuvent être remplacés par
var.
17.4.5.3 Nested Record Patterns et Generics
Les record patterns fonctionnent aussi avec des records génériques.
record Box<T>(T value) {}
record Wrapper(Box<String> box) {}
static void test(Object o) {
switch (o) {
case Wrapper(Box<String>(var v)) -> System.out.println("Boxed string: " + v);
default -> System.out.println("Something else");
}
}
Dans cet exemple :
- Le pattern exige exactement
Box<String>, pasBox<Integer>. - Dans le pattern,
var vcapture la valeur générique unboxed.
17.4.5.4 Erreurs Courantes avec les Nested Record Patterns
Structure de record non correspondante
// ❌ ERROR: pattern does not match record structure
case Person(var n, var city) -> ...
Person a 2 champs, mais l’un d’eux est un record. Vous devez déstructurer correctement.
Nombre incorrect de composants
// ❌ ERROR: Address has 2 components, not 1
case Person(var n, Address(var onlyCity)) -> ...
Incompatibilité générique
// ❌ ERROR: expecting Box<String> but found Box<Integer>
case Wrapper(Box<Integer>(var v)) -> ...
Placement illégal de var
// ❌ var cannot replace the record type itself
case var(Person(var n, var a)) -> ...
Note
varne peut pas remplacer l’ensemble du pattern, seulement les composants individuels.
17.5 Classes Imbriquées en Java
Java supporte plusieurs types de classes imbriquées — des classes déclarées à l’intérieur d’une autre classe.
Ce sont des outils fondamentaux pour l’encapsulation, l’organisation du code, les patterns d’event-handling et la représentation de hiérarchies logiques.
Une classe imbriquée appartient toujours à une classe englobante et a des règles particulières d’accessibilité et d’instanciation selon sa catégorie.
Java définit quatre types de classes imbriquées :
- Static Nested Classes – déclarées avec
staticà l’intérieur d’une autre classe. - Inner Classes (non-static nested classes).
- Local Classes – déclarées dans un bloc (méthode, constructeur ou initializer).
- Anonymous Classes – classes sans nom créées inline, généralement pour redéfinir une méthode ou implémenter une interface.
Warning
statics’applique uniquement aux classes membres imbriquées- Les classes
Top-level→ ne peuvent pas être static - Les classes
Local(déclarées dans les méthodes) → ne peuvent pas être static - Les classes
Anonymous→ ne peuvent pas être static - Une classe
static nestedne peut pas accéder aux membres d’instance sans une référence explicite à un objet externe.
17.5.1 Static Nested Classes
Une static nested class se comporte comme une classe top-level dont le namespace est à l’intérieur de sa classe englobante.
Elle ne peut pas accéder aux membres d’instance de la classe externe mais peut accéder aux membres statiques.
Elle ne conserve pas de référence vers une instance de la classe englobante.
Une classe imbriquée static peut contenir des variables membres non statiques.
17.5.1.1 Syntaxe et Règles d’Accès
- Déclarée via
static classà l’intérieur d’une autre classe. - Peut accéder uniquement aux membres static de la classe externe.
- N’a pas de référence implicite vers l’instance englobante.
- Peut être instanciée sans instance externe.
- Peut contenir des variables membres non statiques.
class Outer {
static int version = 1;
static class Nested {
void print() {
System.out.println("Version: " + version); // OK: accessing static member
}
}
}
class Test {
public static void main(String[] args) {
Outer.Nested n = new Outer.Nested(); // No Outer instance required
n.print();
}
}
17.5.1.2 Erreurs Courantes
- Les static nested classes ne peuvent pas accéder aux variables d’instance :
class Outer {
int x = 10;
static class Nested {
void test() {
// System.out.println(x); // ❌ Compile error
}
}
}
17.5.2 Inner Classes (Non-Static Nested Classes)
Une inner class est associée à une instance de la classe externe et peut accéder à tous les membres de la classe externe, y compris ceux private.
17.5.2.1 Syntaxe et Règles d’Accès
- Déclarée sans
static. - Possède une référence implicite vers l’instance englobante.
- Peut accéder aux membres statiques et aux membres d’instance de la classe externe.
- Comme elle n’est pas statique, elle doit être créée via une instance de la classe englobante.
class Outer {
private int value = 100;
class Inner {
void print() {
System.out.println("Value = " + value); // OK: accessing private
}
}
void make() {
Inner i = new Inner(); // OK inside the outer class
i.print();
}
}
class Test {
public static void main(String[] args) {
Outer o = new Outer();
Outer.Inner i = o.new Inner(); // MUST be created from an instance
i.print();
}
}
À l’intérieur d’une classe interne non-static, on peut faire référence à l’objet englobant en utilisant OuterClass.this.
L’expression InnerClass.this, qui est équivalente à this, fait référence à l’objet Inner courant.
- Exemple :
class Outer {
int x = 10;
class Inner {
int x = 20;
void print() {
System.out.println(x); // 20 (Inner.this.x)
System.out.println(this.x); // 20
System.out.println(Outer.this.x); // 10
}
}
}
17.5.2.2 Erreurs Courantes
- Les inner classes ne peuvent pas déclarer de membres statiques sauf les static final constants.
class Outer {
class Inner {
// static int x = 10; // ❌ Compile error
static final int OK = 10; // ✔ Allowed (constant)
}
}
Warning
- Instancier une inner class SANS instance externe est illégal.
17.5.3 Classes Locales
Une classe locale est une classe imbriquée définie à l’intérieur d’un bloc — le plus souvent une méthode.
Elle n’a pas de modificateur d’accès et n’est visible qu’à l’intérieur du bloc où elle est déclarée.
17.5.3.1 Caractéristiques
- Déclarée à l’intérieur d’une méthode, d’un constructeur ou d’un initializer.
- Peut accéder aux membres de la classe externe.
- Peut accéder aux variables locales si elles sont effectively final.
- Ne peut pas déclarer de membres statiques (sauf static final constants).
class Outer {
void compute() {
int base = 5; // must be effectively final
class Local {
void show() {
System.out.println(base); // OK
}
}
new Local().show();
}
}
Une classe locale, tout comme une classe interne membre, possède une référence implicite vers l’instance englobante via OuterClass.this.
Elle dispose également de LocalClass.this, équivalent à this, qui est valide à l’intérieur du corps de la classe locale.
- Exemple :
class Outer {
int x = 10;
void method() {
class Local {
void print() {
System.out.println(Outer.this.x); // ✔ valide
System.out.println(Local.this); // ✔ valide
}
}
}
}
17.5.3.2 Erreurs Courantes
basedoit être effectively final ; le modifier casse la compilation.
void compute() {
int base = 5;
base++; // ❌ Now base is NOT effectively final
class Local {}
}
17.5.4 Classes Anonymes
Une classe anonyme est une classe one-off créée inline, généralement pour implémenter une interface ou redéfinir une méthode sans nommer une nouvelle classe.
17.5.4.1 Syntaxe et Utilisation
- Créée via
new+ type + body. - Ne peut pas avoir de constructeurs (pas de nom).
- Souvent utilisée pour event handling, callbacks, comparators.
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Anonymous running");
}
};
17.5.4.2 Classe Anonyme qui Étend une Classe
Button b = new Button("Click");
b.onClick(new ClickHandler() {
@Override
public void handle() {
System.out.println("Handled!");
}
});
17.5.5 Comparaison des Types de Classes Imbriquées
Un tableau rapide qui résume tous les types de classes imbriquées.
| Type | A une Instance Externe ? | Peut Accéder aux Membres d’Instance Externe ? | Peut Avoir des Membres Statiques ? | Usage Typique |
|---|---|---|---|---|
| Static Nested | Non | Non | Oui | Namespacing, helpers |
| Inner Class | Oui | Oui | Non (sauf constantes) | Comportement lié à l’objet |
| Local Class | Oui | Oui | Non | Classes temporaires avec scope |
| Anonymous Class | Oui | Oui | Non | Personnalisation inline |
17.6 Imbrication des Interfaces en Java
En Java, une interface peut être déclarée à différents emplacements et suit des règles spécifiques concernant l’imbrication et les membres autorisés.
17.6.1 Où une Interface peut être Déclarée
Une interface peut être :
- Top-level (directement dans un package)
- Interface membre imbriquée (déclarée à l’intérieur d’une classe ou d’une autre interface)
- Interface locale ❌ (non autorisée)
- Interface anonyme ❌ (n’existe pas comme déclaration, seulement des implémentations anonymes)
En Java, il n’est pas permis de déclarer une interface locale (c’est-à-dire à l’intérieur d’une méthode ou d’un bloc).
Les interfaces peuvent être uniquement top-level ou member.
17.6.2 Interfaces Imbriquées
Une interface imbriquée peut être déclarée dans :
17.6.2.1 Interface Imbriquée dans une Classe
- Elle est implicitement
static - Elle ne peut pas être déclarée
non-static -
Elle peut être déclarée
public,protected,privateoupackage-private -
Exemple :
class Outer {
interface InnerInterface {
void test();
}
}
Le mot-clé static est implicite :
class Outer {
static interface InnerInterface { // autorisé mais redondant
void test();
}
}
17.6.2.2 Interface Imbriquée dans une autre Interface
- Elle est implicitement
publicetstatic - Elle ne peut pas être
privateouprotected
interface A {
interface B {
void test();
}
}
17.6.3 Règles dAccès
Une interface imbriquée :
- N’a pas de référence implicite à une instance de la classe englobante
- Ne peut pas accéder directement aux membres d’instance de la classe englobante
- Peut accéder uniquement aux membres
staticde la classe englobante
17.6.4 Types Imbriqués dans les Interfaces
Une interface peut contenir :
- Des classes imbriquées (implicitement
public static) - Des records imbriqués (implicitement
public static) - Des enums imbriqués (implicitement
public static) - D’autres interfaces imbriquées (implicitement
public static)
17.6.5 Résumé Essentiel
- Les interfaces imbriquées sont toujours
static - Les interfaces locales n’existent pas
- Les champs sont toujours
public static final - Les méthodes sont implicitement
public abstract(sauf default/static/private) - Elles peuvent contenir d’autres types imbriqués