将Java Stream过滤为1并且只有1个元素

时间:2014-03-27 17:25:33

标签: java lambda java-8 java-stream

我正在尝试使用Java 8 Stream来查找LinkedList中的元素。但是,我想保证过滤条件只有一个匹配。

拿这段代码:

public static void main(String[] args) {

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));

    User match = users.stream().filter((user) -> user.getId() == 1).findAny().get();
    System.out.println(match.toString());
}

static class User {

    @Override
    public String toString() {
        return id + " - " + username;
    }

    int id;
    String username;

    public User() {
    }

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public int getId() {
        return id;
    }
}

此代码根据其ID找到User。但是无法保证有多少User匹配过滤器。

将过滤行更改为:

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();

将抛出NoSuchElementException(好!)

但是,如果有多个匹配项,我希望它会抛出错误。有没有办法做到这一点?

22 个答案:

答案 0 :(得分:147)

创建自定义Collector

public static <T> Collector<T, ?, T> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                if (list.size() != 1) {
                    throw new IllegalStateException();
                }
                return list.get(0);
            }
    );
}

我们使用Collectors.collectingAndThen

构建我们想要的Collector
  1. 使用List收集器在Collectors.toList()收集我们的对象。
  2. 在最后应用额外的修整器,返回单个元素 - 如果IllegalStateException则抛出list.size != 1
  3. 用作:

    User resultUser = users.stream()
            .filter(user -> user.getId() > 0)
            .collect(toSingleton());
    

    然后,您可以根据需要自定义此Collector,例如,在构造函数中将异常作为参数提供,调整它以允许两个值,等等。

    另一种选择 - 可以说不那么优雅 - 解决方案:

    您可以使用&#39;解决方法&#39;这涉及peek()AtomicInteger,但实际上你不应该使用它。

    你可以做的就是在List收集它,就像这样:

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));
    List<User> resultUserList = users.stream()
            .filter(user -> user.getId() == 1)
            .collect(Collectors.toList());
    if (resultUserList.size() != 1) {
        throw new IllegalStateException();
    }
    User resultUser = resultUserList.get(0);
    

答案 1 :(得分:91)

为了完整起见,这里是@ one-liner'对应@ prunge的优秀答案:

User user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })
        .get();

这从流中获取唯一匹配元素,抛出

    如果流为空,则
  • NoSuchElementException
  • IllegalStateException以防流包含多个匹配元素。

这种方法的一种变体可以避免提前抛出异常,而是将结果表示为包含唯一元素的Optional,如果有零个或多个元素则表示没有(空):

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));

答案 2 :(得分:78)

其他涉及撰写自定义Collector的答案可能更有效(例如Louis Wasserman's,+ 1),但如果您想要简洁,我建议如下:

List<User> result = users.stream()
    .filter(user -> user.getId() == 1)
    .limit(2)
    .collect(Collectors.toList());

然后验证结果列表的大小。

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}

答案 3 :(得分:50)

Guava提供了MoreCollectors.onlyElement(),它在这里做了正确的事情。但是如果你必须自己动手,你可以为此推出自己的Collector

<E> Collector<E, ?, Optional<E>> getOnly() {
  return Collector.of(
    AtomicReference::new,
    (ref, e) -> {
      if (!ref.compareAndSet(null, e)) {
         throw new IllegalArgumentException("Multiple values");
      }
    },
    (ref1, ref2) -> {
      if (ref1.get() == null) {
        return ref2;
      } else if (ref2.get() != null) {
        throw new IllegalArgumentException("Multiple values");
      } else {
        return ref1;
      }
    },
    ref -> Optional.ofNullable(ref.get()),
    Collector.Characteristics.UNORDERED);
}

...或使用您自己的Holder类型而不是AtomicReference。您可以根据需要重复使用Collector

答案 4 :(得分:36)

使用Guava的MoreCollectors.onlyElement()JavaDoc)。

如果流包含两个或更多元素,它会执行您想要的操作并抛出IllegalArgumentException,如果流为空,则抛出NoSuchElementException

用法:

import static com.google.common.collect.MoreCollectors.onlyElement;

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());

答案 5 :(得分:28)

