I am trying to use Java 8 Streams to find elements in a LinkedList. I want to guarantee, however, that there is 1 and only 1 match to the filter criteria.

Take this code:

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

This code finds a User based on their ID. But there are no guarantees how many Users matched the filter.

Changing the filter line to:

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

Will throw a NoSuchElementException (good!)

I would like it to throw an error if there are multiple matches, though. Is there a way to do this?

asked Mar 27 at 17:25
ryvantage

2,58921335

   
count() is a terminal operation so you can't do that. The stream can't be used after. – ZouZou Mar 27 at 17:42 
   
Ok, thanks @ZouZou. I wasn't entirely certain what that method did. Why is there noStream::size ? –  ryvantage Mar 27 at 17:44
2  
@ryvantage Because a stream can only be used once: calculating its size means "iterating" over it and after that you can't use the stream any longer. –  assylias Mar 27 at 17:45
   
Wow. That one comment helped me understand Streams so much more than I did before... –  ryvantage Mar 27 at 17:50

Technically there's an ugly 'workaround' that involves peek() and an AtomicInteger, but really you shouldn't be using that.

What I do in these cases is just collecting it in a list, like this:

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

I am not aware of a way to do this in the API, meanwhile I will work on another example involving a custom element.

Update, You should create your own Collector for this:

public static <T> Collector<T, List<T>, T> singletonCollector() {
return Collector.of(
ArrayList::new,
List::add,
(left, right) -> { left.addAll(right); return left; },
list -> {
if (list.size() != 1) {
throw new IllegalStateException();
}
return list.get(0);
}
);
}

What it does is:

  • It mimicks the Collectors.toList() collector.
  • It applies an extra finisher at the end, that throws an exception, or if no exception, returns the first element of the list.

Used as:

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

You can then customize this singletonCollector as much as you want, for example give the exception as argument in the constructor, tweak it to allow two values, and more.

New update, I revised my old answer once more for singletonCollector(), it can actually be obtained like this:

public static <T> Collector<T, ?, T> singletonCollector() {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> {
if (list.size() != 1) {
throw new IllegalStateException();
}
return list.get(0);
}
);
}

answered Mar 27 at 17:33
skiwi

9,37332767

   
@ryvantage I updated my answer with how I would do it with writing the own custom Collector, which I believe is the correct method. –  skiwi Mar 27 at 18:03
   
what do you think of my reduction? –  assylias Mar 27 at 18:32
1  
@assylias I commented on your answer, I think this one i smore concise and more straight forward though and less error prone. –  skiwi Mar 27 at 18:38
   
The difference main between the two collectors is that the first will stop as soon as it finds a second matching item whereas the second will iterate through the whole list. – assylias Mar 27 at 22:57

Wow, such complexity! :-) The other answers that involve writing a custom Collector are probably more efficient (such as Louis Wasserman's, +1), but if you want brevity, I'd suggest the following:

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

Then verify the size of the result list.

answered Mar 28 at 15:39
Stuart Marks

12.5k11850

   
What's the point of limit(2) in this solution? What difference would it make whether the resulting list was 2 or 100? If it's greater than 1. –  ryvantage Mar 28 at 18:31
2  
It stops immediately if it finds a second match. This is what all the fancy collectors do, just using more code. :-) –  Stuart Marks Mar 29 at 3:24 

You could roll your own Collector for this:

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

...or using your own Holder type instead of AtomicReference. You can reuse that Collector as much as you like.

answered Mar 27 at 17:51
Louis Wasserman

71k894155

   
@skiwi's singletonCollector was smaller and easier to follow than this, that's why I gave him the check. But good to see consensus in the answer: a custom Collector was the way to go. –  ryvantage Mar 27 at 20:37
   
Fair enough. I was primarily aiming for speed, not conciseness. –  Louis WassermanMar 27 at 20:40
   
Yeah? Why is yours faster? –  ryvantage Mar 27 at 20:45
   
Mostly because allocating an all-up List is more expensive than a single mutable reference. –  Louis Wasserman Mar 27 at 20:52 
   
I was unable to get yours to compile –  ryvantage Mar 28 at 18:28

The "escape hatch" operation that lets you do weird things that are not otherwise supported by streams is to ask for an 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 has a convenience method to take an Iterator and get the only element, throwing if there are zero or multiple elements, which could replace the bottom n-1 lines here.

answered May 24 at 19:26
Brian Goetz

8,86341733

 

The exception is thrown by Optional#get, but if you have more than one element that won't help. You could collect the users in a collection that only accepts one item, for example:

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

which throws a java.lang.IllegalStateException: Queue full, but that feels too hacky.

Or you could use a reduction combined with an optional:

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

The reduction essentially returns:

  • null if no user is found
  • the user if only one is found
  • throws an exception if more than one is found

The result is then wrapped in an optional.

But the simplest solution would probably be to just collect to a collection, check that its size is 1 and get the only element.

answered Mar 27 at 17:37
assylias

