En Java, les records servent à modéliser des objets de données compacts, mais leur rapport à l’héritage est souvent mal compris. La question derrière java record extends tient en une idée simple: un record ne s’étend pas comme une classe classique, et c’est précisément ce qui fait sa valeur. Je vais montrer ce que le langage autorise réellement, ce qu’il interdit, et surtout quelles alternatives utiliser quand on a besoin de réutilisation ou d’une hiérarchie de types.
L’essentiel à retenir avant de choisir un record
- Un record est implictement final et ne peut pas hériter d’une classe métier.
- Il peut en revanche implémenter des interfaces et récupérer leurs méthodes par défaut.
- Pour partager des données, la composition est souvent plus robuste que l’héritage.
- Les sealed classes permettent de garder une hiérarchie fermée tout en utilisant des records.
- Un record est surtout pertinent pour des objets de transport immuables, lisibles et simples à maintenir.
Pourquoi un record ne peut pas étendre une classe
Le point de départ est plus strict qu’on ne l’imagine: un record n’est pas une classe “normale” à laquelle on ajouterait un peu de syntaxe moderne. Le compilateur lui impose une forme fermée, avec ses composants, ses accesseurs et ses méthodes dérivées. En pratique, vous n’écrivez jamais d’extends sur un record, parce que le langage le considère comme final et le rattache à sa superclasse technique sans vous laisser la modifier.
Voici ce que beaucoup essaient de faire, puis découvrent au moment de compiler que ce n’est pas autorisé:
class Person {
String name;
}
record User(String name) extends Person { }
Le problème n’est pas seulement syntaxique. Il est conceptuel: si le comportement et l’état d’un record sont définis par sa liste de composants, l’ajout d’un parent métier introduirait un second niveau d’état hérité, ce qui casserait la simplicité du modèle. La documentation Oracle est très claire sur ce point: un record est conçu comme un conteneur de valeurs, pas comme un maillon d’une hiérarchie ouverte.
Autrement dit, si votre besoin principal est de réutiliser un état partagé entre plusieurs types, le record n’est probablement pas le bon outil. Une fois ce verrou compris, la vraie question devient ce qu’un record peut hériter sans trahir son modèle.
Ce que les records héritent vraiment
Un record n’hérite pas d’une classe métier, mais il n’est pas pour autant isolé. Il peut implémenter une ou plusieurs interfaces, et donc récupérer des méthodes par défaut. C’est souvent la bonne porte d’entrée quand vous voulez brancher un record sur un contrat fonctionnel sans lui faire porter une hiérarchie complète.
interface Auditable {
default String auditLabel() {
return "audit";
}
}
record Event(String id) implements Auditable { }
Dans ce cas, Event peut utiliser auditLabel() sans hériter d’un état supplémentaire. Ce détail compte beaucoup en backend: on garde un modèle de données propre, tout en branchant le type sur des comportements communs, comme la journalisation, la validation ou la sérialisation.
Il faut aussi garder en tête que les records héritent des règles communes aux classes Java, mais avec des contraintes plus serrées: pas de variables d’instance additionnelles, pas d’initialiseur d’instance, et une logique centrée sur leurs composants. Leur identité est donc dérivée des valeurs déclarées dans l’en-tête, pas d’un arbre d’héritage. C’est ce qui rend leurs equals, hashCode et toString si prévisibles.
À partir de là, le vrai sujet devient le design du modèle: composition, ou hiérarchie fermée.
Quand la composition vaut mieux que l’héritage
Dans la plupart des cas que je rencontre, la bonne réponse n’est pas “trouver une classe parente”, mais “découper correctement les données”. La composition colle très bien à l’esprit des records, parce qu’elle permet d’assembler plusieurs objets de valeur sans introduire de dépendance fragile entre eux.
record Address(String street, String city, String country) { }
record Customer(String id, String name, Address address) { }
Cette approche a un avantage simple: chaque type reste lisible et remplaçable. Si l’adresse évolue, vous modifiez l’objet de valeur Address au lieu de faire grossir une classe mère. Sur un backend, c’est souvent plus sain qu’une hiérarchie “héritée” qui finit par mélanger des responsabilités trop différentes.
Il y a toutefois un piège fréquent: un record est shallowly immutable, donc immuable en surface seulement. Si vous placez une collection mutable dans un composant, le record protège la référence, pas forcément le contenu. Dans ce cas, je préfère figer les données dès le constructeur canonique:
import java.util.List;
record Report(List tags) {
Report {
tags = List.copyOf(tags);
}
}
Cette petite discipline évite beaucoup de bugs silencieux, surtout quand l’objet circule entre plusieurs couches applicatives. Quand le domaine a plusieurs variantes connues à l’avance, les records peuvent alors revenir dans le jeu via les types scellés.
Les hiérarchies fermées où les records brillent
Si vous avez réellement besoin de polymorphisme, la combinaison la plus propre n’est pas “record + extends”, mais sealed class ou sealed interface avec plusieurs records comme sous-types permis. Là, le record garde sa nature fermée, mais il participe à une hiérarchie contrôlée. C’est une bonne solution pour des modèles de domaine comme des paiements, des commandes ou des événements métier.
sealed interface PaymentMethod permits CardPayment, BankTransfer { }
record CardPayment(String maskedPan) implements PaymentMethod { }
record BankTransfer(String iban) implements PaymentMethod { }
Ce style a deux intérêts concrets. D’abord, il empêche l’ajout sauvage de nouveaux sous-types dans le codebase. Ensuite, il rend les traitements plus sûrs, parce que le compilateur peut mieux raisonner sur l’ensemble des cas possibles. Dans les versions modernes de Java, cela facilite aussi les traitements exhaustifs sur une hiérarchie fermée.
Autrement dit, si votre besoin est de représenter “plusieurs formes d’une même chose”, une hiérarchie scellée fait souvent mieux le travail qu’un héritage classique. Les records y apportent la concision; le sealed type apporte le cadre. Cette combinaison est plus prévisible qu’une base abstraite ouverte à tous les sous-types.
Les erreurs que je vois le plus souvent avec les records
Le premier contresens consiste à utiliser un record pour un objet qui doit vivre longtemps, muter souvent ou s’intégrer à un framework qui attend une entité classique. Un record est excellent pour un DTO, un message, une projection ou un objet de valeur; il est souvent moins pertinent pour une entité riche en cycle de vie. Quand le modèle exige du comportement mutable ou des proxys, la classe classique reste plus réaliste.
Le deuxième piège est de croire qu’un record “s’occupe de tout”. Il génère bien une bonne partie du code répétitif, mais il ne remplace pas la modélisation. Vous devez toujours décider où valider les données, comment figer les collections, et si les composants sont réellement de bons candidats pour un objet de valeur.
Le troisième piège, plus subtil, est de surcharger trop vite les méthodes générées. Si vous réécrivez toString ou les accesseurs sans raison précise, vous perdez une partie de la cohérence et de la lisibilité qui font l’intérêt du mécanisme. Je ne dis pas qu’il ne faut jamais les personnaliser; je dis qu’il faut le faire pour une vraie règle métier, pas pour reprendre la main “par habitude”.
| Besoin réel | Choix le plus adapté | Pourquoi |
|---|---|---|
| Transport de données immuables | Record | Moins de boilerplate, identité claire, modèle compact |
| Partage d’état et de comportement commun | Classe classique | Héritage réel, état extensible, plus de liberté |
| Famille fermée de variantes | Sealed interface ou sealed class + records | Polymorphisme contrôlé sans ouverture excessive |
| Réutilisation de données sans héritage | Composition | Moins de couplage, meilleure lisibilité, maintenance plus simple |
Ces erreurs reviennent parce qu’on traite parfois le record comme une “classe plus courte”, alors que c’est plutôt un contrat de conception plus précis. La bonne décision tient donc moins au style qu’au rôle réel de l’objet dans votre architecture.
La règle pratique que j’applique sur un backend Java
Quand je conçois un modèle métier, je pars d’une règle simple: si l’objet représente surtout une donnée stable, je prends un record; si l’objet doit partager de l’état ou de la logique héritée, je prends une classe; si je veux une hiérarchie fermée, je combine sealed et records. Cette discipline évite beaucoup de code inutile et réduit les surprises au moment de faire évoluer l’application.
- Record pour les DTO, messages, projections et objets de valeur.
- Classe classique pour les entités riches, mutables ou fortement comportementales.
- Composition dès qu’il s’agit de réutiliser des morceaux de données sans parent commun.
- Sealed + records quand vous voulez un polymorphisme fermé et lisible.
Si je devais résumer la décision en une phrase, je dirais ceci: un record n’est pas fait pour hériter, il est fait pour dire clairement ce qu’il contient. C’est précisément cette limite qui le rend fiable, lisible et très efficace dans un backend moderne.