如果只有一个值,则返回一个值的 Java 8 收集器 [重复]
Posted
技术标签:
【中文标题】如果只有一个值,则返回一个值的 Java 8 收集器 [重复]【英文标题】:Java 8 Collector that returns a value if there's only a single value [duplicate] 【发布时间】:2015-01-04 18:48:40 【问题描述】:我对函数式编程和流的东西有点陌生,但我所知道的一点点却非常有用!
这种情况我遇到过好几次了:
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
我真正想要的是:
Optional<SomeProperty> universalCommonProperty = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.singleOrEmpty());
我认为singleOrEmpty
除了与distinct
结合使用之外,在其他情况下也很有用。当我还是一个 uber n00b 时,我花了很多时间重新发明 Java 集合框架,因为我不知道它在那里,所以我尽量不重复我的错误。 Java 是否有一个很好的方法来做这件事singleOrEmpty
?我是不是写错了?
谢谢!
编辑:这是distinct
案例的一些示例数据。如果忽略map
步骤:
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()
当我搞砸我的类型或有遗留代码时,我发现我需要这个。能够快速说出“这个集合的所有元素都共享这个属性,所以现在我可以使用这个共享属性执行一些操作”真是太好了。另一个例子是当用户多选一些不同的元素时,你试图看看你能做什么(如果有的话)对所有元素都有效。
EDIT2:抱歉,如果我的示例具有误导性。关键是singleOrEmpty。我通常发现我在前面放了一个distinct
,但它也可以很容易地成为其他类型的filter
。
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:我认为我通过激励 singleOrEmpty 而不是仅仅要求它自己搞砸了。
Optional<Int> value = someList.stream().collect(Collectors.singleOrEmpty())
[] -> Optional.empty()
[1] -> Optional.of(1)
[1, 1] -> Optional.empty()
【问题讨论】:
@Ned Twigg 你能好心点并发布你的 somelist 内容以便重现你的问题吗? 值得一提的是,在 C# 的 LINQ 中,这类似于SingleOrDefault
。
【参考方案1】:
这会产生创建集合的开销,但它很简单,即使您忘记先对流进行 distinct() 也能正常工作。
static<T> Collector<T,?,Optional<T>> singleOrEmpty()
return Collectors.collectingAndThen(
Collectors.toSet(),
set -> set.size() == 1
? set.stream().findAny()
: Optional.empty()
);
【讨论】:
对于您将 distinct 放在前面的情况,这是一个优雅的解决方案,但我的问题的根源是如何实现singleOrEmpty
。
@NedTwigg 我不确定我是否理解。这将通过您的所有单元测试。 Stream.of(1,2).collect(singleOrEmpty()) -> Optional.empty()
Stream.of(1).collect(singleOrEmpty()) -> Optional.of(1)
等
@NedTwigg 它会为你做不同的事情,因为你把所有的东西都收集到一个Set
中。
我的例子做得不好,它强调了我问题的错误部分。我不想总是做distinct
,有时我需要其他过滤器。
@NedTwigg 没关系。假设您想获得多个字符串的 singleOrEmpty 长度。 Stream.of("one", "two").map(String::length).collect(singleOrEmpty()) -> Optional.of(3)
(所有字符串的长度都是 3)Stream.of("one", "three").map(String::length).collect(singleOrEmpty()) -> Optional.empty()
因为长度不同【参考方案2】:
仅评估前两个元素的“Hacky”解决方案:
.limit(2)
.map(Optional::ofNullable)
.reduce(Optional.empty(),
(a, b) -> a.isPresent() ^ b.isPresent() ? b : Optional.empty());
一些基本解释:
单个元素 [1] -> 映射到 [Optional(1)] -> 减少
"Empty XOR Present" yields Optional(1)
= 可选(1)
两个元素 [1, 2] -> 映射到 [Optional(1), Optional(2)] -> 减少:
"Empty XOR Present" yields Optional(1)
"Optional(1) XOR Optional(2)" yields Optional.Empty
= 可选。空
这是完整的测试用例:
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)));
向贡献了 XOR 想法和上述测试用例的 Ned(OP)致敬!
【讨论】:
是否可以短路终止流而不是收集唯一有效内容为单个元素的 HashSet? @NedTwigg 这肯定是您输入的问题,所以我猜您是否预先做了#distinct
?
@NedTwigg 我正在考虑一个limit(2).reduce(<some_magic>)
解决方案,但似乎将其放入reduce 中在各个层面上都是错误的。
@NedTwigg 破解了它.reduce(Optional.empty(), (a, b) -> a.isPresent() ^ b.isPresent() ? b : Optional.empty())
华丽!介意删除收集器实现吗?它实际上并没有回答问题,而您的“hacky”答案更加优雅和高效!【参考方案3】:
如果您不介意使用Guava,您可以使用Iterables.getOnlyElement
包装您的代码,所以它看起来像这样:
SomeProperty distinctProperty = Iterables.getOnlyElement(
someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.toList()));
IllegalArgumentException
会在有多个值或没有值时产生,还有一个version 有默认值。
【讨论】:
链接失效【参考方案4】:为此构建收集器的更简洁方法如下:
Collectors.reducing((a, b) -> null);
减少收集器将存储第一个值,然后在连续传递中,将当前运行值和新值传递给 lambda 表达式。此时,始终可以返回 null,因为不会使用第一个值调用 this,而是将其简单地存储。
将其插入代码中:
Optional<SomeProperty> universalCommonProperty = someList.stream()
.map(obj -> obj.getSomeProperty())
.distinct()
.collect(Collectors.reducing((a, b) -> null));
【讨论】:
【参考方案5】:您可以轻松编写自己的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;
你可以这样使用:
Optional<T> result = myStream.collect( new AllOrNothing<>());
这是您的示例测试数据
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<>());
运行时会打印出来
[]
Optional.empty
[1]
Optional[1]
[1, 1]
Optional[1]
[2, 2]
Optional[2]
【讨论】:
是否可以短路终止流而不是收集唯一有效内容为单个元素的 HashSet? @NedTwigg 是的,但是您必须自己编写spliterator
来跟踪它。【参考方案6】:
RxJava 似乎有类似的功能in its single()
operator。
single( )
和singleOrDefault( )
如果
Observable
在发出单个项目后完成,则返回该项目,否则抛出异常(或返回默认项目)
我宁愿只有一个Optional
,我宁愿它是一个Collector
。
【讨论】:
【参考方案7】:Guava 有一个收集器,称为 MoreCollectors.toOptional()
https://google.github.io/guava/releases/snapshot/api/docs/com/google/common/collect/MoreCollectors.html#toOptional--
【讨论】:
【参考方案8】:另一种收集器方法:
收藏家:
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);
单身:
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);
测试和示例用法,尽管缺乏并行测试。
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<>()));
【讨论】:
以上是关于如果只有一个值,则返回一个值的 Java 8 收集器 [重复]的主要内容,如果未能解决你的问题,请参考以下文章