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.

 

Topics: #coding #imadethis #java #Csharp #object-oriented-programming #bug-prevention

Comments

No comments yet

You need to be logged in to comment.