将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); }
…或使用您自己的Holder
types而不是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:
更新
好评的@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家,
我们可以使用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); }