Java findFirst stream pitfall
Java streams have findAny and findFirst methods out of the box.
And they works fine, they do exactly what they say: it gets one of the matches.
But in my experience, often I don't want 'one of the matches', I want there to be a single match, and I want that one.
The problem
For example, finding the instances in a stream of customers
var formExpressionInstance = getCustomers().stream() .filter(instance -> isDateInRange(ferenceDate, instance.getDateStart(), instance.getDateEnd())) .findFirst()
If there are multiple matches, maybe a bug added too many things to the collection, or there's a problem with the filter, or an incorrect assumption has been made...
And as usual with bugs: a stacktrace is preferable to continuing with subtly incorrect behavior (like choosing a random customer instance).
A solution
One could make a special collector and use it as such:
var formExpressionInstance = getCustomers().stream() .filter(instance -> isDateInRange(ferenceDate, instance.getDateStart(), instance.getDateEnd())) .collect(oneOrThrow())
It will throw if there is not exactly one match (that is not null).
It can be implemented like this:
@Nonnull @CheckReturnValue public static <T> Collector<T, SingleElementContainer<T>, T> oneOrThrow() { return Collector.of( // Initialize the 'collection'. () -> new SingleElementContainer<>(), // Extend the 'collection' with a new element. (collected, element) -> collected.register(element), // Combine 'collections' from different threads. (left, right) -> left.combine(right), // Finish the collection by extracting the element. (container) -> container.getOrThrow() ); }
You could make a similar collector for zero or one match:
@Nonnull @CheckReturnValue public static <T> Collector<T, SingleElementContainer<T>, Optional<T>> uptoOneOrThrow() { return Collector.of( // Initialize the 'collection'. () -> new SingleElementContainer<>(), // Extend the 'collection' with a new element. (collected, element) -> collected.register(element), // Combine 'collections' from different threads. (left, right) -> left.combine(right), // Finish the collection by extracting the element. (container) -> container.get() ); }
Performance
Admittedly if there is exactly one match, this takes about twice as long, as it will search the list to the end (or to the second match).
However, if you're trying to search a unique match in a huge collection, in many cases the bigger performance problem is that you're not using a Set or Map.
I think it's worth the cost for small collections, just to help find bugs faster. It's not great for big collections, but probably findAny isn't, either.
Do you believe this worth it? Should Java have included it? Some languages like C# do have this built-in.
Comments
No comments yet
You need to be logged in to comment.