UnsupportedOperationException dans les interfaces du framework de collections Java

12

En parcourant le Java Collections Framework, j'ai remarqué que bon nombre des interfaces ont le commentaire (optional operation). Ces méthodes permettent à des classes d'implémentation de passer par UnsupportedOperationExceptionsi elles ne veulent tout simplement pas implémenter cette méthode.

Un exemple de ceci est la addAllméthode dans le Set Interface.

Maintenant, comme indiqué dans cette série de questions, les interfaces sont un contrat déterminant pour ce que l'utilisation peut en attendre.

Les interfaces sont importantes car elles séparent ce que fait une classe de la façon dont elle le fait. Le contrat définissant ce à quoi un client peut s'attendre laisse le développeur libre de l'implémenter comme bon lui semble, tant qu'il respecte le contrat.

et

Une interface est une description des actions qu'un objet peut effectuer ... par exemple, lorsque vous actionnez un interrupteur d'éclairage, la lumière s'allume, vous ne vous souciez pas de savoir comment, tout simplement. Dans la programmation orientée objet, une interface est une description de toutes les fonctions qu'un objet doit avoir pour être un "X".

et

Je pense que l'approche basée sur l'interface est beaucoup plus agréable. Vous pouvez ensuite bien simuler vos dépendances, et tout est fondamentalement moins étroitement couplé.

Quel est l'intérêt d'une interface?

Que sont les interfaces?

Interface + extension (mixin) vs classe de base

Étant donné que le but des interfaces est de définir un contrat et de coupler vos dépendances de manière lâche, le fait que certaines méthodes ne vident pas en quelque UnsupportedOperationExceptionsorte le but? Cela signifie que je ne peux plus passer Setet simplement utiliser addAll. Au contraire, je dois savoir quelle implémentation de Setj'ai été adoptée, donc je peux savoir si je peux l'utiliser addAllou non. Cela me semble assez inutile.

Alors à quoi ça sert UnsupportedOperationException? Est-ce juste pour compenser le code hérité, et ils doivent nettoyer leurs interfaces? Ou a-t-il un objectif plus sensé qui me manque?

MirroredFate
la source
Je ne sais pas qui vous utilisez JRE, mais ma version Oracle 8 ne définit pas addAlldans HashSet. Il s'en remet à l'implémentation par défaut dans AbstractCollectionlaquelle ne jette certainement pasUnsupportedOperationException .
@Snowman Vous avez raison. J'ai mal lu les documents. Je vais modifier ma question.
MirroredFate
1
J'aime démarrer Eclipse et regarder le code source, en rebondissant autour des références de code et des définitions pour m'assurer d'avoir bien compris. Tant que le JRE est lié, src.zipil fonctionne très bien. Cela aide à savoir exactement quel code le JRE exécute parfois et à ne pas s'en remettre au JavaDoc qui peut être un peu bavard.

Réponses:

12

Regardez les interfaces suivantes:

Ces interfaces déclarent toutes les méthodes de mutation comme facultatives. Cela documente implicitement le fait que la classe Collections est capable de renvoyer des implémentations de ces interfaces qui sont immuables: c'est-à-dire que ces opérations de mutation facultatives sont garanties d'échouer. Cependant, selon le contrat dans JavaDoc, toutes les implémentations de ces interfaces doivent autoriser les opérations de lecture. Cela inclut les implémentations "normales" telles que HashSetet LinkedListainsi que les wrappers immuables dans Collections.

Contraste avec les interfaces de file d'attente:

Ces Interface ne pas spécifier les opérations optionnelles: une file d' attente, par définition, est conçu pour des éléments d'offre et sondage de manière FIFO. Une file d'attente immuable est à peu près aussi utile qu'une voiture sans roues.


