将 Java Stream 筛选为 1 个且仅 1 个元素

Filter Java Stream to 1 and only 1 element

提问人:ryvantage 提问时间:3/28/2014 最后编辑:Neuronryvantage 更新时间:5/2/2023 访问量:363323

问:

我正在尝试使用 Java 8 Streams 在 .但是,我想保证有一个且只有一个与筛选条件匹配。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 与过滤器匹配。UserUser

将滤波器线更改为:

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

会扔一个(好!NoSuchElementException

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

Lambda Java-8 Java

评论

0赞 Alexis C. 3/28/2014
count()是终端操作,所以你不能这样做。之后无法使用流。
0赞 ryvantage 3/28/2014
好的,谢谢@ZouZou。我不完全确定这种方法有什么作用。为什么没有?Stream::size
10赞 assylias 3/28/2014
@ryvantage 因为一个流只能使用一次:计算它的大小意味着“迭代”它,之后你就不能再使用这个流了。
4赞 ryvantage 3/28/2014
哇。那条评论帮助我比以前理解得更多......Stream
4赞 smac89 11/29/2017
这时,您意识到您需要一直使用(假设您希望保留广告订单)或 a。如果您的集合仅用于查找单个用户 ID,那么为什么要收集所有其他项目?如果有可能你总是需要找到一些用户ID,而这些用户ID也需要是唯一的,那么为什么要使用列表而不是集合呢?您正在向后编程。为工作使用正确的集合,省去这个麻烦LinkedHashSetHashSet

答:

296赞 skiwi 3/28/2014 #1

创建自定义收集器

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. 与收藏家一起收集我们的物品。ListCollectors.toList()
  2. 在末尾应用一个额外的完成器,这将返回单个元素 - 或抛出 if .IllegalStateExceptionlist.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);

评论

43赞 Tim Büthe 10/29/2015
番石榴将缩短这些解决方案并提供更好的错误消息。只是给已经使用谷歌番石榴的读者的提示。Iterables.getOnlyElement
2赞 denov 5/25/2016
我把这个想法总结成一个班级 - gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc
2赞 TWiStErRob 8/3/2016
自定义收集器仍然收集所有物品,也就是说,难道没有捷径吗?获取单个项目只需 1 个步骤即可完成,检查是否存在另一个项目也是 1 个步骤,无论筛选的流中还有多少项目。O(n)
2赞 Martijn Pieters 5/24/2018
@skiwi:Lonely 的编辑很有帮助且正确,所以我在审查后恢复了它。今天访问此答案的人并不关心您是如何得出答案的,他们不需要看到旧版本和新版本以及更新的部分。这会使您的答案更加混乱且没有帮助。最好将帖子置于最终状态,如果人们想查看这一切是如何进行的,他们可以查看帖子历史记录。
12赞 Javo 12/6/2018
我不得不说,我真的不喜欢辛格尔顿这个名字,因为这具有误导性。它不是它返回的单例,我认为这是编程中的保留字。这是一个“单个元素”或“一个实例”。
27赞 assylias 3/28/2014 #2

更新

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

评论

1赞 skiwi 3/28/2014
我会添加一个标识元素 () 来防止使用 .可悲的是,你的工作并不像你想象的那样工作,考虑一个有元素的,也许你认为你覆盖了它,但我可以,现在它不会抛出我认为的异常,除非我在这里弄错了。nullget()reduceStreamnull[User#1, null, User#2, null, User#3]
2赞 assylias 3/28/2014
@Skiwi如果存在 null 元素,则筛选器将首先抛出 NPE。
3赞 Holger 11/4/2016
由于您知道流无法传递给 reduction 函数,因此删除标识值参数将使函数中的整个处理变得过时:完成工作,甚至更好的是,它已经返回了一个 ,省去了调用结果的必要性。nullnullreduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )OptionalOptional.ofNullable
92赞 Louis Wasserman 3/28/2014 #3

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

...或者使用您自己的类型而不是 .您可以随心所欲地重复使用它。HolderAtomicReferenceCollector

评论

