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

ryv*_*age 202 java lambda java-8 java-stream

我正在尝试使用Java 8 Stream来查找a中的元素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());
}
Run Code Online (Sandbox Code Playgroud)
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;
    }
}
Run Code Online (Sandbox Code Playgroud)

此代码User根据其ID找到.但是无法保证有多少User匹配过滤器.

将过滤线更改为:

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();
Run Code Online (Sandbox Code Playgroud)

会扔一个NoSuchElementException(好!)

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

ski*_*iwi 170

创建自定义 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);
            }
    );
}
Run Code Online (Sandbox Code Playgroud)

我们Collectors.collectingAndThen用来构建我们想要Collector

  1. 收集我们的对象ListCollectors.toList()收藏家.
  2. 在末尾应用额外的修整器,返回单个元素 - 或抛出IllegalStateExceptionif list.size != 1.

用作:

User resultUser = users.stream()
        .filter(user -> user.getId() > 0)
        .collect(toSingleton());
Run Code Online (Sandbox Code Playgroud)

然后,您可以根据需要自定义此项Collector,例如,在构造函数中将异常作为参数提供,调整它以允许两个值,等等.

一种替代方案 - 可以说是不太优雅 - 解决方案

您可以使用涉及peek()和的"解决方法" AtomicInteger,但实际上您不应该使用它.

