问题
My colleague and I had a bug that was due to our assumption that an empty stream calling allMatch()
would return false
.
if (myItems.allMatch(i -> i.isValid()) {
//do something
}
Of course, it is kind of our fault for assuming and not reading documentation. But what I don't understand is why the default allMatch()
behavior for an empty stream returns true
. What was the reasoning for this? Like the anyMatch()
(which contrarily returns false), this operation is used in an imperative way that departs the monad and probably used in an if
statement. Considering those facts, is there any reason why having allMatch()
default to true
on an empty stream be desirable for majority of uses?
回答1:
This is known as vacuous truth. All members of an empty collection satisfy your condition; after all, can you point to one that doesn't?
Similarly, anyMatch
returns false
, because you can't find an element of your collection that does match the condition. This is confusing to a lot of people, but it turns out to be the most useful and consistent way to define "any" and "all" for empty sets.
回答2:
When I call list.allMatch
(or its analogs in other languages), I want to detect if any items in list
fail to match the predicate. If there are no items, none might fail to match. My following logic would pick items and expect them to have matched the predicate. For an empty list, I'll pick no items and the logic will still be sound.
What if allMatch
returned false
for an empty list?
My straightforward logic would fail:
if (!myList.allMatch(predicate)) {
throw new InvalidDataException("Some of the items failed to match!");
}
for (Item item : myList) { ... }
I'll need to remember to replace the check with !myList.empty() && !myList.allMatch()
.
In short, allMatch
returning true
for an empty list is not only logically sound, it also lies on the happy path of execution, requiring fewer checks.
回答3:
Here's another way to think about this:
allMatch()
is to &&
what sum()
is to +
Consider the following logical statements:
IntStream.of(1, 2).sum() + 3 == IntStream.of(1, 2, 3).sum()
IntStream.of(1).sum() + 2 == IntStream.of(1, 2).sum()
This makes sense because sum()
is just a generalization of +
. However, what happens when you remove one more element?
IntStream.of().sum() + 1 == IntStream.of(1).sum()
We can see that it makes sense to define IntStream.of().sum()
, or the sum of an empty sequence of numbers, in a particular way. This gives us the "identity element" of summation, or the value that, when added to something, has no effect (0
).
We can apply the same logic to Boolean
algebra.
Stream.of(true, true).allMatch(it -> it) == Stream.of(true).allMatch(it -> it) && true
More generically:
stream.concat(Stream.of(thing)).allMatch(it -> it) == stream.allMatch(it -> it) && thing
If stream = Stream.of()
then this rule still needs to apply. We can use the "identity element" of && to solve this. true && thing == thing
, so Stream.of().allMatch(it -> it) == true
.
回答4:
It looks like the base of it is mathematical induction. For computer science an application of this could be a base case of a recursive algorithm.
If the stream is empty, the quantification is said to be vacuously satisfied and is always true. Oracle Docs: Stream operations and pipelines
The key here is that it is "vacuously satisfied" which, by nature, is somewhat misleading. Wikipedia has a decent discussion about it.
In pure mathematics, vacuously true statements are not generally of interest by themselves, but they frequently arise as the base case of proofs by mathematical induction. Wikipedia: Vacuous Truth
来源:https://stackoverflow.com/questions/30223079/why-does-stream-allmatch-return-true-for-an-empty-stream