提问人:ryvantage 提问时间:3/28/2014 最后编辑:Neuronryvantage 更新时间:5/2/2023 访问量:363323
将 Java Stream 筛选为 1 个且仅 1 个元素
Filter Java Stream to 1 and only 1 element
问:
我正在尝试使用 Java 8 Stream
s 在 .但是,我想保证有一个且只有一个与筛选条件匹配。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 查找。但不能保证有多少 s 与过滤器匹配。User
User
将滤波器线更改为:
User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();
会扔一个(好!NoSuchElementException
不过,如果有多个匹配项,我希望它抛出错误。有没有办法做到这一点?
答:
创建自定义收集器
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
- 与收藏家一起收集我们的物品。
List
Collectors.toList()
- 在末尾应用一个额外的完成器,这将返回单个元素 - 或抛出 if .
IllegalStateException
list.size != 1
用作:
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);
评论
Iterables.getOnlyElement
O(n)
更新
@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
或者,您可以将 reduction 与可选的 :
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 并获取唯一的元素。
评论
null
get()
reduce
Stream
null
[User#1, null, User#2, null, User#3]
null
null
reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )
Optional
Optional.ofNullable
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
评论
Collector
List
MoreCollectors.onlyElement()
你试过这个吗
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
评论
count()
其他涉及编写自定义 Collector
的答案可能更有效(例如 Louis Wasserman 的,+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);
评论
limit(2)
Collectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
maxSize: the number of elements the stream should be limited to
.limit(1)
.limit(2)
result.size()
limit(1)
“逃生舱口”操作可以让你做一些流不支持的奇怪事情,它要求一个: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 有一种方便的方法可以获取 an 并获取唯一的元素,如果有零个或多个元素,则抛出,这可以替换此处的底部 n-1 线。Iterator
评论
另一种方法是使用减少:
(此示例使用字符串,但可以轻松应用于任何对象类型,包括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));
评论
get()
orElseThrow()
.get()
Optional
Stream#reduce(...)
Stream#collect(Collectors.reducing(...))
Optional
Stream#reduce
null
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();
如果找不到一个或多个用户,则单个运算符将引发异常。
评论
如果你不介意使用第三方库,来自 cyclops-streams 的 SequenceM
(和来自 simple-react 的 LazyFutureStream
)都有 single 和 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
披露 - 我是这两个库的作者。
由于使用抛出合并来处理具有相同键的多个条目,因此很容易: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
评论
.collect(Collectors.toMap(user -> "", Function.identity())).get("")
使用 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
Guava 有一个叫做 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);
}
评论
onlyOne()
IllegalStateException
Optional::get
Supplier
(Runtime)Exception
使用收集器
:
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();
这就把处理错误的负担放在了调用者身上 - 这是应该的。
使用 reduce
这是我发现的更简单、更灵活的方法(基于@prunge答案)
Optional<User> user = users.stream()
.filter(user -> user.getId() == 1)
.reduce((a, b) -> {
throw new IllegalStateException("Multiple elements: " + a + ", " + b);
})
这样,您可以获得:
- 可选 - 与对象一样,或者如果不存在
Optional.empty()
- 如果有多个元素,则为 Exception(最终是您的自定义类型/消息)
评论
我认为这种方式更简单:
User resultUser = users.stream()
.filter(user -> user.getId() > 0)
.findFirst().get();
评论
findFirst
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(...));
评论
stream.findFirst().orElse(null)
如果您不使用 Guava 或 Kotlin,这里有一个基于@skiwi和@Neuron答案的解决方案。
users.stream().collect(single(user -> user.getId() == 1));
或
users.stream().collect(optional(user -> user.getId() == 1));
其中 和 是静态导入的函数,返回相应的收集器。single
optional
我推断,如果将过滤逻辑移到收集器内部,它看起来会更简洁。此外,如果您碰巧删除了带有 ..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();
和 User 类
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
来自 Fabio 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;
}
}
- 在上面的这段代码中,根据条件,如果它在列表中找到多个 true,那么它将通过异常。
- 当它通过错误时,将显示自定义消息,因为它很容易维护服务器端的日志。
- 从列表中存在的第 N 个元素开始,只需要一个元素具有 true 条件,如果列表中有多个元素在那一刻具有 true 状态,它将通过异常。
- 在得到所有这些之后,我们使用 get();从列表中获取一个元素并将其存储到另一个对象中。
- 如果你想要你添加可选的,比如
Optional<activeMaster > = master.stream().filter(status -> status.getStatus() == true).reduce((u, v) -> {throw new IllegalStateException();}).get();
评论
count()
是终端操作,所以你不能这样做。之后无法使用流。Stream::size
Stream
LinkedHashSet
HashSet