5

I'm a big fan of the singleOrEmpty stream operator. It's not in the std lib, but I find it very useful. If a stream has only a single value, it returns that value in an Optional. If it has no values or more than one value, it returns Optional.empty().

Optional<Int> value = someList.stream().{singleOrEmpty}
[]     -> Optional.empty()
[1]    -> Optional.of(1)
[1, 1] -> Optional.empty()
etc.

I asked a question about it earlier and @ThomasJungblut came up with this great implementation:

public static <T> Optional<T> singleOrEmpty(Stream<T> stream) {
    return stream.limit(2)
        .map(Optional::ofNullable)
        .reduce(Optional.empty(),
             (a, b) -> a.isPresent() ^ b.isPresent() ? b : Optional.empty());
}

The only problem is, you have to put it at the beginning of your call

singleOrEmpty(someList.stream().filter(...).map(...))

rather than sequentially at the end

someList.stream().filter().map().singleOrEmpty()

which makes it harder to read than other stream mechanisms.

So as a newbie to this stream processing stuff, does anybody have any tricks for how to go about putting a short-circuiting singleOrEmpty mechanism at the end of a sequence of stream transformations?

Community
  • 1
  • 1
Ned Twigg
  • 2,159
  • 2
  • 22
  • 38
  • I think this is not possible in Java 8, similar story here (http://stackoverflow.com/questions/22308823/extending-listt-in-java-8) – Thomas Jungblut Nov 08 '14 at 02:09

1 Answers1

6

It will not be as fast as the one with limit(2) but you can use it as list.stream().filter(...).map(...).collect(singleOrEmpty())

static <T> Collector<T, ?, Optional<T>> singleOrEmpty() {
    return Collectors.collectingAndThen(
            Collectors.mapping(
                    Optional::of,
                    Collectors.reducing((a, b) -> Optional.empty())
            ),
            o -> o.orElseGet(Optional::empty)
    );
}

Stream.empty().collect(singleOrEmpty());   // Optional.empty
Stream.of(1).collect(singleOrEmpty());     // Optional[1]
Stream.of(1, 1).collect(singleOrEmpty());  // Optional.empty
Stream.of(1, 1).skip(1).collect(singleOrEmpty());  // Optional[1]

For what it's worth, unless this is really performance critical code, I would personally prefer the less clever but much clearer implementation

static<T> Collector<T,?,Optional<T>> singleOrEmpty() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            lst -> lst.size() == 1
                    ? Optional.of(lst.get(0))
                    : Optional.empty()
    );
}
Misha
  • 27,433
  • 6
  • 62
  • 78
  • 2
    First collector can be simplified with overloaded version of `Collectors.reducing(...)`. I think this version is no worse than the second you suggested (sorry for the large piece of code, I just don't think it worth separate answer). `static Collector> singleOrEmptyCollector() { return Collectors.reducing( Optional.empty(), Optional::ofNullable, (opt, t) -> opt.isPresent() ^ t.isPresent() ? t : Optional.empty() ); }` – Stanislav Lukyanov Nov 08 '14 at 02:03
  • Also, there is another thing to mention. If a pipeline has some expensive operation (like `map((a) -> doSomethingReallySlow(a))`) then we still have to perform it on ALL elements even if we just want one. `limit(2)` can save us, but not in all cases. See [`limit` documentation](http://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#limit-long-) for details. So be careful and don't expect this collector to skip all elements but one (or, at least, use it like `stream.of(...).limit(2).collect(singleOrEmpty())`). – Stanislav Lukyanov Nov 08 '14 at 02:12
  • 2
    @StanislavLukyanov Your collector will fail if the stream has 3 (or 5, 7, etc.) elements. For example, `Stream.of(1,1,1).collect(...)` will return Optional[1] instead of Optional.empty. – Misha Nov 08 '14 at 02:18
  • Thanks! You right! Actually, I've figured out even simpler trick: `Collectors.reducing((a, b) -> null)`. Hope this time I didn't miss anything :) – Stanislav Lukyanov Nov 08 '14 at 02:56
  • 1
    `reducing((a,b) -> null)` works! But it feels so very wrong. `stream.reduce` throws a NPE if result of reduction is null but Collectors.reducing doesn't. I wonder if that's intentional or an oversight. One downside of using this trick is that a stream consisting of a single null will appear as empty. Perhaps a more robust solution would be `mapping(Objects::requireNonNull, reducing( (a,b) -> null))`. – Misha Nov 08 '14 at 03:18
  • About single null - we should consider such stream as empty anyway since return type is Optional that only contains non-null values. – Stanislav Lukyanov Nov 08 '14 at 13:17
  • About NPE - I think that's OK. The thing is that `Collectors.reducing` cannot distinct empty stream and stream, reduced to null when `Stream.reduce` uses NPE for this distiction. But `Collectors.reducing` itself cannot throw it cause it just creates `Collector`. `Stream.collect` could throw something when applying this reducing `Collector`, but what exception exactly? Throwing NPE just to handle reducing `Collector` would be wrong as well as throwing something abstract like `Throwable` or `RuntimeException`. – Stanislav Lukyanov Nov 08 '14 at 13:17