你能做什么istead只是收集它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);
Run Code Online (Sandbox Code Playgroud)

  • Guava的`Iterables.getOnlyElement`将缩短这些解决方案并提供更好的错误消息.就像已经使用Google Guava的读者一样. (18认同)
  • 我不得不说我真的不喜欢 toSingleton 这个名字,因为那是误导。它不是返回的单例,我认为它是编程中的保留字。这是一个“单个元素”或“一个实例”。 (5认同)
  • 我将这个想法包装成一个类 - https://gist.github.com/denov/a7eac36a3cda041f8afeabcef09d16fc (2认同)
  • 自定义收集器还是把所有的item都收集起来,也就是`O(n)`,难道没有捷径吗?获取单个项目可以在 1 步中完成,检查另一个项目是否存在也是 1 步,无论过滤流中还有多少项目。 (2认同)
  • @skiwi:孤独的编辑是有帮助的,也是正确的,所以我在审核后重新设置了它.今天访问此答案的人不关心你是如何得到答案的,他们不需要看到旧版本和新版本以及*更新*部分.这使得您的回答更加混乱,而且帮助不大.将帖子置于*最终状态*会好得多,如果人们希望看到它们如何发挥作用,他们可以查看帖子历史记录. (2认同)
  • @skiwi:答案中的代码*绝对*你写的。编辑所做的只是清理你的帖子,只是 *删除* 一个早期版本的 `singletonCollector()` 定义被保留在帖子中的版本废弃,并将其重命名为 `toSingleton()`。我的 Java 流专业知识有点生疏,但重命名对我很有帮助。审核此更改花了我 2 分钟,最多。如果您没有时间查看编辑,我是否建议您以后请其他人来执行此操作,也许在 [Java 聊天室](https://chat.stackoverflow.com/rooms/info/139/爪哇)? (2认同)

glt*_*lts 102

为了完整起见,这里是@ one-liner'对应@ prunge的优秀答案:

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

这从流中获取唯一匹配元素,抛出

  • NoSuchElementException 如果流是空的,或者
  • IllegalStateException 如果流包含多个匹配元素.

这种方法的一种变体可以避免提前抛出异常,而是将结果表示为Optional包含唯一元素,或者如果存在零个或多个元素则表示无结果(空):

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

  • 我喜欢此答案中的初始方法。出于自定义目的,可以将最后一个`get()`转换为`orElseThrow()`。 (3认同)
  • 我喜欢这个的简洁性,以及它避免每次调用时都创建一个不必要的 List 实例的事实。 (3认同)
  • 注意:“Stream#reduce(...)”的行为与“Stream#collect(Collectors.reducing(...))”不同:两者都使流返回“Optional”,但是“Stream#reduce”会抛出异常然而,当在二元运算符内返回“null”时,会出现 NPE。所以答案中的代码可以工作,但如果你混淆了两个代码片段,它就不会工作 (2认同)

Stu*_*rks 80

其他涉及编写习惯的答案Collector可能更有效(例如Louis Wasserman's,+ 1),但如果你想要简洁,我建议如下:

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

然后验证结果列表的大小.

  • 如果发现第二场比赛,它会立即停止.这就是所有花哨的收藏家所做的,只需使用更多代码.:-) (17认同)
  • 如何添加`Collectors.collectingAndThen(toList(),l - > {if(l.size()== 1)返回l.get(0);抛出新的RuntimeException();})` (10认同)
  • @alexbt问题陈述是为了确保只有一个(不多,不少)匹配元素.在我的代码之后,可以测试`result.size()`以确保它等于1.如果它是2,那么有多个匹配,所以这是一个错误.如果代码改为`limit(1)`,则多个匹配将导致单个元素,这与单个元素无法区分.这将错过OP担心的错误案例. (5认同)
  • 在这个解决方案中,限制(2)`有什么意义?结果列表是2还是100会有什么不同?如果它大于1. (4认同)

Lou*_*man 63

番石榴提供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);
}
Run Code Online (Sandbox Code Playgroud)

...或使用您自己的Holder类型而不是AtomicReference.您可以根据需要重复使用它Collector.

  • 主要是因为分配全部`List`比单个可变引用更昂贵. (3认同)
  • @LouisWasserman,关于 `MoreCollectors.onlyElement()` 的最终更新语句实际上应该是第一个(也许是唯一的:)) (2认同)

小智 41

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

IllegalArgumentException如果流包含两个或更多元素,它会执行您想要的操作并抛出一个,NoSuchElementException如果流是空的,则抛出一个.

用法:

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

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
Run Code Online (Sandbox Code Playgroud)

  • 其他用户注意:`MoreCollectors` 是尚未发布(截至 2016-12 年)未发布版本 21 的一部分。 (2认同)
  • 这个答案应该更高. (2认同)

Bri*_*etz 30

"逃生舱"操作可以让你做一些流不支持的怪异事情,就是要求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();
}
Run Code Online (Sandbox Code Playgroud)

番石榴有一种方便的方法来获取Iterator并获得唯一的元素,如果有零个或多个元素则抛出,这可以替换这里的底部n-1行.

  • Guava的方法:Iterators.getOnlyElement(Iterator <T> iterator). (4认同)

ass*_*ias 21

更新

来自@Holger的评论中的好建议:

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

原始答案

抛出异常Optional#get,但是如果你有多个元素无济于事.您可以收集仅接受一个项目的集合中的用户,例如:

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

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

或者你可以使用减少与可选:

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();
Run Code Online (Sandbox Code Playgroud)

减少基本上返回:

  • 如果找不到用户,则返回null
  • 用户如果只找到一个
  • 如果找到多个异常,则抛出异常

然后将结果包装在一个可选项中.

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

  • @Skiwi如果有空元素,过滤器将首先抛出NPE. (2认同)
  • 既然你知道流不能将`null`传递给reduce函数,那么删除identity value参数会使函数中的`null`处理完全废弃:`reduce((u,v) - > {throw new IllegalStateException("找到多个ID");})`完成工作,甚至更好,它已经返回一个`Optional`,忽略了对结果调用`Optional.ofNullable`的必要性. (2认同)

小智 17

我认为这种方式更简单:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();
Run Code Online (Sandbox Code Playgroud)

  • 它只找到第一个,但当它超过一个时也会抛出异常 (8认同)

Fab*_*nte 13

使用减少

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

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

通过这种方式您可以获得:

  • Optional - 与您的对象一样,或者Optional.empty()如果不存在
  • 如果有多个元素,则异常(最终带有您的自定义类型/消息)

  • 这显然是此页面上最优雅的解决方案。 (2认同)

pru*_*nge 12

另一种方法是使用简化:(此示例使用字符串,但可以轻松应用于任何对象类型,包括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);};
}
Run Code Online (Sandbox Code Playgroud)

因此,对于User您将拥有的情况:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();
Run Code Online (Sandbox Code Playgroud)


Han*_*ans 5

番石榴对此有一个Collector称呼MoreCollectors.onlyElement()


Neu*_*ron 5

使用Collector:

public static <T> Collector<T, ?, Optional<T>> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty()
    );
}
Run Code Online (Sandbox Code Playgroud)

用法:

Optional<User> result = users.stream()
        .filter((user) -> user.getId() < 0)
        .collect(toSingleton());
Run Code Online (Sandbox Code Playgroud)

我们返回一个Optional,因为我们通常不能假设它只Collection包含一个元素.如果您已经知道这种情况,请致电:

User user = result.orElseThrow();
Run Code Online (Sandbox Code Playgroud)

这会给错误的人带来错误的负担 - 正如它应该的那样.


小智 5

使用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"));
}
Run Code Online (Sandbox Code Playgroud)