0赞 ryvantage 3/28/2014
@skiwi的singletonCollector比这更小,更容易理解,这就是我给他支票的原因。但很高兴看到答案中的共识:习俗是要走的路。Collector
1赞 Louis Wasserman 3/28/2014
很公平。我主要追求的是速度,而不是简洁。
1赞 ryvantage 3/28/2014
是的?为什么你的更快?
3赞 Louis Wasserman 3/28/2014
主要是因为分配一个 all-up 比单个可变引用更昂贵。List
2赞 Piotr Findeisen 8/17/2017
@LouisWasserman,最后的更新句实际上应该是第一个(也许是唯一:))MoreCollectors.onlyElement()
-3赞 pardeep131085 3/28/2014 #4

你试过这个吗

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

评论

3赞 ryvantage 3/29/2014
有人说不好用,因为它是终端操作。count()
0赞 Neuron 9/5/2018
如果这真的是引用,请添加您的来源
97赞 Stuart Marks 3/28/2014 #5

其他涉及编写自定义 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);

评论

7赞 ryvantage 3/29/2014
这个解决方案的意义何在?生成的列表是 2 还是 100 有什么区别?如果它大于 1。limit(2)
25赞 Stuart Marks 3/29/2014
如果找到第二个匹配项,它会立即停止。这就是所有花哨的收藏家所做的,只是使用更多的代码。:-)
11赞 Lukas Eder 1/11/2016
添加怎么样Collectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
1赞 alexbt 12/19/2017
Javadoc 对 limit 的参数是这样说的:.那么,它不应该代替吗?maxSize: the number of elements the stream should be limited to.limit(1).limit(2)
10赞 Stuart Marks 12/19/2017
@alexbt 问题陈述是确保正好有一个(不多也不少)匹配元素。在我的代码之后,可以进行测试以确保它等于 1。如果是 2,则有多个匹配项,因此这是一个错误。如果代码相反,则多个匹配将导致单个元素,这无法与只有一个匹配项区分开来。这将错过 OP 所关注的错误情况。result.size()limit(1)
35赞 Brian Goetz 5/25/2014 #6

“逃生舱口”操作可以让你做一些流不支持的奇怪事情,它要求一个: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

评论

6赞 anre 4/28/2016
Guava 的方法:Iterators.getOnlyElement(Iterator<T> iterator)。
17赞 prunge 5/13/2015 #7

另一种方法是使用减少: (此示例使用字符串,但可以轻松应用于任何对象类型,包括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();
172赞 glts 1/8/2016 #8

为了完整起见,这里是与@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));

评论

12赞 arin 6/8/2017
我喜欢这个答案中的初始方法。出于自定义目的,可以将最后一个转换为get()orElseThrow()
7赞 LordOfThePigs 1/23/2018
我喜欢这个的简洁性,以及它避免在每次调用时创建不必要的 List 实例的事实。
1赞 Matthew Wise 12/10/2020
如果您的用例允许流为空,请省略链末尾的 ,然后您将得到一个 如果流为空,则该流将为空,或者将填充单个元素。.get()Optional
0赞 Tomasz S 6/29/2021
我不认为这是一个很好的解决方案,因为在错误消息中,我们将只有前两个无效的元素,并且我们不会包含其中两个以上的值。
2赞 Qw3ry 3/31/2022
注意:行为与:两者都使流返回 ,但是在二进制运算符内部返回时会抛出 NPE。所以答案中的代码是有效的,但如果你把两个代码混在一起,它就不会了Stream#reduce(...)Stream#collect(Collectors.reducing(...))OptionalStream#reducenull
2赞 frhack 1/8/2016 #9

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

如果找不到一个或多个用户,则单个运算符将引发异常。

评论

0赞 Kalle Richter 4/14/2018
正确答案,初始化阻塞流或集合可能不是很便宜(就资源而言)。
1赞 John McClean 1/11/2016 #10

如果你不介意使用第三方库,来自 cyclops-streamsSequenceM(和来自 simple-reactLazyFutureStream)都有 single 和 singleOptional 运算符。

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

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

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

1赞 Arne Burmeister 9/8/2016 #11

由于使用抛出合并来处理具有相同键的多个条目,因此很容易: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();

您将获得一个重复的密钥。但最后,我不确定使用 .IllegalStateExceptionif

评论

1赞 glglgl 5/27/2017
很好的解决方案!如果你这样做了,你就会有一个更普遍的行为。.collect(Collectors.toMap(user -> "", Function.identity())).get("")
82赞 trevorade 11/12/2016 #12

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