&#34;逃生舱&#34;允许你做流不支持的奇怪事情的操作是要求Iterator

Iterator<T> it = users.stream().filter((user) -> user.getId() < 0).iterator();
if (!it.hasNext()) 
    throw new NoSuchElementException();
else {
    result = it.next();
    if (it.hasNext())
        throw new TooManyElementsException();
}

Guava有一个方便的方法来获取Iterator并获得唯一的元素,如果有零个或多个元素则抛出,这可以替换这里的底部n-1行。

答案 6 :(得分:19)

更新

来自@Holger的评论中的好建议:

Optional<User> match = users.stream()
              .filter((user) -> user.getId() > 1)
              .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });

原始答案

Optional#get引发异常,但如果您有多个元素无效。您可以收集仅接受一个项目的集合中的用户,例如:

User match = users.stream().filter((user) -> user.getId() > 1)
                  .collect(toCollection(() -> new ArrayBlockingQueue<User>(1)))
                  .poll();

引发了java.lang.IllegalStateException: Queue full,但感觉实在太过分了。

或者您可以使用缩减结合可选项:

User match = Optional.ofNullable(users.stream().filter((user) -> user.getId() > 1)
                .reduce(null, (u, v) -> {
                    if (u != null && v != null)
                        throw new IllegalStateException("More than one ID found");
                    else return u == null ? v : u;
                })).get();

这种减少基本上会回归:

    如果找不到用户,则
  • null
  • 如果只找到一个用户
  • 如果发现多个,则抛出异常

然后将结果包装在一个可选的。

但最简单的解决方案可能只是收集到一个集合,检查它的大小是否为1并获得唯一的元素。

答案 7 :(得分:11)

另一种方法是使用减少: (此示例使用字符串,但可以轻松应用于任何对象类型,包括User

List<String> list = ImmutableList.of("one", "two", "three", "four", "five", "two");
String match = list.stream().filter("two"::equals).reduce(thereCanBeOnlyOne()).get();
//throws NoSuchElementException if there are no matching elements - "zero"
//throws RuntimeException if duplicates are found - "two"
//otherwise returns the match - "one"
...

//Reduction operator that throws RuntimeException if there are duplicates
private static <T> BinaryOperator<T> thereCanBeOnlyOne()
{
    return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);};
}

因此对于User的情况,你会有:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();

答案 8 :(得分:5)

使用Collector

public static <T> Collector<T, ?, Optional<T>> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty()
    );
}

用法:

Optional<User> result = users.stream()
        .filter((user) -> user.getId() < 0)
        .collect(toSingleton());

我们返回Optional,因为我们通常不能假设Collection只包含一个元素。如果您已经知道这种情况,请致电:

User user = result.orElseThrow();

这会给错误的人带来负担 - 就像它应该的那样。

答案 9 :(得分:4)

Guava有一个名为MoreCollectors.onlyElement()Collector

答案 10 :(得分:3)

我认为这种方式更简单:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();

答案 11 :(得分:1)

我们可以使用RxJava(非常强大的reactive extension库)

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

User userFound =  Observable.from(users)
                  .filter((user) -> user.getId() == 1)
                  .single().toBlocking().first();

如果找不到用户或多个用户,则 operator会引发异常。

答案 12 :(得分:1)

如果您不介意使用第三方库,SequenceM来自cyclops-streams(以及来自LazyFutureStreamsimple-react),则只需使用单个和第二方库。 singleOptional运算符。

如果singleOptional()中有0个或多个1个元素,则

Stream会引发异常,否则会返回单个值。

String result = SequenceM.of("x")
                          .single();

SequenceM.of().single(); // NoSuchElementException

SequenceM.of(1, 2, 3).single(); // NoSuchElementException

String result = LazyFutureStream.fromStream(Stream.of("x"))
                          .single();
如果singleOptional()中没有值或多个值,则

Optional.empty()会返回Stream

Optional<String> result = SequenceM.fromStream(Stream.of("x"))
                          .singleOptional(); 
//Optional["x"]

Optional<String> result = SequenceM.of().singleOptional(); 
// Optional.empty

Optional<String> result =  SequenceM.of(1, 2, 3).singleOptional(); 
// Optional.empty

