287

我正在尝试使用 Java 8StreamLinkedList. 但是,我想保证只有一个匹配过滤条件。

拿这个代码:

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 找到一个。但是不能保证有多少Users 匹配过滤器。

将过滤线更改为:

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

会抛出一个NoSuchElementException(好!)

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

4

24 回答 24

253

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

  1. List使用收集器收集我们的对象Collectors.toList()
  2. 最后应用一个额外的完成器,它返回单个元素——或者抛出一个IllegalStateExceptionif list.size != 1

用作:

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

然后,您可以根据Collector需要对其进行自定义,例如将异常作为构造函数中的参数,调整它以允许两个值等等。

另一种——可以说不那么优雅——的解决方案:

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

你可以做的只是将它收集在 a 中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);
于 2014-03-27T17:33:03.767 回答
150

为了完整起见,这里是与@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));
于 2016-01-07T19:51:39.967 回答
90

涉及编写自定义的其他答案Collector可能更有效(例如Louis Wasserman's,+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);
}
于 2014-03-28T15:39:23.007 回答
85

番石榴在这里提供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

于 2014-03-27T17:51:16.513 回答
67

使用番石榴的MoreCollectors.onlyElement()源代码)。

IllegalArgumentException如果流包含两个或多个元素,它会执行您想要的操作,NoSuchElementException如果流为空,则抛出 a。

用法:

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

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());
于 2016-11-11T21:19:48.490 回答
33

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

于 2014-05-24T19:26:28.033 回答
26

更新

@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,但感觉太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();

减少基本上返回:

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

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

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

于 2014-03-27T17:37:16.047 回答
21

我认为这种方式更简单:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();
于 2019-09-12T10:29:17.187 回答
17

另一种方法是使用归约:(此示例使用字符串,但可以轻松应用于任何对象类型,包括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();
于 2015-05-13T02:01:28.927 回答
15

使用减少

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

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

这样您可以获得:

  • 可选的 - 与您的对象一样,或者Optional.empty()如果不存在
  • 如果有多个元素,则异常(最终带有您的自定义类型/消息)
于 2018-08-24T14:07:02.287 回答
10

Guava对此有一个Collector叫做MoreCollectors.onlyElement().

于 2017-01-24T20:16:00.820 回答
8

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

这将处理错误的负担放在了调用者身上——这是理所当然的。

于 2018-05-24T16:49:01.193 回答
3

使用 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"));
}
于 2022-01-04T15:14:00.647 回答
2

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

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

于 2016-01-07T21:40:10.073 回答
1

如果您不介意使用 3rd 方库,SequenceM来自cyclops-streams(和LazyFutureStream来自simple-react)都有一个和 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

披露 - 我是这两个图书馆的作者。

于 2016-01-11T06:08:30.297 回答
1

由于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.

于 2016-09-08T07:52:12.510 回答
1

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

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);
}
于 2017-05-16T07:15:35.480 回答
1
 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);

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

于 2021-08-09T08:50:33.483 回答
0

如果您不使用 Guava 或 Kotlin,这里有一个基于 @skiwi 和 @Neuron 答案的解决方案。

users.stream().collect(single(user -> user.getId() == 1));

或者

users.stream().collect(optional(user -> user.getId() == 1));

wheresingleoptional是返回相应收集器的静态导入函数。

我推断如果将过滤逻辑移到收集器内部,它看起来会更简洁。如果您碰巧删除了带有.filter.

代码要点https://gist.github.com/overpas/ccc39b75f17a1c65682c071045c1a079

于 2020-10-14T12:54:03.543 回答
0

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

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;
 }
}
于 2021-12-28T11:45:33.407 回答
0
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 个元素开始,只希望只有一个元素具有真实条件,如果列表中有多个元素具有真实状态,那么此时它将通过异常。
  4. 在得到所有这些之后,我们使用 get(); 从列表中取出一个元素并将其存储到另一个对象中。
  5. 如果您想添加可选的,例如Optional<activeMaster > = master.stream().filter(status -> status.getStatus() == true).reduce((u, v) -> {throw new IllegalStateException();}).get();
于 2022-02-11T05:09:33.653 回答
-1
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());
于 2020-03-28T11:38:46.667 回答
-1

受@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(...));
于 2020-09-12T19:51:23.780 回答
-2

你试过这个

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

于 2014-03-28T07:03:11.400 回答