它执行您想要的操作,如果流由两个或多个元素组成,则抛出一个,如果流为空,则抛出一个。IllegalArgumentExceptionNoSuchElementException

用法:

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

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

评论

2赞 qerub 12/12/2016
其他用户注意事项:是尚未发布(截至 2016-12 年)未发布的版本 21 的一部分。MoreCollectors
4赞 Emdadul Sawon 7/24/2017
这个答案应该更高。
11赞 Hans 1/25/2017 #13

Guava 有一个叫做 MoreCollectors.onlyElement() 的。Collector

1赞 Xavier Dury 5/16/2017 #14

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

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

评论

0赞 simon04 1/5/2018
整洁! 对 >1 个元素进行抛掷,对 0 个元素进行 NoSuchElementException' (in )。onlyOne()IllegalStateExceptionOptional::get
0赞 Xavier Dury 6/26/2018
@simon04 您可以重载方法以获取 .Supplier(Runtime)Exception
10赞 Neuron 5/25/2018 #15

使用收集器

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

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

19赞 Fabio Bonfante 8/24/2018 #16

使用 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(最终是您的自定义类型/消息)

评论

4赞 LordOfThePigs 7/1/2021
这显然是此页面上最优雅的解决方案。
0赞 Fabio Bonfante 9/1/2022
@LordOfThePigs 谢谢,鉴于此页面上也有 Brian Goetz 的答案,这真的意义重大;-)
22赞 pilladooo 9/12/2019 #17

我认为这种方式更简单:

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

评论

14赞 lczapski 9/12/2019
它只找到第一个,但当它不止一个时,情况也是抛出异常
0赞 Radek Postołowicz 6/21/2022
这是不好的做法。如果有 2 个或更多对象,则会导致非确定性行为。整个JDK是个坏主意。findFirst
0赞 Jacob Zimmerman 11/12/2022
@RadekPosto łowicz 1) 如果它是并行流,它不就是非确定性的吗?2)如果有多个项目适合过滤器,您多久关心一次是哪一个?(称其为“第一”可能是个坏主意) 3) 这些都不适用于这篇文章,因为 lczapski 说了什么。
0赞 Radek Postołowicz 11/14/2022
它是不确定的,因为它选择第一个元素,而可能有很多元素。没有明确的排序,它只是随机选择第一个。
-1赞 Nitin 3/28/2020 #18
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());

评论

6赞 David Buck 3/28/2020
虽然这段代码可以解决这个问题,但包括解释它如何以及为什么解决这个问题,将真正有助于提高你的帖子的质量,并可能导致更多的赞成票。请记住,您是在为将来的读者回答问题,而不仅仅是现在提问的人。请编辑您的答案以添加解释,并指出适用的限制和假设。
-1赞 JavAlex 9/13/2020 #19

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

评论

1赞 David Nouls 6/1/2021
此解决方案不会检测流中存在多个值的情况。所以它被忽视了。
0赞 JavAlex 6/2/2021
实际上,我只想获取流中的第一个元素。
1赞 David Nouls 6/3/2021
最初的问题想要一个也是唯一一个。接受的答案会引发异常。
1赞 LordOfThePigs 7/1/2021
是的。。。如果你想做同样的事情,你可以做与你在这里所做的完全等效且更具可读性的事情。stream.findFirst().orElse(null)
0赞 Overpass 10/14/2020 #20

如果您不使用 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

1赞 Aelaf 8/9/2021 #21
 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);

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

1赞 M-sAnNan 12/28/2021 #22

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

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;
 }
}
5赞 Nicolas Mafra 1/4/2022 #23

使用 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"));
}
0赞 Kalpesh Patil 2/11/2022 #24
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. 在上面的这段代码中,根据条件,如果它在列表中找到多个 true,那么它将通过异常。
  2. 当它通过错误时,将显示自定义消息,因为它很容易维护服务器端的日志。
  3. 从列表中存在的第 N 个元素开始,只需要一个元素具有 true 条件,如果列表中有多个元素在那一刻具有 true 状态,它将通过异常。
  4. 在得到所有这些之后,我们使用 get();从列表中获取一个元素并将其存储到另一个对象中。
  5. 如果你想要你添加可选的,比如Optional<activeMaster > = master.stream().filter(status -> status.getStatus() == true).reduce((u, v) -> {throw new IllegalStateException();}).get();