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

我试图使用Java 8 __abc2查找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;
}
}

这段代码根据User的ID找到User。但是不能保证有多少__abc0匹配过滤器。

更改过滤器行为:

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

将抛出NoSuchElementException(很好!)

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

320998 次浏览

创建一个自定义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 by

  1. 使用Collectors.toList()收集器在List中收集对象。
  2. 在最后应用一个额外的结束符,返回单个元素——或者如果list.size != 1. 0则抛出IllegalStateException

用作:

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

然后,你可以随心所欲地自定义这个Collector,例如在构造函数中将异常作为参数,调整它以允许两个值,甚至更多。

另一种可能不那么优雅的解决方案是:

你可以使用一个包含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);

更新

@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,并获得唯一的元素。

番石榴提供了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

你试过这个吗

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

其他涉及编写自定义Collector的答案可能更有效(例如路易斯·沃瑟曼的, +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);
}

“逃生舱口”;让你做一些流不支持的奇怪的事情的操作是请求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行。

另一种选择是使用还原: (本例使用字符串,但可以轻松应用于包括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();

为了完整起见,下面是@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));

我们可以使用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();

如果没有找到用户或找到多个用户, 操作符将抛出异常。

如果你不介意使用第三方库,SequenceM from cyclops-streams(和simple-react from LazyFutureStream)都有单个&singleOptional运营商。

如果Stream中有0或多个1元素,则singleOptional()抛出异常,否则返回单个值。

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

如果Stream中没有值或有多个值,singleOptional()返回Optional.empty()

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

披露-我是这两个库的作者。

由于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更可读。

使用Guava的MoreCollectors.onlyElement() (源代码)。

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

用法:

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


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

番石榴有一个名为MoreCollectors.onlyElement()Collector

我正在使用这两个收集器:

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

使用Collector:

public static <T> Collector<T, ?, Optional<T>> singleElementCollector() {
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(singleElementCollector());

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

User user = result.orElseThrow();

这就把处理错误的负担放在了调用者身上——这是应该的。

使用减少

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

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

这样你就可以得到:

  • Optional -和你的对象一样,如果不存在Optional.empty()
  • 如果有多个元素,则使用Exception(最终使用YOUR自定义类型/消息)

我认为这种方式更简单:

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

受到@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(...));

如果你不使用Guava或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的要点

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

我使用的是整型而不是原语,因为它将有空指针异常。你只需要处理这个异常…看起来很简洁,我觉得;)

我自己尝试了一个示例代码,这里是解决方案。

User user = Stream.of(new User(2), new User(2), new User(1), new User(2))
.filter(u -> u.getAge() == 2).findFirst().get();

和用户类

class User {
private int age;


public User(int age) {
this.age = age;
}


public int getAge() {
return age;
}


public void setAge(int age) {
this.age = age;
}
}

使用Reduce和Optional

法比奥Bonfante响应:

public <T> T getOneExample(Collection<T> collection) {
return collection.stream()
.filter(x -> /* do some filter */)
.reduce((x,y)-> {throw new IllegalStateException("multiple");})
.orElseThrow(() -> new NoSuchElementException("none"));
}
public List<state> getAllActiveState() {
List<Master> master = masterRepository.getActiveExamMasters();
Master activeMaster = new Master();
try {
activeMaster = master.stream().filter(status -> status.getStatus() == true).reduce((u, v) -> {
throw new IllegalStateException();
}).get();
return stateRepository.getAllStateActiveId(activeMaster.getId());
} catch (IllegalStateException e) {
logger.info(":More than one status found TRUE in Master");
return null;
}
}
  1. 在上面的代码中,根据条件,如果它在列表中发现了一个以上的真值,那么它将通过异常。
  2. 当它通过错误将显示自定义消息,因为它便于维护服务器端日志。
  3. 从列表中出现的第n个元素开始,只要有一个元素为真状态,如果此时列表中有多个元素为真状态,则会发生异常。
  4. 在得到所有这些之后,我们使用get();从列表中取出一个元素并将其存储到另一个对象中。
  5. 如果你愿意,你可以添加像Optional<activeMaster > = master.stream().filter(status -> status.getStatus() == true).reduce((u, v) -> {throw new IllegalStateException();}).get();这样的optional