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


228

我正在尝试使用Java 8 Stream来查找中的元素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与滤波器匹配。

将过滤器线更改为:

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

会抛出一个NoSuchElementException(好!)

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


count()是终端操作,所以您不能这样做。此流之后将无法使用。
Alexis C.

好的,谢谢@ZouZou。我不确定该方法的作用。为什么没有Stream::size
ryvantage 2014年

7
@ryvantage因为一个流只能使用一次:计算其大小意味着对其进行“迭代”,之后您将无法再使用该流。
assylias 2014年

3
哇。这一评论使我Stream比以前更加了解……
ryvantage 2014年

2
这是当您意识到需要一直使用LinkedHashSet(假设要保留插入顺序)或HashSet一直使用。如果您的集合仅用于查找单个用户ID,那么为什么还要收集所有其他项?如果有可能总是需要找到一些也需要唯一的用户ID,那么为什么要使用列表而不是集合?您正在向后编程。使用正确的作品集,避免头痛
smac89

Answers:


189

创建一个自定义 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. ListCollectors.toList()收集器一起收集我们的对象。
  2. 最后应用一个额外的修整器,它返回单个元素-或抛出IllegalStateExceptionif 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);

23
番石榴Iterables.getOnlyElement将缩短这些解决方案并提供更好的错误消息。只是对已经使用Google Guava的其他读者的提示。
蒂姆·布斯(TimBüthe)2015年


1
@LonelyNeuron请不要编辑我的代码。这使我处于需要验证我的整个答案的情况,这是我四年前写的,而我现在根本没有时间这样做。
斯基威

2
@skiwi:寂寞的编辑很有帮助且正确,因此我在审查后将其恢复。今天访问此答案的人不在乎您如何得出答案,他们不需要查看旧版本和新版本以及“ 更新”部分。这会使您的答案更加混乱,帮助程度降低。将帖子置于最终状态要好得多,如果人们想查看发布的结果,可以查看帖子的历史记录。
马丁·彼得斯

1
@skiwi:答案中的代码绝对是您编写的。编辑者所做的全部工作就是清理您的帖子,只删除singletonCollector()该帖子中剩余的版本过时的定义的较早版本,然后将其重命名为toSingleton()。我的Java流专业知识有点生锈,但是重命名对我很有帮助。查看此更改花了我2分钟的时间,排名第一。如果您没有时间审查编辑,我是否可以建议您以后再要求其他人(可能在Java聊天室)进行编辑
马丁·彼得斯

118

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

3
我喜欢此答案中的初始方法。为了进行定制,可以将最后一个转换get()orElseThrow()
arin

1
我喜欢这个例子的简洁性,以及它避免每次调用它都创建不必要的List实例的事实。
LordOfThePigs

83

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

5
什么是点limit(2)在这个解决方案?结果列表是2还是100,会有什么不同?如果是大于1
ryvantage

18
如果找到第二个匹配项,它将立即停止。这就是所有花哨的收集者所做的,只是使用更多的代码。:-)
斯图尔特(Stuart)

10
如何添加Collectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder

1
Javadoc所说的是关于limit的参数:maxSize: the number of elements the stream should be limited to。所以,不是应该.limit(1)代替.limit(2)吗?
alexbt

5
@alexbt问题声明是要确保仅存在一个(不多也不少)匹配元素。在我的代码之后,可以进行测试result.size()以确保其等于1。如果为2,则说明存在多个匹配项,因此是错误的。如果代码执行了limit(1),则一个以上的匹配项将导致单个元素,无法将其与仅存在一个匹配项区分开。这将丢失OP所关注的错误情况。
Stuart Marks

67

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


@skiwi的singletonCollector较小,并且比这更易于遵循,这就是为什么我给了他支票。但是很高兴能在答案中达成共识:习惯Collector是行之有效的方法。
ryvantage 2014年

1
很公平。我的主要目的是追求速度,而不是简洁。
Louis Wasserman 2014年

1
是啊 为什么您的速度更快?
ryvantage 2014年

3
主要是因为分配全部内容List比单个可变引用更昂贵。
Louis Wasserman 2014年

1
@LouisWasserman,最后的更新句子MoreCollectors.onlyElement()实际上应该是第一个(也许是唯一一个:))
Piotr Findeisen

46

使用番石榴MoreCollectors.onlyElement()JavaDoc)。

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

用法:

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

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