Une idée courante qui revient à plusieurs reprises est d'avoir une hiérarchie d'héritage qui a des objets mutables et immuables. Cependant, ceux-ci ont tous des inconvénients. La complexité brouille les eaux sans vraiment résoudre le problème.

  • Une hypothétique Setpourrait avoir les opérations de lecture et une sous-interface MutableSetpourrait avoir les opérations d'écriture. Liskov nous dit qu'un a MutableSetpourrait alors être transmis à tout ce qui a besoin d'un Set. Au début, cela semble correct, mais considérons une méthode qui s'attend à ce que l'ensemble sous-jacent ne soit pas modifié pendant la lecture: il serait possible pour deux threads d'utiliser le même ensemble et de violer l'invariant de l'ensemble qui ne change pas. Cela pourrait causer un problème, par exemple si une méthode lit un élément de l'ensemble deux fois et qu'il est là la première fois mais pas la deuxième fois.

  • Setpourrait ne pas avoir d'implémentations directes, mais avoir MutableSetet ImmutableSetcomme sous-interfaces qui sont ensuite utilisées pour implémenter des classes. Cela a le même problème que ci-dessus: à un moment donné de la hiérarchie, une interface a des invariants contradictoires. L'un dit "cet ensemble doit être modifiable" et l'autre dit "cet ensemble ne peut pas changer".

  • Il pourrait y avoir deux hiérarchies complètement distinctes pour les structures de données mutables et immuables. Cela ajoute une tonne de complexité supplémentaire pour ce qui finit par être très peu de gain. Cela a également la faiblesse spécifique des méthodes qui ne se soucient pas de la mutabilité (par exemple, je veux juste parcourir une liste) doivent désormais prendre en charge deux interfaces distinctes. Étant donné que Java est de type statique, cela signifie des méthodes supplémentaires pour gérer les deux hiérarchies d'interface.

  • Nous pourrions avoir une interface unique et permettre aux implémentations de lever des exceptions si une méthode ne lui est pas applicable. C'est la route que Java a empruntée, et elle est la plus logique. Le nombre d'interfaces est réduit au minimum, et il n'y a pas d'invariants de mutabilité car l'interface documentée ne garantit en aucun cas la mutabilité . Si un invariant d'immuabilité est requis, utilisez les wrappers de Collections. Si une méthode n'a pas besoin de modifier une collection, ne la modifiez tout simplement pas. L'inconvénient est qu'une méthode ne peut pas garantir qu'une collection ne changera pas dans un autre thread si elle est fournie une collection de l'extérieur, mais c'est une préoccupation de la méthode appelante (ou de sa méthode d'appel) de toute façon.


Lecture connexe: Pourquoi Java 8 n'inclut-il pas de collections immuables?

Communauté
la source
1
Mais si les méthodes sont optionnelles, quelle place ont-elles dans l'interface? Ne devrait-il pas y avoir une interface séparée contenant les méthodes optionnelles, par exemple MutableCollection?
MirroredFate
Non. Il n'y a aucun moyen d'avoir des objets mutables et immuables dans la même hiérarchie de manière significative. Il y avait une question récente qui avait un bon diagramme montrant la complexité et une explication de pourquoi c'est une mauvaise idée, mais elle est supprimée. Peut-être que quelqu'un d'autre connaît une question pour expliquer cela, je ne trouve rien. Mais je mettrai à jour ma réponse pour expliquer un peu.
C'est une sorte de déclaration générale sur les files d'attente immuables. J'en ai utilisé un il y a quelques jours pour résoudre ce problème .
Karl Bielefeldt
@Snowman Mais cela semble distinguer ces objets mutables et immuables comme des opposés les uns des autres. Je pense que les objets immuables ne sont vraiment que des objets qui n'ont pas la capacité de muter. Honnêtement, la façon dont elle est en ce moment est plus complexe et confuse, parce que vous devez comprendre ce qui est une mise en œuvre mutable et ce qui ne l'est pas. Il me semble que la seule différence entre mettre toutes les méthodes dans une seule interface au lieu de diviser les méthodes mutables est une de clarté.
MirroredFate
@MirroredFate a lu ma dernière édition.
2

C'est essentiellement YAGNI. Toutes les collections concrètes de la bibliothèque standard sont modifiables, implémentant ou héritant des opérations facultatives. Ils ne se soucient pas des collections immuables à usage général, pas plus que la grande majorité des développeurs Java. Ils ne créeront pas une hiérarchie d'interface complète uniquement pour les collections immuables, puis n'incluront aucune implémentation.

D'un autre côté, il existe quelques valeurs à usage spécial ou collections "virtuelles" qui pourraient être très utiles en tant qu'immuables, comme un ensemble vide et nCopies . En outre, il existe des collections immuables tierces (telles que Scala), qui pourraient vouloir appeler le code Java existant, de sorte qu'elles ont laissé la possibilité de collections immuables ouvertes de la manière la moins perturbatrice.

Karl Bielefeldt
la source
Ok, ça a du sens. Il semble cependant qu'il aborde le problème dans la mauvaise direction. Pourquoi ne pas commencer par définir des interfaces de collection immuables, puis définir les interfaces mutables pour les implémentations de collection mutables?
MirroredFate