Mon collègue et moi avons eu un bogue dû à notre supposition qu'un flux vide appelant allMatch()
retournerait false
.
if (myItems.allMatch(i -> i.isValid()) {
//do something
}
Bien sûr, c'est en quelque sorte notre faute si nous supposons et ne lisons pas la documentation. Mais ce que je ne comprends pas, c'est pourquoi le comportement par défaut de allMatch()
pour un flux vide renvoie true
. Quel en était le raisonnement? Comme la anyMatch()
(qui renvoie au contraire faux), cette opération est utilisée de manière impérative qui quitte la monade et probablement utilisée dans une instruction if
. Compte tenu de ces faits, y a-t-il une raison pour laquelle avoir allMatch()
par défaut à true
sur un flux vide est souhaitable pour la majorité des utilisations?
Ceci est connu sous le nom de vérité vide . Tous les membres d'une collection vide satisfont votre condition; après tout, pouvez-vous en indiquer un qui ne l'est pas? De même, anyMatch
renvoie false, car vous ne pouvez pas trouver un élément de votre collection qui correspond à la condition. C'est déroutant pour beaucoup de gens, mais cela s'avère être le moyen le plus utile et le plus cohérent de définir "n'importe" et "tous" pour les ensembles vides.
Voici une autre façon de penser à ce sujet:
allMatch()
est à &&
ce que sum()
est à +
Considérez les déclarations logiques suivantes:
IntStream.of(1, 2).sum() + 3 == IntStream.of(1, 2, 3).sum()
IntStream.of(1).sum() + 2 == IntStream.of(1, 2).sum()
Cela a du sens car sum()
n'est qu'une généralisation de +
. Cependant, que se passe-t-il lorsque vous supprimez un élément de plus?
IntStream.of().sum() + 1 == IntStream.of(1).sum()
Nous pouvons voir qu'il est logique de définir IntStream.of().sum()
, ou la somme d'une séquence vide de nombres, d'une manière particulière. Cela nous donne "l'élément d'identité" de la somme, ou la valeur qui, lorsqu'elle est ajoutée à quelque chose, n'a aucun effet (0
).
On peut appliquer la même logique à l'algèbre Boolean
.
Stream.of(true, true).allMatch(it -> it) == Stream.of(true).allMatch(it -> it) && true
Plus génériquement:
stream.concat(Stream.of(thing)).allMatch(it -> it) == stream.allMatch(it -> it) && thing
Si stream = Stream.of()
alors cette règle doit toujours s'appliquer. Nous pouvons utiliser "l'élément d'identité" de && pour résoudre ce problème. true && thing == thing
, Donc Stream.of().allMatch(it -> it) == true
.
Lorsque j'appelle list.allMatch
(Ou ses analogues dans d'autres langues), je veux détecter si des éléments de list
ne correspondent pas au prédicat. S'il n'y a aucun élément, aucun ne peut échouer. Ma logique suivante choisirait des éléments et s'attendrait à ce qu'ils correspondent au prédicat. Pour une liste vide, je ne choisirai aucun élément et la logique restera saine.
Que faire si allMatch
a renvoyé false
pour une liste vide?
Ma logique simple échouerait:
if (!myList.allMatch(predicate)) {
throw new InvalidDataException("Some of the items failed to match!");
}
for (Item item : myList) { ... }
Je devrai me souvenir de remplacer le chèque par !myList.empty() && !myList.allMatch()
.
En bref, allMatch
renvoyant true
pour une liste vide est non seulement logique, mais se trouve également sur la bonne voie d'exécution, nécessitant moins de vérifications.
Il semble que sa base soit l'induction mathématique. Pour l'informatique, une application de ceci pourrait être le cas de base d'un algorithme récursif.
Si le flux est vide, la quantification est dite satisfaite de façon vide et est toujours vraie. Oracle Docs: opérations de flux et pipelines
La clé ici est qu'elle est "satisfaite de manière vide" ce qui, par nature, est quelque peu trompeur. Wikipédia a une discussion décente à ce sujet.
En mathématiques pures, les énoncés vides de vérité ne présentent généralement pas d'intérêt en eux-mêmes, mais ils apparaissent fréquemment comme cas de base des preuves par induction mathématique. Wikipedia: Vacuous Truth