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

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

拿这个代码:

 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; } } 

这个代码根据他们的IDfind一个User 。 但是没有保证多lessUser匹配filter。

将filter行更改为:

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

会抛出一个NoSuchElementException (好!)

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

从技术上讲,有一个丑陋的“解决方法”,涉及peek()和一个AtomicInteger ,但你真的不应该使用它。

我在这些情况下所做的只是将其收集在一个列表中,如下所示:

 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); 

我不知道在API中执行此操作的方法,同时我将介绍另一个涉及自定义元素的示例。

更新 ,你应该为此创build自己的Collector

 public static <T> Collector<T, List<T>, T> singletonCollector() { return Collector.of( ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }, list -> { if (list.size() != 1) { throw new IllegalStateException(); } return list.get(0); } ); } 

它所做的是:

  • 它模仿Collectors.toList()收集器。
  • 它在最后应用一个额外的装订器,引发exception,或者如果没有exception,则返回列表的第一个元素。

用作:

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

然后,你可以自定义这个singletonCollector ,只要你想要的,例如在构造函数中给出exception作为参数,调整它以允许两个值,等等。

新的更新 ,我再次修改我的旧回答singletonCollector() ,它可以实际上是这样得到的:

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

哇,这么复杂! :-)其他涉及编写自定义收集器的答案可能更有效(如Louis Wasserman's ,+1),但是如果您想要简洁,build议如下:

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

然后validation结果列表的大小。

为了完整起见,这里是与@ prunge的优秀答案相对应的“单行”

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

这从stream中获得唯一的匹配元素,抛出

  • NoSuchElementException在stream为空的情况下,或者
  • 如果stream包含多个匹配元素,则IllegalStateException

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

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

你可以推出你自己的collections家:

 <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); } 

…或使用您自己的Holdertypes而不是AtomicReference 。 您可以尽可能多地重复使用该Collector

(更新:番石榴提供MoreCollectors.onlyElement()在这里做正确的事情)

“逃生孵化”操作可以让你做怪异的事情,而不是stream支持的是要求一个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(); } 

番石榴有一个方便的方法来取一个Iterator并得到唯一的元素,抛出如果有零个或多个元素,这可以取代底部的n-1行。

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

它做你想做的,如果stream包含两个或多个元素,则抛出IllegalArgumentException如果stream为空,则抛出NoSuchElementException

例:

 import static com.google.common.collect.MoreCollectors.onlyElement; User match = users.stream().filter((user) -> user.getId() < 0).collect(onlyElement()); 

更新(2017-05-30):

这个方法现在是公开的,从Guava 21开始。下面是这个方法的最新的JavaDocs:

http://google.github.io/guava/releases/23.0/api/docs/com/google/common/collect/MoreCollectors.html#onlyElement–

更新

好评的@Holgerbuild议:

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

原始答案

Optional#get #get引发exception,但如果有多个元素不起作用。 您可以收集只接受一个项目的集合中的用户,例如:

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

它抛出一个java.lang.IllegalStateException: Queue full ,但感觉太hacky。

或者你可以使用一个减less结合可选:

 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(); 

这个减less实质上是返回

  • 如果没有find用户,则返回null
  • 用户,如果只有一个被发现
  • 如果find多个,则会引发exception

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

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

另一种方法是使用reduction(这个例子使用了string,但是可以很容易地应用到任何对象types,包括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(); 

番石榴有一个叫做MoreCollectors.onlyElement()的collections家,

https://google.github.io/guava/releases/snapshot/api/docs/com/google/common/collect/MoreCollectors.html#onlyElement–

我们可以使用RxJava (非常强大的反应扩展库)

 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(); 

如果没有用户或多个用户被发现,那么单个 运算符会引发exception。

由于Collectors.toMap(keyMapper, valueMapper)使用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

你有没有尝试过

  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. 

如果您不介意使用第三方库,则Cyclops stream中的 SequenceM (以及简单反应中的 LazyFutureStream )都有单个和单个可选操作符。

如果stream中有0个或多个元素,singleOptional将抛出exception,否则返回单个值。

  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(); 

single可选返回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 

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

我正在使用这两个collections家:

 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); }