披露 - 我是两个图书馆的作者。

答案 13 :(得分:1)

由于Collectors.toMap(keyMapper, valueMapper)使用投掷合并来处理具有相同密钥的多个条目,因此很容易:

List<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

int id = 1;
User match = Optional.ofNullable(users.stream()
  .filter(user -> user.getId() == id)
  .collect(Collectors.toMap(User::getId, Function.identity()))
  .get(id)).get();

您将获得IllegalStateException重复密钥。但最后我不确定使用if代码是否更具可读性。

答案 14 :(得分:1)

我正在使用这两个收藏家:

public static <T> Collector<T, ?, Optional<T>> zeroOrOne() {
    return Collectors.reducing((a, b) -> {
        throw new IllegalStateException("More than one value was returned");
    });
}

public static <T> Collector<T, ?, T> onlyOne() {
    return Collectors.collectingAndThen(zeroOrOne(), Optional::get);
}

答案 15 :(得分:1)

使用reduce

这是我发现的更简单,更灵活的方法(基于@prunge答案)

Optional<User> user = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })

通过这种方式获得:

  • 可选-与对象一样,或者Optional.empty()(如果不存在)
  • 如果有多个元素,则为异常(最终会带有您的自定义类型/消息)

答案 16 :(得分:0)

我采用了直接方法并且刚刚实施了这个方法:

public class CollectSingle<T> implements Collector<T, T, T>, BiConsumer<T, T>, Function<T, T>, Supplier<T> {
T value;

@Override
public Supplier<T> supplier() {
    return this;
}

@Override
public BiConsumer<T, T> accumulator() {
    return this;
}

@Override
public BinaryOperator<T> combiner() {
    return null;
}

@Override
public Function<T, T> finisher() {
    return this;
}

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

@Override //accumulator
public void accept(T ignore, T nvalue) {
    if (value != null) {
        throw new UnsupportedOperationException("Collect single only supports single element, "
                + value + " and " + nvalue + " found.");
    }
    value = nvalue;
}

@Override //supplier
public T get() {
    value = null; //reset for reuse
    return value;
}

@Override //finisher
public T apply(T t) {
    return value;
}


} 

使用JUnit测试:

public class CollectSingleTest {

@Test
public void collectOne( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    Integer o = lst.stream().collect( new CollectSingle<>());
    System.out.println(o);
}

@Test(expected = UnsupportedOperationException.class)
public void failOnTwo( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    lst.add(8);
    Integer o = lst.stream().collect( new CollectSingle<>());
}

}

此实现线程安全。

答案 17 :(得分:0)

User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());

答案 18 :(得分:0)

受@skiwi的启发,我通过以下方式解决了该问题:

public static <T> T toSingleton(Stream<T> stream) {
    List<T> list = stream.limit(1).collect(Collectors.toList());
    if (list.isEmpty()) {
        return null;
    } else {
        return list.get(0);
    }
}

然后:

User user = toSingleton(users.stream().filter(...).map(...));

答案 19 :(得分:0)

如果您不使用番石榴或Kotlin,这是基于@skiwi和@Neuron答案的解决方案。

users.stream().collect(single(user -> user.getId() == 1));

users.stream().collect(optional(user -> user.getId() == 1));

其中singleoptional是静态导入的函数,它们返回相应的收集器。

我认为,如果将过滤逻辑移到收集器内部,它将看起来更加简洁。如果您碰巧用.filter删除了字符串,那么代码也不会中断。

代码https://gist.github.com/overpas/ccc39b75f17a1c65682c071045c1a079

的要点

答案 20 :(得分:0)

 List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
Integer value  = list.stream().filter((x->x.intValue()==8)).findFirst().orElse(null);

我使用了整数类型而不是原始类型,因为它会有空指针异常。你只需要处理这个异常...看起来很简洁,我认为;)

答案 21 :(得分:-2)

你试过这个吗?

long c = users.stream().filter((user) -> user.getId() == 1).count();
if(c > 1){
    throw new IllegalStateException();
}

long count()
Returns the count of elements in this stream. This is a special case of a reduction and is equivalent to:

     return mapToLong(e -> 1L).sum();

This is a terminal operation.

来源:https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html