Raison de ne pas utiliser une collection gardée / contrainte

Existe-t-il des raisons / arguments pour ne pas implémenter une collection Java limitant ses membres en fonction d’un prédicat / contrainte?

Étant donné que cette fonctionnalité devait souvent être nécessaire, je m’attendais à ce qu’elle soit déjà mise en œuvre sur des frameworks de collections tels qu’apache-commons ou Guava. Mais bien qu’apache en ait eu , Guava en a déconseillé la version et recommande de ne pas utiliser des approches similaires.

Le contrat d’interface Collection indique qu’une collection peut imposer des ressortingctions à ses éléments, à condition qu’elle soit correctement documentée. Par conséquent, je ne vois pas pourquoi une collection protégée serait découragée. Quelle autre option existe-t-il pour, par exemple, faire en sorte qu’une collection Integer ne contienne jamais de valeurs négatives sans masquer toute la collection?

C’est juste une question de préférence – jetez un coup d’œil au fil conducteur sur la vérification avant vs après vérification – je pense que c’est à quoi cela revient. La vérification de add() suffisante pour les objects immuables.

Il peut difficilement y avoir une réponse (“acceptable”), je vais donc append quelques reflections:

Comme mentionné dans les commentaires, Collection#add(E) permet déjà de lancer une IllegalArgumentException , avec la raison

si une propriété de l’élément l’empêche d’être ajoutée à cette collection

On pourrait donc dire que ce cas a été explicitement pris en compte dans la conception de l’interface de collection, et qu’il n’existe aucune raison évidente, profonde, purement technique (liée à un contrat d’interface) de ne pas permettre la création d’une telle collection.


Cependant, lorsque l’on réfléchit aux modèles d’application possibles, on trouve rapidement des cas où le comportement observé d’une telle collection pourrait être… peu intuitif, pour le moins.

Dcsohl en avait déjà parlé dans l’un de ses commentaires et évoquait des cas où une telle collection ne serait qu’un point de vue sur une autre collection:

 List listWithIntegers = new ArrayList(); List listWithPositiveIntegers = createView(listWithIntegers, e -> e > 0); //listWithPositiveIntegers.add(-1); // Would throw IllegalArgumentException listWithIntegers.add(-1); // Fine // This would be true: assert(listWithPositiveIntegers.contains(-1)); 

Cependant, on pourrait soutenir que

  • Une telle collection ne doit pas nécessairement être uniquement une vue . Au lieu de cela, on pourrait imposer que seules les nouvelles collections avec de telles contraintes peuvent être créées
  • Le comportement est similaire à celui de Collections.unmodifiableCollection(Collection) , qui est largement anticipé tel quel. (Bien que cela serve un cas d’utilisation beaucoup plus large et omniprésent, c’est-à-dire d’éviter que l’état interne d’une classe ne soit exposé en renvoyant une version modifiable d’une collection via une méthode d’access.)

Mais dans ce cas, le potentiel “d’incohérence” est beaucoup plus élevé.

Par exemple, considérons un appel à Collection#addAll(Collection) . Il permet également de lancer une IllegalArgumentException “si une propriété d’un élément de la collection spécifiée l’empêche d’être ajoutée à cette collection” . Mais il n’y a aucune garantie sur des choses comme l’atomicité. Pour le formuler comme suit: Il n’est pas précisé quel sera l’ état de la collection lorsqu’une telle exception aura été levée. Imaginez un cas comme celui-ci:

 List listWithPositiveIntegers = createList(e -> e > 0); listWithPositiveIntegers.add(1); // Fine listWithPositiveIntegers.add(2); // Fine listWithPositiveIntegers.add(Arrays.asList(3,-4,5)); // Throws assert(listWithPositiveIntegers.contains(3)); // True or false? assert(listWithPositiveIntegers.contains(5)); // True or false? 

(Cela peut être subtile, mais cela peut être un problème).

Tout cela risque de devenir encore plus délicat lorsque la condition change après la création de la collection (que ce soit une vue ou non). Par exemple, on pourrait imaginer une séquence d’appels comme celle-ci:

 List listWithPredicate = create(predicate); listWithPredicate.add(-1); // Fine someMethod(); listWithPredicate.add(-1); // Throws 

Où dans someMethod() , il y a une ligne innocente comme

 predicate.setForbiddingNegatives(true); 

L’un des commentaires mentionnait déjà des problèmes de performances possibles. C’est certes vrai, mais j’estime qu’il ne s’agit pas vraiment d’un argument technique fort : De toute façon, il n’existe aucune garantie de complexité formelle pour l’exécution d’une méthode de l’interface Collection . Vous ne savez pas combien de temps prend un appel collection.add(e) . Pour un LinkedList il s’agit de O (1), mais pour un TreeSet il peut s’agir de O (n log n) (et qui sait ce que n est à ce moment-là).

Peut-être que le problème de performance et les incohérences possibles peuvent être considérés comme des cas particuliers d’une déclaration plus générale:

Une telle collection permettrait en principe d’ exécuter du code arbitraire au cours de nombreuses opérations, en fonction de l’implémentation du prédicat.

Cela peut avoir des implications arbitraires et rend impossible le raisonnement sur les algorithmes, les performances et le comportement exact (en termes de cohérence).


L’essentiel est le suivant: il existe de nombreuses raisons possibles de ne pas utiliser une telle collection. Mais je ne peux pas penser à une raison technique forte et générale. Il peut donc y avoir des cas d’application pour une telle collection, mais il convient de garder à l’esprit les mises en garde, en considérant exactement comment une telle collection est destinée à être utilisée .

Je dirais qu’une telle collection aurait trop de responsabilités et violerait le PÉR.

Le problème principal que je vois ici est la lisibilité et la maintenabilité du code qui utilise la collection. Supposons que vous ayez une collection à laquelle vous autorisez l’ajout uniquement des entiers positifs ( Collection ) et que vous l’utilisiez dans tout le code. Ensuite, les exigences changent et vous ne pouvez y append que des entiers positifs impairs. Comme il n’y a pas de vérification de la compilation, il vous serait beaucoup plus difficile de trouver toutes les occurrences du code où vous ajoutez des éléments à cette collection que si vous aviez une classe wrapper séparée qui encapsule la collection.

Bien que, bien entendu, il n’est pas aussi proche de l’extrême, cela ressemble quelque peu à l’utilisation de la référence à Object pour tous les objects de l’application.

La meilleure approche consiste à utiliser des contrôles de temps de compilation et à suivre les principes OOP bien établis tels que la sécurité de type et l’encapsulation. Cela signifie créer une classe wrapper distincte ou un type distinct pour les éléments de collection.

Par exemple, si vous voulez vraiment vous assurer que vous ne travaillez qu’avec des entiers positifs dans un contexte, vous pouvez créer un type distinct, PositiveInteger extends Number , puis les append à une Collection . De cette façon, vous gagnez en sécurité lors de la compilation et la conversion de PositiveInteger en OddPositiveInteger nécessite beaucoup moins d’effort.

Les énumérations sont un excellent exemple de préférence des types dédiés aux valeurs contraintes à l’exécution (chaînes constantes ou entiers).