33

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()
2
  • @Ned Twigg can you be kind and post up your somelist content so your issue can be reproduced? Commented Nov 7, 2014 at 21:44
  • It's worth mentioning that in C#'s LINQ this would be similar to SingleOrDefault. Commented Nov 8, 2014 at 12:48

8 Answers 8

20

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()
    );
}
Sign up to request clarification or add additional context in comments.

6 Comments

It is an elegant solution for the case that you're putting a distinct in front, but the root of my question is how to implement singleOrEmpty.
@NedTwigg I'm not sure I understand. This will pass all your unit tests. Stream.of(1,2).collect(singleOrEmpty()) -> Optional.empty() Stream.of(1).collect(singleOrEmpty()) -> Optional.of(1) etc
@NedTwigg it will do the distinct for you, because you collect all the stuff into a Set.
I did a bad job with my example, it emphasized the wrong part of my question. I don't want to always do distinct, sometimes I need other filters.
@NedTwigg That's fine. Let's say you want to get singleOrEmpty length of multiple strings. Stream.of("one", "two").map(String::length).collect(singleOrEmpty()) -> Optional.of(3) (all strings have length of 3) Stream.of("one", "three").map(String::length).collect(singleOrEmpty()) -> Optional.empty() because lengths are different
|
13

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

17 Comments

Is it possible to short-circuit terminate a stream rather than to collect a HashSet whose only valid contents will be a single element?
@NedTwigg this is certainly a question of your input, so I guess in the case you did a #distinct upfront?
@NedTwigg I was thinking about a limit(2).reduce(<some_magic>) solution, but it seems like fitting it into reduce is wrong on every level.
@NedTwigg cracked it .reduce(Optional.empty(), (a, b) -> a.isPresent() ^ b.isPresent() ? b : Optional.empty())
Gorgeous! Mind removing the Collector implementation? It doesn't actually answer the question, and your "hacky" answer is much more elegant and performant!
|
7

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.

1 Comment

The link is broken
6

A more concise way to build a Collector for this is as follows:

Collectors.reducing((a, b) -> null);

The reducing collector will store the first value, and then on successive passes, pass the current running value and the new value into the lambda expression. At this point, null can always be returned since this will not be called with the first value, which will simply be stored.

Plugging this into the code:

Optional<SomeProperty> universalCommonProperty = someList.stream()
    .map(obj -> obj.getSomeProperty())
    .distinct()
    .collect(Collectors.reducing((a, b) -> null));

Comments

4

You can easily write your own Collector

public class AllOrNothing<T> implements Collector<T, Set<T>, Optional<T>>{



@Override
public Supplier<Set<T>> supplier() {
    return () -> new HashSet<>();
}



@Override
public BinaryOperator<Set<T>> combiner() {
    return (set1, set2)-> {
        set1.addAll(set2);
        return set1;
    };
}

@Override
public Function<Set<T>, Optional<T>> finisher() {
    return (set) -> {
        if(set.size() ==1){
            return Optional.of(set.iterator().next());
        }
        return Optional.empty();
    };
}

@Override
public Set<java.util.stream.Collector.Characteristics> characteristics() {
    return Collections.emptySet();
}

@Override
public BiConsumer<Set<T>, T> accumulator() {
    return Set::add;
}

}

Which you can use like this:

   Optional<T> result = myStream.collect( new AllOrNothing<>());

Here's your example test data

public static void main(String[] args) {
    System.out.println(run());

    System.out.println(run(1));
    System.out.println(run(1,1));
    System.out.println(run(2,2));
    System.out.println(run(1,2));
}

private static Optional<Integer> run(Integer...ints){

    List<Integer> asList = Arrays.asList(ints);
    System.out.println(asList);
    return asList
                .stream()
                .collect(new AllOrNothing<>());
}

which when run will print out

[]
Optional.empty
[1]
Optional[1]
[1, 1]
Optional[1]
[2, 2]
Optional[2]

2 Comments

Is it possible to short-circuit terminate a stream rather than to collect a HashSet whose only valid contents will be a single element?
@NedTwigg yes, but you would have to write your own spliterator which kept track of it.
3

It seems RxJava has similar functionality in its single() operator.

single( ) and singleOrDefault( )

if the Observable completes after emitting a single item, return that item, otherwise throw an exception (or return a default item)

I'd rather just have an Optional, and I'd rather it be a Collector.

Comments

2

Guava has a collector for this called MoreCollectors.toOptional()

https://google.github.io/guava/releases/snapshot/api/docs/com/google/common/collect/MoreCollectors.html#toOptional--

Comments

1

Another collector approach:

Collectors:

public final class SingleCollector<T> extends SingleCollectorBase<T> {
    @Override
    public Function<Single<T>, T> finisher() {
        return a -> a.getItem();
    }
}

public final class SingleOrNullCollector<T> extends SingleCollectorBase<T> {
    @Override
    public Function<Single<T>, T> finisher() {
        return a -> a.getItemOrNull();
    }
}

SingleCollectorBase:

public abstract class SingleCollectorBase<T> implements Collector<T, Single<T>, T> {
    @Override
    public Supplier<Single<T>> supplier() {
        return () -> new Single<>();
    }

    @Override
    public BiConsumer<Single<T>, T> accumulator() {
        return (list, item) -> list.set(item);
    }

    @Override
    public BinaryOperator<Single<T>> combiner() {
        return (s1, s2) -> {
            s1.set(s2);
            return s1;
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return EnumSet.of(Characteristics.UNORDERED);
    }
}

Single:

public final class Single<T> {

    private T item;
    private boolean set;

    public void set(T item) {
        if (set) throw new SingleException("More than one item in collection");
        this.item = item;
        set = true;
    }

    public T getItem() {
        if (!set) throw new SingleException("No item in collection");
        return item;
    }

    public void set(Single<T> other) {
        if (!other.set) return;
        set(other.item);
    }

    public T getItemOrNull() {
        return set ? item : null;
    }
}

public class SingleException extends RuntimeException {
    public SingleException(String message) {
        super(message);
    }
}

Tests and example usages, albeit lacking parallel tests.

public final class SingleTests {

    @Test
    public void collect_single() {
        ArrayList<String> list = new ArrayList<>();
        list.add("ABC");

        String collect = list.stream().collect(new SingleCollector<>());
        assertEquals("ABC", collect);
    }

    @Test(expected = SingleException.class)
    public void collect_multiple_entries() {
        ArrayList<String> list = new ArrayList<>();
        list.add("ABC");
        list.add("ABCD");

        list.stream().collect(new SingleCollector<>());
    }

    @Test(expected = SingleException.class)
    public void collect_no_entries() {
        ArrayList<String> list = new ArrayList<>();

        list.stream().collect(new SingleCollector<>());
    }

    @Test
    public void collect_single_or_null() {
        ArrayList<String> list = new ArrayList<>();
        list.add("ABC");

        String collect = list.stream().collect(new SingleOrNullCollector<>());
        assertEquals("ABC", collect);
    }

    @Test(expected = SingleException.class)
    public void collect_multiple_entries_or_null() {
        ArrayList<String> list = new ArrayList<>();
        list.add("ABC");
        list.add("ABCD");

        list.stream().collect(new SingleOrNullCollector<>());
    }

    @Test
    public void collect_no_entries_or_null() {
        ArrayList<String> list = new ArrayList<>();

        assertNull(list.stream().collect(new SingleOrNullCollector<>()));
    }

}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.