jdk8 Stream流
1.Stream API 简介
Stream API是Java 8中加入的一套新的API,主要用于处理集合操作,不过它的处理方式与传统的方式不同,
称为“数据流处理”。流(Stream)类似于关系数据库的查询操作,是一种声明式操作。比如要从数据库中获取所有年龄大于20岁的用户的名称,
并按照用户的创建时间进行排序,如果在sql中就会很容易完成,但是在java程序中,在jdk8以前可能要使用很多的if条件,但是在jdk8的stream
流中,我们可以这样:
List<String> userNames =
users.stream()
.filter(user -> user.getAge() > 20)
.sorted(comparing(User::getAddDate))
.map(User::getUserName)
.collect(toList());
在Java中,集合是一种数据结构,或者说是一种容器,用于存放数据,流不是容器,它不关心数据的存放,只关注如何处理。
在遍历一个数组的时候,我们回采用for-each
的方式,这种属于外部遍历,而流使用的是内部遍历的方式,也就是在内部帮你把逻辑给处理好了
可以用一下的方式:
// 外部
List<String> list = Arrays.asList("A", "B", "C", "D");
for (String str : list) {
System.out.println(str);
}
// 内部
list.stream().forEach(System.out::println);
其实在stream流的内部,Stream API将迭代操作封装到了内部,它会自动的选择最优的迭代方式,并且使用并行方式处理时,
将集合分成多段,每一段分别使用不同的线程处理,最后将处理结果合并输出。
需要注意的是,每个流的只能去遍历一次,如果对一个流遍历两次,会抛出java.lang.IllegalStateException
异常
List<String> list = Arrays.asList("A", "B", "C", "D");
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 这里会抛出java.lang.IllegalStateException异常,因为流已经被关闭
流通常是有三个部分组成
1.数据源:流的获取,比如list.stream()
方法;
2.中间处理:中间处理是对流元素的一系列处理。比如过滤filter
,排序sorted
,映射map
;
3.终端处理:终端处理会生成结果,结果可以是任何不是流值。生成List,可用collect(Collectors.toList())
,生成Map可用collect(Collectors.toMap())
也可以不返回结果,如stream.forEach(System.out::println)
就是将结果打印到控制台中,并没有返回。
2.使用流
-
filter()
:对元素进行过滤 -
map()
:将流的元素映射成另一个类型 -
distinct()
:去除流中重复的元素 -
sorted()
:对元素进行排序 -
forEach
:对流中的每个元素执行某个操作 -
peek()
:与forEach()方法效果类似,不同的是,该方法会返回一个新的流,而forEach()无返回 -
limit()
:截取流中前面几个元素 -
skip()
:跳过流中前面几个元素 -
toArray()
:将流转换为数组 -
reduce()
:对流中的元素归约操作,将每个元素合起来形成一个新的值 -
collect()
:对流的汇总操作,比如输出成List集合 -
anyMatch()
:匹配流中的元素,类似的操作还有allMatch()和noneMatch()方法 -
findFirst()
:查找第一个元素,类似的还有findAny()方法 -
max()
:求最大值 -
min()
:求最小值 -
count()
:求总数
使用方法
2.1过滤和排序
Stream.of(1, 8, 5, 2, 1, 0, 9, 2, 0, 4, 8)
.filter(n -> n > 2) // 对元素过滤,保留大于2的元素
.distinct() // 去重,类似于SQL语句中的DISTINCT
.skip(1) // 跳过前面1个元素
.limit(2) // 返回开头2个元素,类似于SQL语句中的SELECT TOP
.sorted() // 对结果排序
.forEach(System.out::println);
//filter后剩下:8,5,9,4,8
//去重后排序剩下:5,9
2.2查找和匹配
Stream中提供的查找方法有anyMatch()、allMatch()、noneMatch()、findFirst()、findAny()
,
这些方法被用来查找或匹配某些元素是否符合给定的条件:
boolean hasMatch = Stream.of("Java", "C#", "PHP", "C++", "Python")
.anyMatch(s -> s.equals("Java"));
// hasMatch:true
boolean hasAllMatch = Stream.of("Java", "C#", "PHP", "C++", "Python")
.allMatch(s -> s.contains("#"));
//hasAllMatch:false
Optional<String> element = Stream.of("Java", "C#", "PHP", "C++", "Python")
.filter(s -> s.contains("C"))
// .findFirst() // 查找第一个元素
.findAny(); // 查找任意元素
//element:Optional[C#]
实际上测试结果发现,findFirst()
和findAny()
返回的都是第一个元素,两者之间到底有没什么区别呢,
通过查看javadoc描述,大致意思是findAny()
是为了提高并行操作时的性能,所以在使用中如果没有特殊需求,都是
建议使用findAny()
2.3 归约
归约操作就是将流中的元素进行合并,形成一个新的值,常见的归约操作包括求和等运算,
求最大值或最小值。归约操作一般使用reduce()
方法,
与map()
方法搭配使用,可以处理一些很复杂的归约操作。
List<Book> books =Arrays.asList(
new Book("Java编程思想", "Bruce Eckel", "机械工业出版社", 108.00D),
new Book("Java 8实战", "Mario Fusco", "人民邮电出版社", 79.00D),
new Book("MongoDB权威指南(第2版)", "Kristina Chodorow", "人民邮电出版社", 69.00D)
);
// 计算所有图书的总价
Optional<Double> totalPrice = books.stream()
.map(Book::getPrice)
.reduce((n, m) -> n + m);
// 价格最高的图书
Optional<Book> expensive = books.stream().max(Comparator.comparing(Book::getPrice));
// 价格最低的图书
Optional<Book> cheapest = books.stream().min(Comparator.comparing(Book::getPrice));
// 计算总数
long count = books.stream().count();
在计算图书总价的时候首先使用map()
方法得到所有图书价格的流,然后再使用reduce()
方法进行归约计算。
3.数据收集
数据收集是流式数据处理的终端处理,与中间处理不同的是,终端处理会消耗流,
也就是说,终端处理之后,这个流就会被关闭,如果再进行中间处理,就会抛出异常。数据收集主要使用collect
方法,
该方法也属于归约操作,像reduce()
方法那样可以接收各种做法作为参数,将流中的元素累积成一个汇总结果,
具体的做法是通过定义新的Collector
接口来定义的。
3.1规约和汇总
// 求和
long count = books.stream().collect(counting());
// 价格最高的图书
Optional<Book> expensive = books.stream().collect(maxBy(comparing(Book::getPrice)));
// 价格最低的图书
Optional<Book> cheapest = books.stream().collect(minBy(comparing(Book::getPrice)));
上面的代码假设你已经使用静态导入了Collectors
和Comparator
两个类,
这样你就不用再去写Collectors.counting()
和Comparator.comparing()
这样的代码了:
import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;
Collectors
类还包含一个joining()
方法,该方法用于连接字符串:
String str = Stream.of("A", "B", "C", "D").collect(joining(","));
3.2分组
和关系型数据库类似,流也提供了类似数据库的group by的特性,由Collectors.groupingBy()
方法提供:
Map<String, List<Book>> booksGroup = books.stream().collect(groupingBy(Book::getPublisher));
上面的代码按照出版社对图书进行分组,分组的结果是一个Map
对象
,Map
的key
值是出版社的名称,value
值是每个出版社分组对应的集合。
分组方法groupingBy()
接收一个Function
接口作为参数,
上面的例子中我们使用了方法引用传递了出版社作为分组的依据
,但实际情况可能比这复杂,比如将价格在0-50之间的书籍分成一组,50-100之间的分成一组,
超过100的分成一组,这时候,我们可以直接使用Lambda表达式来表示这个分组逻辑:
// 获取流
List<Book> books = Arrays.asList(
new Book("Java编程思想", "Bruce Eckel", "机械工业出版社", 108.00D),
new Book("Java 8实战", "Mario Fusco", "人民邮电出版社", 79.00D),
new Book("MongoDB权威指南(第2版)", "Kristina Chodorow", "人民邮电出版社", 69.00D),
new Book("MongoDB", "Kristina Chodorow", "人民邮电出版社", 50.00D),
new Book("MongoDB", "Kristina Chodorow", "人民邮电出版社", 50.00D)
);
Map<String, List<Book>> booksGroup = books
.stream()
.collect(groupingBy(book -> {
if (book.getPrice() > 0 && book.getPrice() <= 50) {
return "A";
} else if (book.getPrice() > 50 && book.getPrice() <=100) {
return "B";
} else {
return "C";
}
}));
//输出结果
{A=[Book{name='MongoDB', author='Kristina Chodorow', address='人民邮电出版社', price=50.0}, Book{name='MongoDB', author='Kristina Chodorow', address='人民邮电出版社', price=50.0}],
B=[Book{name='Java 8实战', author='Mario Fusco', address='人民邮电出版社', price=79.0}, Book{name='MongoDB权威指南(第2版)', author='Kristina Chodorow', address='人民邮电出版社', price=69.0}],
C=[Book{name='Java编程思想', author='Bruce Eckel', address='机械工业出版社', price=108.0}]}
groupingBy()
方法还支持多级分组,
他有一个重载方法,除了接收一个Function
类型的参数外,还接收一个Collector
类型的参数:
Map<String, Map<String, List<Book>>> booksGroup = books.stream().collect(
groupingBy(Book::getPublisher, groupingBy(book -> {
if (book.getPrice() > 0 && book.getPrice() <= 50) {
return "A";
} else if (book.getPrice() > 50 && book.getPrice() <=100) {
return "B";
} else {
return "C";
}
}))
);
//输出结果
{Mario Fusco={B=[Book{name='Java 8实战', author='Mario Fusco', address='人民邮电出版社', price=79.0}]},
Bruce Eckel={C=[Book{name='Java编程思想', author='Bruce Eckel', address='机械工业出版社', price=108.0}]},
Kristina Chodorow={A=[Book{name='MongoDB', author='Kristina Chodorow', address='人民邮电出版社', price=50.0},
Book{name='MongoDB', author='Kristina Chodorow', address='人民邮电出版社', price=50.0}],
B=[Book{name='MongoDB权威指南(第2版)', author='Kristina Chodorow', address='人民邮电出版社', price=69.0}]}}
groupingBy()
的第二个参数可以是任意类型,只要是Collector
接口的实例就可以,比如先分组,再统计数量:
Map<String, Long> countGroup = books.stream()
.collect(groupingBy(Book::getPublisher, counting()));
还可以获取分组后每组价格最高的的图书:
Map<String, Book> expensiveGroup = books.stream()
.collect(groupingBy(Book::getAuthor, collectingAndThen(
maxBy(comparingDouble(Book::getPrice)),
Optional::get
)));
//输入结果
/*{Mario Fusco=Book{name='Java 8实战', author='Mario Fusco', address='人民邮电出版社', price=79.0},
Bruce Eckel=Book{name='Java编程思想', author='Bruce Eckel', address='机械工业出版社', price=108.0},
Kristina Chodorow=Book{name='MongoDB', author='Kristina Chodorow', address='人民邮电出版社', price=80.0}}*/