Ned Twigg Ned Twigg - 1 month ago 8
Java Question

Java 8 Collector that returns a value if there's only a single value

I'm a little green on this functional programming and streams stuff, but what little I do know has been very useful!

I've had this situation come up several times:

List<SomeProperty> distinctProperties = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.toList());

if (distinctProperties.size() == 1) {
SomeProperty commonProperty = distinctProperties.get(0);
// take some action knowing that all share this common property
}


What I really want is:

Optional<SomeProperty> universalCommonProperty = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.singleOrEmpty());


I think the
singleOrEmpty
thing can be useful in other situations besides just in combination with
distinct
. When I was an uber n00b I spent a lot of time reinventing the Java Collections Framework because I didn't know it was there, so I'm trying not to repeat my mistakes. Does Java come with a good way to do this
singleOrEmpty
thing? Am I formulating it wrong?

Thanks!

EDIT: Here's some example data for the
distinct
case. If you ignore the
map
step:

Optional<SomeProperty> universalCommonProperty = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.singleOrEmpty());

[] -> Optional.empty()
[1] -> Optional.of(1)
[1, 1] -> Optional.of(1)
[2, 2] -> Optional.of(2)
[1, 2] -> Optional.empty()


I find I need this when I screw up my types, or have legacy code. It's really nice to be able to quickly say "All the elements of this collection share this property, so now I can take some action using this shared property." Another example is when a user multi-selects some diverse elements, and you're trying to see what stuff you can do (if anything) that's valid for all of them.

EDIT2: Sorry if my example is a misleading. The key is singleOrEmpty. I commonly find that I put a
distinct
in front, but it could just as easily be a
filter
of some other kind.

Optional<SomeProperty> loneSpecialItem = someList.stream()
.filter(obj -> obj.isSpecial())
.collect(Collectors.singleOrEmpty());

[special] -> Optional.of(special)
[special, special] -> Optional.empty()
[not] -> Optional.empty()
[not, special] -> Optional.of(special)
[not, special, not] -> Optional.of(special)


EDIT3: I think I screwed up by motivating the singleOrEmpty instead of just asking for it on its own.

Optional<Int> value = someList.stream().collect(Collectors.singleOrEmpty())
[] -> Optional.empty()
[1] -> Optional.of(1)
[1, 1] -> Optional.empty()

Answer

"Hacky" solution that only evaluates the first two elements:

    .limit(2)
    .map(Optional::ofNullable)
    .reduce(Optional.empty(),
        (a, b) -> a.isPresent() ^ b.isPresent() ? b : Optional.empty());

Some basic explanation:

Single element [1] -> map to [Optional(1)] -> reduce does

"Empty XOR Present" yields Optional(1)

= Optional(1)

Two elements [1, 2] -> map to [Optional(1), Optional(2)] -> reduce does:

"Empty XOR Present" yields Optional(1)
"Optional(1) XOR Optional(2)" yields Optional.Empty

= Optional.Empty

Here is the complete testcase:

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());
}

@Test
public void test() {
    testCase(Optional.empty());
    testCase(Optional.of(1), 1);
    testCase(Optional.empty(), 1, 1);
    testCase(Optional.empty(), 1, 1, 1);
}

private void testCase(Optional<Integer> expected, Integer... values) {
    Assert.assertEquals(expected, singleOrEmpty(Arrays.stream(values)));
}

Kudos to Ned (the OP) who has contributed the XOR idea and the above testcase!