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. not exists 跟not in 纪念一下

  2. [IO] C# DirFileHelper文件与文件夹操作类教程与源码下载 (转载)

    点击下载 DirFileHelper.zip 主要功能如下所示 // 摘要: // 向文本文件的尾部追加内容 // // 参数: // filePath: // 文件的绝对路径 // // conte ...

  3. 使用graphics2D给图片上画字符

    //读取图片BufferedImage frontImage = ImageIO.read(new File(eCardXMLConfigManager.geteCardXMLConfigManage ...

  4. SVN库迁移过程总结

    一.背景:老SVN是安装在32位服务器上:现在64位服务器上安装了新版本SVN服务,所以需要将SVN从老服务器上迁移到新服务器上. 1.SVN Server下载:https://www.visuals ...

  5. CSS jQuery HTML5 CSS3

    jquery css3图片文字介绍鼠标滚动页面动画单页 http://www.17sucai.com/preview/1/2013-12-30/%E5%8A%A8%E7%94%BB%E5%8D%95% ...

  6. Quartz-2D绘图之路径(Paths)详解

    在上篇文章中,我们简单的理解了绘图上下文,今天我们来认识一下Quartz-2D中另一个重要的概念,路径(Paths). 一.理解路径 路径定义了一个或多个形状,或是子路径.一个子路径可由直线,曲线,或 ...

  7. ios专题 - GCD(1)

    什么是GCD? Grand Central Dispatch或者GCD,是一套低层API,提供了一种新的方法来进行并发程序编写.从基本功能上讲,GCD有点像 NSOperationQueue,他们都允 ...

  8. PHP 学习笔记 (二)

    PHP中的错误级别: PHP中的报错有3中级别: NOTICE.WARNING.ERROR. NOTICE是级别最轻的一种,一般表示代码不规范,但是程序是可以正常运行的 Warning是比NOTICE ...

  9. [学习笔记]设计模式之Flyweight

    为方便读者,本文已添加至索引: 设计模式 学习笔记索引 写在前面 Flyweight(享元)模式运用共享技术,可以有效地支持大量细粒度的对象.今天我们会去参观小霍比特人们的酿酒工坊……等等,不是享元模 ...

  10. 纯JS写出日历

    封装代码: (function(cmf){ cmf.showDcalendar=function(){ var fnname=cmf.fn var id="cmfrili2" va ...