2
其他用户注意事项:MoreCollectors是尚未发布(截至2016
一部分。– qerub

2
这个答案应该更高。
Emdadul Sawon

31

让您执行流不支持的怪异操作的“逃生舱口”操作是要求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();
}

番石榴有一种方便的方法来获取Iterator和获取唯一的元素,如果有零个或多个元素则抛出该异常,这可以在此处替换底部的n-1行。


4
番石榴的方法:Iterators.getOnlyElement(Iterator <T> iterator)。
2013年

23

更新资料

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


1
我将添加一个identity元素(null)以防止使用get()。遗憾的reduce是,您的工作方式并不像您想象的那样,请考虑Stream其中包含null元素的功能,也许您认为您已覆盖了它,但是我可以[User#1, null, User#2, null, User#3],除非我在这里弄错了,否则它不会抛出异常。
skiwi 2014年

2
@Skiwi如果存在空元素,则过滤器将首先抛出NPE。
assylias 2014年

2
既然你知道流不能传递null到减速功能,并消除身份值参数会使整个交易与null在功能过时:reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )做这项工作,甚至更好,它已经返回Optional,eliding的必要性,呼吁Optional.ofNullable在结果。
Holger

15

一种替代方法是使用reduce :(此示例使用字符串,但可以轻松应用于包括的任何对象类型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();

8

使用减少

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

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

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

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

6

我认为这种方式更简单:

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

4
它只找到第一个,但是当它不止一个时,也要抛出异常
lczapski

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

用法:

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

我们返回一个Optional,因为我们通常不能假定Collection恰好包含一个元素。如果您已经知道是这种情况,请致电:

User user = result.orElseThrow();

这将处理错误的负担加到了调用方上,这是应该的。



1

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

单个 操作者如果没有用户或多于一个用户被发现引发异常。


正确的答案,初始化阻塞流或集合可能(在资源方面)不是很便宜。
Karl Richter '18

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


1
很好的解决方案!如果这样做.collect(Collectors.toMap(user -> "", Function.identity())).get(""),您将获得更通用的行为。
glglgl

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

整齐!onlyOne()抛出IllegalStateException大于1个元素,并且NoSuchElementException`(以表示Optional::get)为0个元素。
simon04 '18

@ simon04你可以重载采取的方法Supplier(Runtime)Exception
Xavier Dury

1

如果您不介意使用第三方库,SequenceM则从cyclops流(和LazyFutureStream来自simple-react)中都可以使用single和singleOptional运算符。

singleOptional()如果中的元素0大于或等于,则会引发异常,否则将返回单个值。1Stream

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

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


0

我采用直接方法,只是实现了这一点:

public class CollectSingle<T> implements Collector<T, T, T>, BiConsumer<T, T>, Function<T, T>, Supplier<T> {
T value;

@Override
public Supplier<T> supplier() {
    return this;
}

@Override
public BiConsumer<T, T> accumulator() {
    return this;
}

@Override
public BinaryOperator<T> combiner() {
    return null;
}

@Override
public Function<T, T> finisher() {
    return this;
}

@Override
public Set<Characteristics> characteristics() {
    return Collections.emptySet();
}

@Override //accumulator
public void accept(T ignore, T nvalue) {
    if (value != null) {
        throw new UnsupportedOperationException("Collect single only supports single element, "
                + value + " and " + nvalue + " found.");
    }
    value = nvalue;
}

@Override //supplier
public T get() {
    value = null; //reset for reuse
    return value;
}

@Override //finisher
public T apply(T t) {
    return value;
}


} 

使用JUnit测试:

public class CollectSingleTest {

@Test
public void collectOne( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    Integer o = lst.stream().collect( new CollectSingle<>());
    System.out.println(o);
}

@Test(expected = UnsupportedOperationException.class)
public void failOnTwo( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    lst.add(8);
    Integer o = lst.stream().collect( new CollectSingle<>());
}

}

这个实现 不是线程安全的。


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

5
尽管这段代码可以解决问题,但包括解释如何以及为什么解决该问题的说明,确实可以帮助提高您的帖子质量,并可能导致更多的投票。请记住,您将来会为读者回答问题,而不仅仅是现在问的人。请编辑您的答案以添加说明,并指出适用的限制和假设。
David Buck

-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


3
据说count()不好用,因为它是终端操作。
ryvantage 2014年

如果这真的是报价,请添加您的资源
Neuron
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.