如果只有一个值,则返回一个值的 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()) -&gt; Optional.empty()Stream.of(1).collect(singleOrEmpty()) -&gt; Optional.of(1) @NedTwigg 它会为你做不同的事情,因为你把所有的东西都收集到一个Set中。 我的例子做得不好,它强调了我问题的错误部分。我不想总是做distinct,有时我需要其他过滤器。 @NedTwigg 没关系。假设您想获得多个字符串的 singleOrEmpty 长度。 Stream.of("one", "two").map(String::length).collect(singleOrEmpty()) -&gt; Optional.of(3)(所有字符串的长度都是 3)Stream.of("one", "three").map(String::length).collect(singleOrEmpty()) -&gt; 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(&lt;some_magic&gt;) 解决方案,但似乎将其放入reduce 中在各个层面上都是错误的。 @NedTwigg 破解了它.reduce(Optional.empty(), (a, b) -&gt; 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 收集器 [重复]的主要内容,如果未能解决你的问题,请参考以下文章

如何在java中返回四分之三布尔值的答案

JAVA入门之方法

定义 Java 中的方法

ruby 编写一种方法模式,该模式将数字数组作为输入,并返回最常用值的数组。如果只有一个 -

查询收集返回多个值的子查询的最大值

java 8 Optional