Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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()
like image 346
Ned Twigg Avatar asked Nov 07 '14 21:11

Ned Twigg


People also ask

How do I find duplicates in a string in Java 8?

In Java 8 Stream, filter with Set. Add() is the fastest algorithm to find duplicate elements, because it loops only one time. Set<T> items = new HashSet<>(); return list. stream() .

How do you get a single value stream?

To find an element matching specific criteria in a given list, we: invoke stream() on the list. call the filter() method with a proper Predicate. call the findAny() construct, which returns the first element that matches the filter predicate wrapped in an Optional if such an element exists.

How do I use distinct in stream?

distinct() returns a stream consisting of distinct elements in a stream. distinct() is the method of Stream interface. This method uses hashCode() and equals() methods to get distinct elements. In case of ordered streams, the selection of distinct elements is stable.

How do you count the number of occurrences of an element in a Java 8 map?

With Eclipse Collections (formerly GS Collections), you can make use of a data structure called Bag that can hold the number of occurrences of each element. Using IntBag , the following will work: MutableList<Person> personsEC = ListAdapter. adapt(persons); IntBag intBag = personsEC.


3 Answers

This will incur an overhead of creating a set but it's simple and will work correctly even if you forget to distinct() the stream first.

static<T> Collector<T,?,Optional<T>> singleOrEmpty() {     return Collectors.collectingAndThen(             Collectors.toSet(),             set -> set.size() == 1                      ? set.stream().findAny()                      : Optional.empty()     ); } 
like image 86
Misha Avatar answered Sep 22 '22 06:09

Misha


"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!

like image 36
Thomas Jungblut Avatar answered Sep 22 '22 06:09

Thomas Jungblut


If you don't mind using Guava, you can wrap your code with Iterables.getOnlyElement, so it would look something like that:

SomeProperty distinctProperty = Iterables.getOnlyElement(
        someList.stream()
                .map(obj -> obj.getSomeProperty())
                .distinct()
                .collect(Collectors.toList()));

IllegalArgumentException will be raised if there is more than one value or no value, there is also a version with default value.

like image 35
S.D. Avatar answered Sep 22 '22 06:09

S.D.