110k10153289

   
I would add an identity element (null) to prevent using get(). Sadly your reduce is not working as you think it does, consider a Stream that has null elements in it, maybe you think that you covered it, but I can be [User#1, null, User#2, null, User#3], now it will not throw an exception I think, unless I'm mistaken here. –  skiwiMar 27 at 18:36
   
@Skiwi if there are null elements the filter will throw a NPE first. –  assylias Mar 27 at 18:37

Have you tried this

    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.

answered Mar 28 at 7:03

   
It was said that count() is not good to use because it is a terminal operation. – ryvantage Mar 28 at 18:29

lambda -- Filter Java Stream to 1 and only 1 element的更多相关文章

  1. Java 8 Lambda 表达式及 Stream 在集合中的用法

    简介 虽然 Java 8 已经发布有一段时间了,但是关于 Java 8 中的 Lambda 表达式最近才开始系统的学习,刚开始就被 Stream 的各种骚操作深深的吸引住了,简直漂亮的不像 Java. ...

  2. Java Stream 使用详解

    Stream是 Java 8新增加的类,用来补充集合类. Stream代表数据流,流中的数据元素的数量可能是有限的,也可能是无限的. Stream和其它集合类的区别在于:其它集合类主要关注与有限数量的 ...

  3. 初探Lambda表达式/Java多核编程【1】从集合到流

    从集合到流 接上一小节初探Lambda表达式/Java多核编程[0]从外部迭代到内部迭代,本小节将着手使用"流"这一概念进行"迭代"操作. 首先何为" ...

  4. 初探Lambda表达式/Java多核编程【2】并行与组合行为

    今天又翻了一下书的目录,第一章在这之后就结束了.也就是说,这本书所涉及到的新的知识已经全部点到了. 书的其余部分就是对这几个概念做一些基础知识的补充以及更深层次的实践. 最后两个小节的内容较少,所以合 ...

  5. 十分钟学会Java8:lambda表达式和Stream API

    Java8 的新特性:Lambda表达式.强大的 Stream API.全新时间日期 API.ConcurrentHashMap.MetaSpace.总得来说,Java8 的新特性使 Java 的运行 ...

  6. JDK1.8中的Lambda表达式和Stream

    1.lambda表达式 Java8最值得学习的特性就是Lambda表达式和Stream API,如果有python或者javascript的语言基础,对理解Lambda表达式有很大帮助,因为Java正 ...

  7. Java Stream函数式编程案例图文详解

    导读 作者计划把Java Stream写成一个系列的文章,本文只是其中一节.更多内容期待您关注我的号! 一.什么是Java Stream? Java Stream函数式编程接口最初是在Java 8中引 ...

  8. Java Stream函数式编程图文详解(二):管道数据处理

    一.Java Stream管道数据处理操作 在本号之前发布的文章<Java Stream函数式编程?用过都说好,案例图文详解送给你>中,笔者对Java Stream的介绍以及简单的使用方法 ...

  9. Java Stream函数式编程第三篇:管道流结果处理

    一.Java Stream管道数据处理操作 在本号之前写过的文章中,曾经给大家介绍过 Java Stream管道流是用于简化集合类元素处理的java API.在使用的过程中分为三个阶段.在开始本文之前 ...

随机推荐

  1. jquery中Live方法不可用,Jquery中Live方法失效

    jquery中Live方法不可用,Jquery中Live方法失效 >>>>>>>>>>>>>>>>> ...

  2. C#发送邮件-C#教程

    如何利用C#实现邮件发送功能?闲话不多说请看代码: public static void SendMail(MyEmail email){//发送验证邮箱邮件.//1.创建邮件MailMessage ...

  3. HTML 转义字符

    在HTML中,一个包含特殊字符(如<>&)的字符串,要显示在页面上,由于添加到文本节点时会被认为是HTML的标签结构,造成一些错误,因此,要将这些特殊字符进行转义. 例如在< ...

  4. ==和equals

  5. Graphics类绘制图形

    1. 画直线 void drawLine(int startX,int startY,int endX,int endY); 四个参数分别为:起始点的x坐标和y坐标以及终点的x坐标和y坐标,该方法用于 ...

  6. python中os模块的常用接口和异常中Exception的运用

    1.os.path.join(arg1, arg2) 将arg1和arg2对应的字符串连接起来并返回连接后的字符串,如果arg1.arg2为变量,就先将arg1.arg2转换为字符串后再进行连接. 2 ...

  7. WPF扩展标记

    标记扩展和 WPF XAML,标记扩展是 XAML 语言以及 XAML 服务的 .NET 实现的常规功能 XAML 处理器和标记扩展 XAML 分析器可将特性值解释为可转换成基元的文本字符串,或可通过 ...

  8. 转】VB中Set的用法

    Set 语句 将对象引用赋给变量或属性. 语法 Set objectvar = {[New] objectexpression | Nothing} Set 语句的语法包含下面部分: 部分 描述 ob ...

  9. ligerUI路径问题

    ligerUI放mv的Content目录下,路径为固定的并且必须引进一下文件 <link href="~/Content/Ligerui/Source/lib/ligerUI/skin ...

  10. android小知识

    string 与 []byte 互转: public String BytesToString(byte[] data) { return new String(data); } public byt ...