Chapitre 9. Méthodes par défaut
Ce chapitre couvre
- Définition des méthodes par défaut
- Évolution des API de manière compatible
- Modèles d’utilisation des méthodes par défaut
- Règles de résolution de types
Traditionnellement, une interface Java regroupe des méthodes connexes dans un contrat. Toute classe qui implémente une interface doit fournir une implémentation pour chaque méthode définie par l’interface ou hériter de l’implémentation d’une superclasse. Mais cela pose un problème lorsque les concepteurs de bibliothèques doivent mettre à jour une interface pour ajouter une nouvelle méthode. En effet, les classes concrètes existantes (qui peuvent ne pas être sous leur contrôle) doivent être modifiées pour refléter le nouveau contrat d’interface. Ceci est particulièrement problématique car l’API Java 8 introduit de nombreuses nouvelles méthodes sur les interfaces existantes, telles que la méthode de sort sur l’interface List que vous avez utilisée dans les chapitres précédents. Imaginez le ressenti de tous ceux responsable de la maintenance des frameworks de collection tels que Guava et Apache Commons qui ont maintenant besoin de modifier toutes les classes implémentant l’interface List pour fournir une implémentation pour la méthode sort aussi.
A cet effet, Java 8 introduit un nouveau mécanisme pour résoudre ce problème. Cela peut sembler surprenant, mais les interfaces dans Java 8 peuvent désormais déclarer des méthodes avec un code d’implémentation ; Cela peut arriver de deux façons. D’abord, Java 8 permet des méthodes statiques à l’intérieur des interfaces. Deuxièmement, Java 8 introduit une nouvelle fonctionnalité appelée méthodes par défaut qui vous permet de fournir une implémentation par défaut pour les méthodes dans une interface. En d’autres termes, les interfaces peuvent fournir une implémentation concrète pour les méthodes. Par conséquent, les classes existantes implémentant une interface hériteront automatiquement des implémentations par défaut si elles n’en fournissent pas explicitement. Cela vous permet de faire évoluer des interfaces de manière non intrusive. Vous avez toujours utilisé plusieurs méthodes par défaut. Deux exemples que vous avez vus sont la méthode sort dans l’interface List et stream dans l’interface de collection.
La méthode de tri de l’interface List que vous avez vue au chapitre 1 est nouvelle pour Java 8 et est définie comme ceci:
Notez le nouveau modificateur default avant le type de retour. C’est ainsi que vous pouvez dire qu’une méthode est une méthode par défaut. Ici, la méthode de tri appelle la méthode Collections.sort pour effectuer le tri. Grâce à cette nouvelle méthode, vous pouvez trier une liste en appelant directement la méthode :
Il y a autre chose de nouveau dans ce code. Notez que vous appelez la méthode Comparator.naturalOrder. C’est une nouvelle méthode statique dans l’interface Comparator qui renvoie un objet Comparator pour trier les éléments dans l’ordre naturel (le tri alphanumérique standard).
La méthode stream dans Collection que vous avez vu au chapitre 4 ressemble à ceci :
Ici, la méthode stream, que vous avez largement utilisée dans les chapitres précédents pour traiter les collections, appelle la méthode StreamSupport.stream pour renvoyer un flux. Remarquez comment le corps de la méthode stream appelle la méthode, qui est également une méthode par défaut de l’interface de collection.
Les interfaces sont-elles comme des classes abstraites maintenant ? Oui et non ; il y a des différences fondamentales, que nous expliquons dans ce chapitre. Mais plus important, pourquoi devriez-vous vous soucier des méthodes par défaut ? Les principaux utilisateurs des méthodes par défaut sont les concepteurs de bibliothèques. Comme nous l’expliquons plus loin, des méthodes par défaut ont été introduites pour faire évoluer les bibliothèques telles que l’API Java de manière compatible, comme l’illustre la figure 9.1.
En un mot, l’ajout d’une méthode à une interface est la source de nombreux problèmes ; les classes existantes implémentant l’interface doivent être modifiées pour fournir une implémentation pour la méthode. Si vous contrôlez l’interface et toutes les implémentations, alors ce n’est pas si mal. Mais ce n’est souvent pas le cas. C’est la motivation des méthodes par défaut : elles permettent aux classes d’hériter automatiquement d’une implémentation par défaut à partir d’une interface.
Donc, si vous êtes un concepteur de bibliothèque, ce chapitre est important car les méthodes par défaut fournissent un moyen de faire évoluer les interfaces sans provoquer de modifications aux implémentations existantes. En outre, comme nous l’expliquons plus loin dans le chapitre, les méthodes par défaut peuvent aider à structurer vos programmes en fournissant un mécanisme flexible pour l’héritage multiple du comportement : une classe peut hériter des méthodes par défaut de plusieurs interfaces. Par conséquent, vous pouvez toujours être intéressé par les méthodes par défaut, même si vous n’êtes pas un concepteur de bibliothèque.
Les méthodes statiques et les interfaces
Un modèle commun en Java consiste à définir à la fois une interface et une classe utilitaire définissant de nombreuses méthodes statiques pour travailler avec des instances de l’interface. Par exemple, Collections est une classe utilitaire pour gérer les objets Collection. Maintenant que les méthodes statiques peuvent exister à l’intérieur des interfaces, ces classes d’utilitaire dans votre code peuvent disparaître et leurs méthodes statiques peuvent être déplacées à l’intérieur d’une interface. Ces classes resteront dans l’API Java afin de préserver la compatibilité ascendante.
Le chapitre est structuré comme suit. Nous vous présenterons d’abord un cas d’utilisation de l’évolution d’une API et les problèmes qui peuvent survenir. Nous expliquerons ensuite quelles sont les méthodes par défaut et comment elles peuvent résoudre les problèmes rencontrés dans des cas d’utilisation. Ensuite, nous montrerons comment vous pouvez créer vos propres méthodes par défaut pour obtenir une forme d’héritage multiple en Java. Nous concluons avec quelques informations plus techniques sur la façon dont le compilateur Java résout les ambiguïtés possibles lorsqu’une classe hérite de plusieurs méthodes par défaut avec la même signature.
9.1. API évolutives
P
Pour comprendre pourquoi il est difficile de faire évoluer une API une fois qu’elle a été publiée, disons dans le cadre de cette section que vous êtes le concepteur d’une bibliothèque de dessin Java populaire. Votre bibliothèque contient une interface Resizable qui définit de nombreuses méthodes qu’une classe concrète doit prendre en charge : setHeight, setWidth, getHeight, getWidth et setAbsoluteSize. En outre, vous lui fournissez plusieurs implémentations prêtes à l’emploi, telles que Square et Rectangle. Parce que votre bibliothèque est si populaire, vous avez des utilisateurs qui ont créé leurs propres implémentations intéressantes telles que Ellipse en utilisant votre interface Resizable.
Quelques mois après la publication de votre API, vous réalisez que Resizable ne dispose pas de certaines fonctionnalités. Par exemple, ce serait bien si l’interface avait une méthode setRelativeSize qui prend comme argument un facteur de croissance pour redimensionner une forme. Vous pouvez dire que c’est facile à corriger : ajoutez simplement la méthode setRelativeSize à Resizable et mettez à jour vos implémentations de Square et Rectangle. Eh bien, pas si vite. Qu’en est-il de tous vos utilisateurs qui ont créé leurs propres implémentations de l’interface Resizable ? Malheureusement, vous n’avez pas accès et ne pouvez pas modifier leurs propres implémentations de cette interface. C’est le même problème auquel sont confrontés les concepteurs de bibliothèques Java lorsqu’ils doivent faire évoluer l’API Java. Regardons un exemple en détail pour voir les conséquences de la modification d’une interface qui a déjà été publiée.
9.1.1. API version 1
La première version de votre interface Resizable a les méthodes suivantes :
Implémentation utilisateur
L’un de vos utilisateurs les plus fidèles décide de créer sa propre implémentation de Resizable appelé Ellipse:
Il a créé un jeu qui traite différents types de formes redimensionnables (y compris sa propre Ellipse):
9.1.2. API version 2
Après que votre bibliothèque ait été utilisée pendant quelques mois, vous recevez de nombreuses demandes pour mettre à jour vos implémentations de Resizable: Square, Rectangle, etc. pour prendre en charge la méthode setRelativeSize. Donc, vous venez avec la version 2 de votre API, comme montré ici et illustré dans la figure 9.2:
Figure 9.2. Faire évoluer une API en ajoutant une méthode à Resizable. Recompiler l’application produit des erreurs car elle dépend de l’interface Resizable.
Problèmes pour vos utilisateurs
Cette mise à jour de Resizable crée quelques problèmes. Premièrement, l’interface nécessite maintenant une implémentation de setRelativeSize. Mais l’implémentation Ellipse que votre utilisateur a créée n’implémente pas la méthode setRelativeSize. L’ajout d’une nouvelle méthode à une interface est compatible avec les binaires ; cela signifie que les implémentations de fichiers de classes existantes seront toujours exécutées sans l’implémentation de la nouvelle méthode, s’il n’y a pas de tentative de les recompiler. Dans ce cas, le jeu sera toujours exécuté (à moins qu’il ne soit recompilé) malgré l’ajout de la méthode setRelativeSize à l’interface Resizable. Néanmoins, l’utilisateur peut modifier la méthode Utils.paint dans son jeu pour utiliser la méthode setRelativeSize car la méthode paint attend une liste d’objets Resizable en argument. Si un objet Ellipse est passé, une erreur sera levée au moment de l’exécution car la méthode setRelativeSize n’est pas implémentée :
Deuxièmement, si l’utilisateur tente de re builder l’intégralité de son application (y compris Ellipse), il obtiendra l’erreur de compilation suivante:
Par conséquent, la mise à jour d’une API publiée crée des incompatibilités. C’est pourquoi l’évolution des API existantes, telles que l’API Java Collections officielle, pose des problèmes aux utilisateurs des API. Il existe des alternatives à l’évolution d’une API, mais ce sont de mauvais choix. Par exemple, vous pouvez créer une version distincte de votre API et conserver à la fois l’ancienne et la nouvelle version, mais cela n’est pas pratique pour plusieurs raisons. Tout d’abord, ce sera plus complexe pour vous de maintenir les deux versions en tant que concepteur de bibliothèque. Deuxièmement, vos utilisateurs peuvent devoir utiliser les deux versions de votre API dans la même base de code, ce qui a un impact sur l’espace mémoire et le temps de chargement car davantage de fichiers de classe sont requis pour leurs projets.
C’est là que les méthodes par défaut viennent à la rescousse. Elles permettent aux concepteurs de bibliothèques de développer des API sans casser le code existant car les classes implémentant une interface mise à jour héritent automatiquement d’une implémentation par défaut.
Différents types de compatibilités: binaire, source et comportemental
Il existe trois principaux types de compatibilité lors de l’introduction d’un changement dans un programme Java : les compatibilités binaires, des sources et celles comportementales. Vous avez vu que l’ajout d’une méthode à une interface est compatible au niveau des binaires mais entraîne une erreur de compilation si la classe implémentant l’interface est recompilé. Il est bon de connaître les différents types de compatibilités, examinons-les plus en détail.
La compatibilité binaire signifie que les binaires existants qui s’exécutent sans erreur continuent de s’exécuter (ce qui implique la vérification, la préparation et la résolution) sans erreur après l’introduction de la modification. Par exemple, l’ajout d’une méthode à une interface est compatible avec les binaires car si elle n’est pas appelée, les méthodes existantes de l’interface peuvent toujours fonctionner sans problème.
Dans sa forme la plus simple, la compatibilité des sources signifie qu’un programme existant sera toujours compilé après l’introduction d’un changement. Par exemple, l’ajout d’une méthode à une interface n’est pas compatible avec les sources. Les implémentations existantes ne seront pas recompilées car elles doivent implémenter la nouvelle méthode.
Enfin, la compatibilité comportementale signifie que l’exécution d’un programme après un changement avec les mêmes entrées entraîne le même comportement. Par exemple, l’ajout d’une méthode à une interface est compatible avec le comportement parce que la méthode n’est jamais appelée dans le programme.
9.2. Les méthodes par défaut en quelques mots
Vous avez vu comment l’ajout de méthodes à une API publiée perturbe les implémentations existantes. Les méthodes par défaut sont une nouvelle fonctionnalité ajoutée dans Java 8 pour aider à faire évoluer les API d’une manière compatible. Une interface peut maintenant contenir des signatures de méthodes pour lesquelles une classe l’implémentant ne fournit pas d’implémentations. L’implémentation de ces méthodes est faite directement dans les interfaces (donc des méthodes par défaut elle même) plutôt que dans les classes d’implémentation.
Alors, comment reconnaissez-vous une méthode par défaut ? C’est très simple. Elle commence par le modificateur défault et contient un corps comme une méthode déclarée dans une classe. Par exemple, dans le contexte d’une bibliothèque de collections, vous pouvez définir une interface Sized avec une méthode abstraite size et une méthode par défaut isEmpty comme ceci :
Maintenant, toute classe qui implémente l’interface Sized héritera automatiquement de l’implémentation de isEmpty. Par conséquent, l’ajout d’une méthode à une interface avec une implémentation par défaut n’est pas une incompatibilité de source.
Revenons à notre exemple initial de la bibliothèque de dessin Java et de votre jeu. Concrètement, pour faire évoluer votre bibliothèque de manière compatible (ce qui signifie que les utilisateurs de votre bibliothèque n’ont pas besoin de modifier toutes leurs classes implémentant Resizable), utilisez une méthode par défaut et fournissez une implémentation par défaut pour setRelativeSize :
Parce que les interfaces peuvent maintenant avoir des méthodes avec implémentation, cela signifie-t-il que l’héritage multiple est arrivé en Java ? Que se passe-t-il si une classe d’implémentation définit également la même signature de méthode ou si les méthodes par défaut étaient surchargées ? Ne vous inquiétez pas de ces problèmes pour l’instant ; Il y a quelques règles à suivre et des mécanismes à votre disposition pour régler ces problèmes. Nous les explorerons en détail dans la section 9.5.
Vous avez peut-être deviné que les méthodes par défaut sont largement utilisées dans l’API Java 8. Vous avez vu dans l’introduction de ce chapitre que la méthode stream dans l’interface de collection que nous avons largement utilisée dans les chapitres précédents est une méthode par défaut. La méthode de tri de l’interface List est également une méthode par défaut. La plupart des interfaces fonctionnelles présentées au chapitre 3 telles que Predicate, Function et Comparator ont également introduit de nouvelles méthodes par défaut telles que Predicate.and ou Function.andThen (rappelez-vous qu’une interface fonctionnelle ne contient qu’une seule méthode abstraite, les méthodes par défaut sont non-abstraites méthodes).
Classes abstraites et interfaces dans Java 8
Alors, quelle est la différence entre une classe abstraite et une interface ? Elles peuvent toutes deux contenir des méthodes abstraites et des méthodes avec un corps.
Tout d’abord, une classe peut étendre uniquement une classe abstraite, mais une classe peut implémenter plusieurs interfaces.
Deuxièmement, une classe abstraite peut imposer un état à travers des variables d’instance (champs). Une interface ne peut pas avoir de variables d’instance.
Pour vérifier votre compréhension des méthodes par défaut, allez au Quiz 9.1.
Quiz 9.1: removeIf
Pour ce quiz, prétendez que vous êtes l’un des maîtres du langage et de l’API Java. Vous avez reçu de nombreuses demandes pour une méthode removeIf à utiliser sur ArrayList, TreeSet, LinkedList et toutes les autres collections. La méthode removeIf doit supprimer tous les éléments d’une collection qui correspondent à un prédicat donné. Votre tâche dans ce quizz est de trouver la meilleure façon d’améliorer l’API Collections avec cette nouvelle méthode.
Réponse :
Quel est le moyen le plus perturbant d’améliorer l’API Collections ? Vous pouvez copier et coller l’implémentation de removeIf dans chaque classe concrète de l’API Collections, mais ce serait un crime pour la communauté Java. Que pouvez vous faire d’autre ? Eh bien, toutes les classes de collection implémentent une interface appelée java.util.Collection. Vous venez d’apprendre que les méthodes par défaut sont un moyen d’ajouter des implémentations à l’intérieur d’une interface de manière compatible avec les sources. Et toutes les classes implémentant Collection (y compris les classes de vos utilisateurs qui ne font pas partie de l’API Collections) pourront utiliser l’implémentation de removeIf. La solution de code pour removeIf est la suivante (qui est à peu près l’implémentation dans l’API Java 8 Collections officielle). C’est une méthode par défaut dans l’interface Collection :
9.3. Modèles d’utilisation pour les méthodes par défaut
Vous avez vu comment les méthodes par défaut peuvent être utiles pour faire évoluer une bibliothèque de manière compatible. Y at-il autre chose que vous pouvez faire avec elles ? Vous pouvez également créer vos propres interfaces avec des méthodes par défaut. Vous pouvez le faire pour deux cas d’utilisation que nous explorons dans cette section : les méthodes optionnelles et l’héritage multiple du comportement.
9.3.1. Méthodes optionnelles
Il est probable que vous ayez rencontré des classes qui implémentent une interface mais qui laissent certaines implémentations de méthode vides. Prenez, par exemple, l’interface Iterator. Elle définit les méthodes hasNext et next mais aussi la méthode remove. Avant Java 8, la suppression était souvent ignorée car l’utilisateur décidait de ne pas utiliser cette fonctionnalité. Par conséquent, de nombreuses classes implémentant Iterator ont une implémentation vide pour la suppression, ce qui entraîne un code standard inutile.
Avec les méthodes par défaut, vous pouvez fournir une implémentation par défaut pour ces méthodes, de sorte que les classes concrètes n’ont pas besoin de fournir explicitement une implémentation vide. Par exemple, l’interface Iterator dans Java 8 fournit une implémentation par défaut pour remove comme ceci :
Par conséquent, vous pouvez réduire le code standard. Toute classe implémentant l’interface Iterator n’a plus besoin de déclarer une méthode remove vide pour l’ignorer, car elle a maintenant une implémentation par défaut.
9.3.2. Héritage multiple du comportement
Les méthodes par défaut permettent quelque chose qui n’était pas possible de manière élégante auparavant : l’héritage multiple du comportement. C’est la capacité d’une classe à réutiliser le code à partir de plusieurs endroits, comme illustré dans la figure 9.3.
Figure 9.3. Héritage unique vs héritage multiple
Rappelez-vous que les classes en Java peuvent hériter d’une seule autre classe, mais les classes ont toujours été autorisées à implémenter plusieurs interfaces. Pour confirmer, voici comment la classe ArrayList est définie dans l’API Java :
Héritage multiple des types
Ici, ArrayList étend une classe et implémente six interfaces. Par conséquent, une ArrayList est un sous-type direct de sept types : AbstractList, List, RandomAccess, Cloneable, Serializable, Iterable et Collection. Donc, dans un sens, nous avons déjà plusieurs héritages de types.
Comme les méthodes d’interface peuvent avoir des implémentations dans Java 8, les classes peuvent hériter du comportement (code d’implémentation) de plusieurs interfaces. Examinons un exemple pour voir comment vous pouvez utiliser cette capacité à votre avantage. Garder les interfaces minimales et orthogonales vous permet d’obtenir une réutilisation et une composition du comportement à l’intérieur de votre code.
Interfaces minimales avec des fonctionnalités orthogonales
Disons que vous devez définir plusieurs formes avec des caractéristiques différentes pour le jeu que vous êtes entrain de créer. Certaines formes devraient être redimensionnables mais non rotatives ; certaines devraient être rotatives et mobiles mais pas redimensionnables. Comment pouvez-vous obtenir une bonne réutilisation du code ?
Vous pouvez commencer par définir une interface rotative autonome avec deux méthodes abstraites, setRotationAngle et getRotationAngle. L’interface déclare également une méthode rotateBy par défaut qui peut être implémentée en utilisant les méthodes setRotationAngle et get-RotationAngle comme ceci:
Cette technique est quelque peu liée au design pattern template où un algorithme (squelette de code) est défini à partir d’autres méthodes qui doivent être implémentées.
Maintenant, toute classe qui implémente Rotatable devra fournir une implémentation pour setRotationAngle et getRotationAngle mais héritera de l’implémentation par défaut de rotateBy.
De même, vous pouvez définir les deux interfaces, Moveable et Redimensionnable, que vous avez vues précédemment. Elles contiennent toutes les deux des implémentations par défaut. Voici le code pour Moveable :
Et voici le code pour Resizable:
Composer des interfaces
Vous pouvez maintenant créer différentes classes concrètes pour votre jeu en composant ces interfaces. Par exemple, les monstres peuvent être mobiles, rotatifs et redimensionnables :
La classe Monster héritera automatiquement des méthodes par défaut des interfaces Rotatable, Moveable et Resizable. Dans ce cas, Monster hérite des implémentations de rotateBy, moveHorizontally, moveVertically et setRelativeSize.
Vous pouvez maintenant appeler les différentes méthodes directement :
Supposons que vous deviez déclarer une autre classe mobile et rotative, mais non redimensionnable, comme le soleil. Il n’y a pas besoin de copier et coller du code ; vous pouvez réutiliser les implémentations par défaut à partir des interfaces Moveable et Rotatable comme indiqué ici. La Figure 9.4 illustre le diagramme UML de ce scénario :
Voici un autre avantage de la définition d’interfaces avec des implémentations par défaut comme celles de votre jeu. Disons que vous devez modifier l’implémentation de moveVertically pour le rendre plus efficace. Vous pouvez maintenant modifier son implémentation directement dans l’interface Moveable, et toutes les classes l’implémentant hériteront automatiquement du code (à condition qu’ils n’aient pas implémenté la méthode eux-mêmes).
L’héritage considéré comme nuisible
L’héritage ne devrait pas être votre réponse à tout quand il s’agit de réutiliser le code. Par exemple, hériter d’une classe qui a 100 méthodes et champs juste pour réutiliser une méthode est une mauvaise idée, car elle ajoute une complexité inutile. Il vaudrait mieux utiliser la délégation : créer une méthode qui appelle directement la méthode de la classe dont vous avez besoin via une variable membre. C’est pourquoi vous trouverez parfois des classes qui sont déclarées « finales » intentionnellement : elles ne peuvent pas être héritées pour empêcher ce type d’antipattern ou avoir leur comportement de base perturbé. Par exemple, String est final car nous ne voulons pas que quiconque puisse interférer avec de ses fonctionnalités.
La même idée est applicable aux interfaces avec des méthodes par défaut. En gardant votre interface minimale, vous pouvez obtenir une meilleure composition car vous ne pouvez sélectionner que les implémentations dont vous avez besoin.
Vous avez vu que les méthodes par défaut sont utiles pour de nombreux modèles d’utilisation. Mais voici quelques pistes de réflexion : que se passe-t-il si une classe implémente deux interfaces ayant la même signature de méthode par défaut ? Quelle méthode la classe est-elle autorisée à utiliser ? Nous explorerons ce problème dans la section suivante.
9.4. Règles de résolution
Comme vous le savez, en Java, une classe peut étendre une seule classe parente mais implémenter plusieurs interfaces. Avec l’introduction des méthodes par défaut dans Java 8, il est possible qu’une classe hérite de plus d’une méthode avec la même signature. Quelle version de la méthode devrait être utilisée ? De tels conflits seront probablement assez rares dans la pratique, mais quand ils se produisent, il doit y avoir des règles qui spécifient comment gérer le conflit. Cette section explique comment le compilateur Java résout de tels conflits potentiels. Nous cherchons à répondre à des questions telles que « Dans le code qui suit, quelle méthode hello est appelée par C ? » Notez que les exemples qui suivent sont destinés à explorer des scénarios problématiques ; cela ne signifie pas que de tels scénarios se produiront fréquemment dans la pratique :
En outre, vous avez peut-être entendu parler du problème du diamant dans C ++ où une classe peut hériter de deux méthodes ayant la même signature. Laquelle est choisie ? Java 8 fournit également des règles de résolution pour ce problème.
9.4.1. Trois règles de résolution à connaître
Il y a trois règles à suivre lorsqu’une classe hérite d’une méthode avec la même signature à partir de plusieurs endroits (comme une autre classe ou interface) :
- La classe gagne toujours. Une déclaration de méthode dans la classe ou une superclasse est prioritaire sur toute déclaration de méthode par défaut.
- Sinon, l’interface la plus spécifique est celle qui l’emporte : la méthode avec la même signature dans l’interface. (Si B étends A, B est plus spécifique que A).
- En outre, si le choix est toujours ambigu, la classe héritière de plusieurs interfaces doit explicitement choisir l’implémentation de la méthode par défaut à utiliser en remplaçant et en appelant explicitement la méthode désirée.
Ce sont des règles qui vous devez vraiment savoir. Regardons maintenant quelques exemples.
9.4.2. La plupart des interfaces spécifiques fournissant l’interface par défaut
Revenons à notre exemple du début de cette section où C implémente à la fois B et A, qui définissent une méthode par défaut appelée hello. De plus, B étend A. La figure 9.5 fournit un diagramme UML pour le scénario.
Figure 9.5. L’interface fournissant les paramètres par défaut la plus spécifique gagne.
Quelle déclaration de la méthode Hello sera utilisée par le compilateur ? La règle 2 indique que la méthode avec l’interface par défaut la plus spécifique est sélectionnée. Parce que B est plus spécifique que A, le salut de B est sélectionné. Par conséquent, le programme affichera « Bonjour de B. »
Considérons maintenant ce qui se passerait si C héritait de D comme ceci (illustré à la figure 9.6) :
Figure 9.6. Hériter d’une classe et implémenter deux interfaces
La Règle 1 indique qu’une déclaration de méthode dans la classe est prioritaire. Mais la classe D n’override pas hello ; elle implémente l’interface A. Par conséquent, elle a une méthode par défaut à partir de l’interface A. La Règle 2 indique que s’il n’y a pas de méthodes dans la classe ou la superclasse, alors la méthode par défaut située dans l’interface la plus spécifique est sélectionnée. Le compilateur a donc le choix entre la méthode hello de l’interface A et la méthode hello de l’interface B. Parce que B est plus spécifique, le programme affichera à nouveau « Hello from B ». Pour vérifier votre compréhension des règles de résolution, essayez le questionnaire 9.2.
Quiz 9.2: Rappelez-vous les règles de résolution
Pour ce quiz, réutilisons l’exemple précédent, sauf que D override explicitement la méthode hello de A. Que pensez-vous que vous allez imprimer?
Réponse :
Le programme affichera « Hello from D » car une déclaration de méthode provenant d’une superclasse a la priorité, comme indiqué par la règle 1.
Notez que si D avait été déclaré comme ceci,
alors C serait forcé d’implémenter lui-même la méthode hello, même si les implémentations par défaut existent ailleurs dans la hiérarchie.
9.4.3. Conflits et désambiguïsation explicite
Les exemples que vous avez vus jusqu’à présent pourraient être résolus en utilisant les deux premières règles de résolution. Disons maintenant que B n’étends plus A (illustré à la figure 9.7) :
Figure 9.7. Implémentation de deux interfaces
La règle 2 ne vous aide pas maintenant car il n’y a pas d’interface plus spécifique à sélectionner. Les deux méthodes hello de A et B pourraient être des options valides. Ainsi, le compilateur Java produira une erreur de compilation car il ne sait pas quelle méthode est la plus appropriée : “Error : class C inherits unrelated defaults for hello() from types B and A.”
Résoudre le conflit
Il n’y a pas beaucoup de solutions pour résoudre le conflit entre les deux méthodes. Elles sont toutes les 2 valides ; vous devez décider explicitement quelle déclaration de méthode vous voulez que C utilise. Pour ce faire, vous pouvez redéfinir la méthode hello dans la classe C puis, dans son corps, appeler explicitement la méthode que vous souhaitez utiliser. Java 8 introduit la nouvelle syntaxe X.super.m (…) où X est la superinterface dont vous voulez appeler la méthode. Par exemple, si vous voulez que C utilise la méthode par défaut de B, cela ressemblera à ceci :
Essayez le Quiz 9.3 pour aborder un autre cas délicat.
Quiz 9.3: Presque la même signature
Pour ce quiz, supposons que les interfaces A et B sont déclarées comme suit:
Et la classe C est déclarée comme ceci:
Réponse :
C ne peut pas distinguer quelle méthode de A ou B est plus spécifique. C’est pourquoi la classe C ne compilera pas.
9.4.4. Problème de diamant
Considérons un scénario final qui envoie toujours autant de frissons à travers la communauté C ++:
La Figure 9.8 illustre le diagramme UML de ce scénario. C’est ce qu’on appelle un problème de diamant parce que la forme du diagramme ressemble à un diamant. Alors, quelle déclaration de méthode par défaut D hérite – celle de B ou celle de C ? Il n’y a en fait qu’une seule déclaration de méthode à choisir. Seul A déclare une méthode par défaut. Parce que l’interface est une superinterface de D, le code affichera « Hello from A. »
Figure 9.8. Le problème du diamant
Que se passe-t-il si B a aussi une méthode hello par défaut avec la même signature ? La Règle 2 indique que vous sélectionnez l’interface fournisseur par défaut la plus spécifique. Parce que B est plus spécifique que A, la déclaration de méthode par défaut de B sera sélectionnée. Si B et C déclarent une méthode hello avec la même signature, vous avez un conflit et vous devez le résoudre explicitement, comme nous l’avons montré plus haut.
Juste une remarque, vous pouvez vous demander ce qui se passe si vous ajoutez une méthode hello abstraite (une qui n’est pas par défaut) dans l’interface C comme suit (toujours pas de méthodes dans A et B) :
La nouvelle méthode hello abstraite en C a priorité sur la méthode hello par défaut de l’interface A car C est plus spécifique. Par conséquent, la classe D doit maintenant fournir une implémentation explicite pour hello ; sinon le programme ne compilera pas.
Problème de diamant C ++
Le problème du diamant est plus compliqué en C ++. D’abord, C ++ permet l’héritage multiple des classes. Par défaut, si une classe D hérite des classes B et C et que les classes B et C héritent toutes deux de A, la classe D aura alors accès à une copie d’un objet B et à une copie d’un objet C. En conséquence, les utilisations des méthodes de A doivent être explicitement qualifiées : proviennent-elles de B ou proviennent-elles de C ? De plus, les classes ont un état, de sorte que la modification des variables membres de B ne soit pas répercutée sur la copie de l’objet C.
Vous avez vu que le mécanisme de résolution de la méthode par défaut est assez simple si une classe hérite de plusieurs méthodes avec la même signature. Vous avez juste besoin de suivre systématiquement trois règles pour résoudre tous les conflits possibles :
- Tout d’abord, une déclaration de méthode explicite dans la classe ou une superclasse est prioritaire sur toute déclaration de méthode par défaut.
- Dans le cas contraire, la méthode par défaut avec la même signature dans l’interface la plus spécifique est sélectionnée.
- Enfin, s’il y a encore un conflit, vous devez explicitement remplacer les méthodes par défaut et choisir celle que votre classe devrait utiliser.
9.5. Résumé
Voici les concepts clés que vous devriez retenir de ce chapitre :
- Les interfaces dans Java 8 peuvent avoir un code d’implémentation par le biais de méthodes par défaut et de méthodes statiques.
- Les méthodes par défaut commencent par un mot-clé default et contiennent un corps comme les méthodes de classe.
- L’ajout d’une méthode abstraite à une interface déjà publiée provoque une incompatibilité de source.
- Les méthodes par défaut aident les concepteurs de bibliothèque à faire évoluer les API de manière rétrocompatible.
- Les méthodes par défaut peuvent être utilisées pour créer des méthodes facultatives et des héritages multiples de comportement.
- Il existe des règles de résolution pour résoudre les conflits lorsqu’une classe hérite de plusieurs méthodes par défaut avec la même signature.
- Une déclaration de méthode dans la classe ou une superclasse est prioritaire sur toute déclaration de méthode par défaut. Dans le cas contraire, la méthode par défaut avec la même signature dans l’interface la plus spécifique est sélectionnée.
- Lorsque deux méthodes sont spécifiques et de même niveau, une classe peut explicitement remplacer une méthode et sélectionner celle à appeler.