【问题标题】:Java 8 Collector that returns a value if there's only a single value [duplicate]如果只有一个值,则返回一个值的 Java 8 收集器 [重复]
【发布时间】: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

标签: java java-8 java-stream


【解决方案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】:

      【讨论】:

        【解决方案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<>()));
            }
        
        }
        

        【讨论】:

          猜你喜欢
          • 2015-01-04
          • 1970-01-01
          • 2018-03-26
          • 2019-06-12
          • 2018-04-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多