Pourquoi les collections Java ont-elles été implémentées avec des «méthodes facultatives» dans l'interface?


69

Lors de ma première implémentation d'extension du framework de collection Java, j'ai été assez surpris de voir que l'interface de la collection contienne des méthodes déclarées comme optionnelles. L'implémenteur est censé déclencher des exceptions UnsupportedOperationExceptions s'il n'est pas pris en charge. Cela m'a immédiatement semblé un choix de conception d'API médiocre.

Après avoir lu une bonne partie de l'excellent livre "Effective Java" de Joshua Bloch, et appris plus tard qu'il est peut-être responsable de ces décisions, il n'a pas semblé se fondre dans les principes énoncés dans le livre. Je pense que déclarer deux interfaces: Collection et MutableCollection qui étend Collection avec les méthodes "optionnelles" aurait conduit à un code client beaucoup plus facile à gérer.

Il y a un excellent résumé des problèmes ici .

Y avait-il une bonne raison pour laquelle des méthodes facultatives ont été choisies au lieu de l'implémentation de deux interfaces?


OMI, il n'y a pas de bonne raison. La STL C ++ a été créée par Stepanov au début des années 80. Bien que sa convivialité ait été limitée par la syntaxe délicate des modèles C ++, il s'agit d'un modèle de cohérence et de convivialité par rapport aux classes de collection Java.
Kevin Cline

Il y avait un STL C ++ 'portage' en Java. Mais il était 5 fois plus grand en nombre de classes. Dans cet esprit, je ne pense pas que le plus grand est plus cohérent et utilisable. docs.oracle.com/javase/6/docs/technotes/guides/collections/…
m3th0dman

@ m3th0dman C'est beaucoup plus gros en Java, car Java n'a rien d'équivalent en puissance aux modèles C ++.
kevin cline

C’est peut-être juste un style étrange que j’ai développé, mais mon code a tendance à traiter toutes les collections comme "en lecture seule", (plus précisément, quelque chose que vous comptez ou sur lequel vous faites une itération) à l’ exception des quelques méthodes qui créent réellement les collections. Probablement une bonne pratique quand même (surtout pour la concurrence). Et les méthodes optionnelles dont se plaignent tant de personnes n’ont jamais été un réel problème pour moi. Les cauchemars génériques avec "super" et "étend" n'ont jamais été (beaucoup) d'un problème. Je me demandais si d'autres utilisaient cette pratique générale?
user949300

Réponses:


28

La FAQ fournit la réponse. En bref, ils ont constaté une explosion combinatoire potentielle des interfaces nécessaires avec des vues modifiables, non modifiables, avec suppression seulement, avec ajout seulement, de longueur fixe, immuables (pour le filetage), etc. pour chaque ensemble possible de méthodes d'option implémentées.


6
Tout cela aurait été évité si Java avait un constmot clé comme C ++.
Etienne de Martel

@ Etienne: Mieux vaut des métaclasses comme Python. Vous pouvez ensuite créer par programmation le nombre combinatoire d’interfaces. Le problème avec const est que cela ne vous donne une ou deux dimensions: vector<int>, const vector<int>, vector<const int>, const vector<const int>. Jusqu'ici, tout va bien, mais vous essayez ensuite d'implémenter des graphes et vous voulez rendre la structure du graphe constante, mais les attributs de nœud sont modifiables, etc.
Neil G

2
@Etienne, encore une autre raison pour ajouter "Learn Scala" à ma liste de choses à faire!
Glenviewjeff

2
Ce que je ne comprends pas, c’est pourquoi ils n’ont pas mis au point une canméthode permettant de vérifier si une opération est possible? Cela garderait l'interface simple et rapide.
Mehrdad,

3
@ Etienne de Martel Nonsense. Comment cela aiderait-il dans l'explosion combinatoire?
Tom Hawtin - tackline

10

Il me semble que ce Interface Segregation Principlen’était pas aussi bien exploré à l’époque que maintenant; cette façon de faire (c’est-à-dire que votre interface inclut toutes les opérations possibles et que vous avez des méthodes "dégénérées" qui jettent des exceptions pour celles dont vous n’avez pas besoin) était populaire avant que SOLID et ISP ne deviennent la norme de facto pour le code qualité.


2
Pourquoi un vote négatif? Quelqu'un n'aime pas le FAI?
Wayne Molina

