可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
This question already has an answer here:
-
Filter Java Stream to 1 and only 1 element
17 answers
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()
回答1:
"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!
回答2:
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()
);
}
回答3:
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.
回答4:
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));
回答5:
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]
回答6:
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
.
回答7:
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<>()));
}
}
回答8:
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--