Il est également intéressant de noter que, dans les cadres prenant en charge la variance, il existe un énorme avantage à séparer les aspects d'une interface qui peuvent être covariants ou contradictoires par rapport à ceux qui sont fondamentalement invariants. Même en l'absence d'un tel support, dans les cadres qui n'utilisent pas l'effacement des types, il serait utile de séparer les aspects d'une interface indépendants du type (par exemple, permettre de récupérer le Countcontenu d'une collection sans avoir à se soucier des types d'éléments qu'elle contient), mais dans les frameworks basés sur l'effacement de type comme Java, ce n'est pas un problème.
Supercat

4

Certaines personnes peuvent détester les "méthodes optionnelles", mais dans de nombreux cas, elles peuvent offrir une meilleure sémantique que des interfaces hautement séparées. Entre autres choses, ils permettent à un objet de gagner des capacités ou des caractéristiques au cours de sa vie, ou qu’un objet (en particulier un objet wrapper) ne sache pas quand il est construit quelles capacités exactes il doit signaler.

Bien que j'appelle difficilement les classes de collection Java comme étant des modèles de bonne conception, je suggérerais qu'un bon cadre de collections devrait inclure à sa base un grand nombre de méthodes facultatives ainsi que des moyens de demander à une collection ses caractéristiques et ses capacités . Une telle conception permettra à une seule classe wrapper d'être utilisée avec une grande variété de collections sans occulter accidentellement les capacités que la collection sous-jacente pourrait posséder. Si les méthodes n'étaient pas facultatives, il serait alors nécessaire de disposer d'une classe de wrapper différente pour chaque combinaison de fonctions pouvant être gérées par les collections, sinon certaines enveloppes seraient inutilisables dans certaines situations.

Par exemple, si une collection prend en charge l'écriture d'un élément par index ou l'ajout d'éléments à la fin, mais ne prend pas en charge l'insertion d'éléments au milieu, le code souhaitant l'encapsuler dans un wrapper qui enregistrerait toutes les actions effectuées sur celui-ci nécessiterait une version. du wrapper de journalisation qui fournissait la combinaison exacte des capacités prises en charge, ou si aucune n'était disponible, devrait utiliser un wrapper prenant en charge l'ajout ou l'écriture par index mais pas les deux. Si, toutefois, une interface de collection unifiée fournissait les trois méthodes en tant que "facultatives", mais incluait ensuite des méthodes pour indiquer laquelle des méthodes facultatives serait utilisable, une seule classe d'encapsuleur pourrait gérer des collections qui implémenteraient n'importe quelle combinaison de caractéristiques. Lorsqu'on lui demande quelles fonctionnalités il prend en charge, un wrapper peut simplement signaler tout ce que la collection encapsulée prend en charge.

Notez que l'existence de "capacités facultatives" peut dans certains cas permettre aux collections agrégées d'implémenter certaines fonctions de manière beaucoup plus efficace que si les capacités étaient définies par l'existence d'implémentations. Par exemple, supposons qu'une concatenateméthode soit utilisée pour former une collection composite à partir de deux autres, le premier d'entre eux étant un ArrayList avec 1 000 000 d'éléments et le dernier étant une collection de 20 éléments pouvant uniquement être itérée dès le début. Si la collection composite était demandée pour le 1 000 013e élément (index 1 000 012), elle pourrait demander à ArrayList combien d'éléments elle contenait (c.-à-d. 1 000 000), soustrayez celle-ci de l'index demandé (donnant 12), lisez et ignorez 12 éléments du deuxième collection, puis renvoie l'élément suivant.

Dans une telle situation, même si la collection composite ne dispose pas d'un moyen instantané de retourner un élément par index, le demander à la collection composite pour le 1 000 013e élément serait toujours beaucoup plus rapide que de lire 1 000 013 éléments individuellement et d'ignorer tous les éléments sauf le dernier. un.


@ kevincline: Êtes-vous en désaccord avec le libellé cité? Je considérerais que la conception des moyens par lesquels les implémentations d'interface peuvent décrire leurs caractéristiques et capacités essentielles est l'un des aspects les plus importants de la conception d'interface, même si cela ne reçoit souvent pas beaucoup d'attention. Si différentes implémentations d'une interface auront différentes manières optimales d'accomplir certaines tâches courantes, les clients soucieux de la performance doivent avoir un moyen de choisir la meilleure approche dans les scénarios où cela serait important.
Supercat

1
désolé, commentaire incomplet. J'étais sur le point de dire que "'moyens de poser des questions sur une collection" ... "déplace ce qui devrait être un contrôle de compilation au moment de l'exécution.
kevin cline

@kevincline: dans les cas où un client a besoin d'une certaine capacité et ne peut pas s'en passer, des vérifications au moment de la compilation peuvent être utiles. D'autre part, il existe de nombreuses situations dans lesquelles les collections peuvent avoir des capacités autres que celles pour lesquelles elles peuvent être garanties à la compilation. Dans certains cas, il peut être judicieux de disposer d'une interface dérivée dont le contrat spécifie que toutes les implémentations légitimes de l' interface dérivée doivent prendre en charge des méthodes particulières qui sont facultatives dans l'interface de base, mais dans les cas où le code pourra gérer quelque chose, que ce soit ou non. il a quelques fonctionnalités ...
Supercat

... mais bénéficiera de la fonctionnalité existante, il est préférable que le code demande à l'objet s'il supporte la fonctionnalité plutôt que de demander au système de types si le type de l'objet promet de prendre en charge la fonctionnalité. Si une interface "spécifique" particulière sera largement utilisée, on pourrait inclure une AsXXXméthode dans l'interface de base qui renverra l'objet sur lequel elle est invoquée si elle implémente cette interface, renvoyer un objet encapsuleur prenant en charge cette interface si possible ou une exception sinon. Par exemple, une ImmutableCollectioninterface peut nécessiter un contrat ...
Supercat

2
Votre proposition pose un problème: un modèle "ask then do" pose un problème de simultanéité si les capacités d'un objet peuvent changer pendant sa durée de vie. (Si vous devez risquer une exception de toute façon, vous pourriez aussi bien ne pas vous donner la peine de demander ...)
jhominal

-1

Je l’attribuerais aux développeurs originaux, mais je ne savais pas mieux à l’époque. Nous avons parcouru un long chemin dans la conception orientée objet depuis 1998 ou à peu près, lorsque Java 2 et Collections ont été lancés pour la première fois. Ce qui semble être une mauvaise conception évidente n’était plus si évident au début de la programmation orientée objet.

Mais cela a peut-être été fait pour empêcher un casting supplémentaire. S'il s'agissait d'une deuxième interface, il vous faudrait transtyper vos instances de collections pour appeler ces méthodes facultatives, ce qui est également un peu moche. Dans l'état actuel des choses, vous interceptez immédiatement une exception UnsupportedOperationException et corrigez votre code. Mais s'il y avait deux interfaces, vous devriez utiliser instanceof et casting partout. Peut-être ont-ils considéré cela comme un compromis valable. De plus, au début de Java, une instance de 2 jours était très mal vue en raison de la lenteur de ses performances, ils auraient peut-être tenté d'empêcher une utilisation excessive de celle-ci.

Bien sûr, il s’agit d’une pure spéculation. Je doute que nous puissions y répondre à moins que l’un des architectes de la collection originale ne sonne.


7
Quel casting? Si une méthode vous renvoie un Collectionet pas un MutableCollection, c'est un signe clair qu'elle n'est pas destinée à être modifiée. Je ne sais pas pourquoi quelqu'un aurait besoin de les lancer. Disposer d'interfaces séparées signifie que vous obtiendrez ce type d'erreur lors de la compilation au lieu d'une exception lors de l'exécution. Plus tôt vous obtenez l'erreur, mieux c'est.
Etienne de Martel

1
Parce que c'est beaucoup moins flexible, l'un des plus gros avantages de la bibliothèque de collections est que vous pouvez renvoyer partout les interfaces de haut niveau sans vous soucier de la mise en œuvre réelle. Si vous utilisez deux interfaces, vous êtes maintenant plus étroitement couplé. Dans la plupart des cas, vous voudrez simplement renvoyer List et non ImmutableList car vous souhaitez généralement laisser cette tâche à la classe appelante.
Jberg

6
Si je vous donne une collection en lecture seule, c'est que je ne veux pas qu'elle soit modifiée. Lancer une exception et s’appuyer sur la documentation est un peu comme un patch. En C ++, je renverrais simplement un constobjet, indiquant instantanément à l'utilisateur qu'il ne peut pas être modifié.
Etienne de Martel

7
1998 n'était pas "les débuts de la conception OO".
quant_dev
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.