一、什么是Stream流(WHAT)
在Java中,集合和数组是我们经常会用到的数据结构,需要经常对他们做增、删、改、查、聚合、统计、过滤等操作。相比之下,关系型数据库中也同样有这些操作,但是在Java 8之前,集合和数组的处理并不是很便捷。
不过,这一问题在Java 8中得到了改善,Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream是Java 8新增的接口,Stream可以认为是一个高级版本的 Iterator。它代表着数据流,流中的数据元素的数量可以是有限的,也可以是无限的。
Stream作为java8的新特性,基于lambda表达式,是对集合对象功能的增强,它专注于对集合对象进行各种高效、便利的聚合操作或者大批量的数据操作,提高了编程效率和代码可读性。如果在项目中经常用到集合,遍历集合可以试下lambda表达式,经常还要对集合进行过滤和排序,Stream就派上用场了。
本文就来介绍下如何使用Stream。特别说明一下,关于Stream的性能及原理不是本文的重点,这里简单提一下:
Stream的原理:将要处理的元素看做一种流,流在管道中传输,并且可以在管道的节点上处理,包括过滤筛选、去重、排序、聚合等。元素流在管道中经过中间操作的处理,最后由最终操作得到前面处理的结果。
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。
Stream有以下特性及优点:
无存储。Stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
为函数式编程而生。对Stream的任何修改都不会修改背后的数据源,比如对Stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新Stream。
惰式执行。Stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
可消费性。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
我们举一个例子,来看一下到底Stream可以做什么事情:
上面的例子中,获取一些带颜色塑料球作为数据源,首先过滤掉红色的、把它们融化成随机的三角形。再过滤器并删除小的三角形。最后计算出剩余图形的周长。
如上图,对于流的处理,主要有三种关键性操作:分别是流的创建、中间操作(intermediate operation)以及最终操作(terminal operation)。
二、为什么要使用Stream流(WHY)
那我们为什么要使用Stream流呢?什么场景下会使用到它呢?不会用它会死么?我先不试着去回答这些问题,我们来直接看一下Demo,这个Demo展示的是一些常见的场景下,原始的写法以及使用Stream流之后的写法,相信看完这个Demo之后大家对于上述的问题心中都会有自己的答案了,OK,show me the code:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
public class Java8Tester {
public static void main(String args[]){
System.out.println("使用 Java 7: ");
// 计算空字符串
List strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
System.out.println("列表: " +strings);
long count = getCountEmptyStringUsingJava7(strings);
System.out.println("空字符数量为: " + count);
count = getCountLength3UsingJava7(strings);
System.out.println("字符串长度为 3 的数量为: " + count);
// 删除空字符串
List filtered = deleteEmptyStringsUsingJava7(strings);
System.out.println("筛选后的列表: " + filtered);
// 删除空字符串,并使用逗号把它们合并起来
String mergedString = getMergedStringUsingJava7(strings,", ");
System.out.println("合并字符串: " + mergedString);
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 获取列表元素平方数
List squaresList = getSquares(numbers);
System.out.println("平方数列表: " + squaresList);
List integers = Arrays.asList(1,2,13,4,15,6,17,8,19);
System.out.println("列表: " +integers);
System.out.println("列表中最大的数 : " + getMax(integers));
System.out.println("列表中最小的数 : " + getMin(integers));
System.out.println("所有数之和 : " + getSum(integers));
System.out.println("平均数 : " + getAverage(integers));
System.out.println("随机数: ");
// 输出10个随机数
Random random = new Random();
for(int i=0; i < 10; i++){
System.out.println(random.nextInt());
}
System.out.println("使用 Java 8: ");
System.out.println("列表: " +strings);
count = strings.stream().filter(string->string.isEmpty()).count();
System.out.println("空字符串数量为: " + count);
count = strings.stream().filter(string -> string.length() == 3).count();
System.out.println("字符串长度为 3 的数量为: " + count);
filtered = strings.stream().filter(string ->!string.isEmpty()).collect(Collectors.toList());
System.out.println("筛选后的列表: " + filtered);
mergedString = strings.stream().filter(string ->!string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("合并字符串: " + mergedString);
squaresList = numbers.stream().map( i ->i*i).distinct().collect(Collectors.toList());
System.out.println("Squares List: " + squaresList);
System.out.println("列表: " +integers);
IntSummaryStatistics stats = integers.stream().mapToInt((x) ->x).summaryStatistics();
System.out.println("列表中最大的数 : " + stats.getMax());
System.out.println("列表中最小的数 : " + stats.getMin());
System.out.println("所有数之和 : " + stats.getSum());
System.out.println("平均数 : " + stats.getAverage());
System.out.println("随机数: ");
random.ints().limit(10).sorted().forEach(System.out::println);
// 并行处理
count = strings.parallelStream().filter(string -> string.isEmpty()).count();
System.out.println("空字符串的数量为: " + count);
}
private static int getCountEmptyStringUsingJava7(List strings){
int count = 0;
for(String string: strings){
if(string.isEmpty()){
count++;
}
}
return count;
}
private static int getCountLength3UsingJava7(List strings){
int count = 0;
for(String string: strings){
if(string.length() == 3){
count++;
}
}
return count;
}
private static List deleteEmptyStringsUsingJava7(List strings){
List filteredList = new ArrayList();
for(String string: strings){
if(!string.isEmpty()){
filteredList.add(string);
}
}
return filteredList;
}
private static String getMergedStringUsingJava7(List strings, String separator){
StringBuilder stringBuilder = new StringBuilder();
for(String string: strings){
if(!string.isEmpty()){
stringBuilder.append(string);
stringBuilder.append(separator);
}
}
String mergedString = stringBuilder.toString();
return mergedString.substring(0, mergedString.length()-2);
}
private static List getSquares(List numbers){
List squaresList = new ArrayList();
for(Integer number: numbers){
Integer square = new Integer(number.intValue() * number.intValue());
if(!squaresList.contains(square)){
squaresList.add(square);
}
}
return squaresList;
}
private static int getMax(List numbers){
int max = numbers.get(0);
for(int i=1;i < numbers.size();i++){
Integer number = numbers.get(i);
if(number.intValue() > max){
max = number.intValue();
}
}
return max;
}
private static int getMin(List numbers){
int min = numbers.get(0);
for(int i=1;i < numbers.size();i++){
Integer number = numbers.get(i);
if(number.intValue() < min){
min = number.intValue();
}
}
return min;
}
private static int getSum(List numbers){
int sum = (int)(numbers.get(0));
for(int i=1;i < numbers.size();i++){
sum += (int)numbers.get(i);
}
return sum;
}
private static int getAverage(List numbers){
return getSum(numbers) / numbers.size();
}
}
个人认为使用Stream的优势:
- 充分JDK库提供现有的API,代码写起来简洁优化,且减少逻辑错误的可能性
- 方便实现并发。在多核情况下,可以使用并行Stream API来发挥多核优势。在单核的情况下,我们自己写的for性能不比Stream API 差多少。
个人认为使用Stream的劣势:
- 调试的难度增大,但是可以通过打thread级别的断点来实现调试
总之,综合比较,推荐在做批量数据集处理的时候,在项目JDK版本允许的情况下(如果JDK版本小于8,先升级一下版本到8),尽量地去使用Java 8 Stream的特性,你的代码质量会有一个比较高的level的提升。
三、如何使用Stream流?(HOW)
使用Stream流分为三步:
- 创建Stream流
- 通过Stream流对象执行中间操作
- 执行最终操作,得到结果
创建Stream流
Java 8 JDK中为我们提供了多种方式来创建或转化成Stream流:
- 通过集合的stream()方法为集合创建串行流或者parallelStream()为集合创建并行流
- 使用流的静态方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator)。
- 通过Arrays.stream(Object[])方法。
- BufferedReader.lines()从文件中获得行的流。
- Files类的操作路径的方法,如list、find、walk等。
- 随机数流Random.ints()。
- 其它一些类提供了创建流的方法,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()。
最底层都是依赖底层的StreamSupport类来完成Stream创建。
- 通过已有的集合来创建流
List strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
Stream stream = strings.stream();
通过一个已有的List创建一个流,这种通过集合创建出一个Stream的方式也是比较常用的一种方式。
- 通过Stream创建流
可以使用Stream类提供的方法,直接返回一个由指定元素组成的流。
Stream stream = Stream.of("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
下面我们列举一些常见的创建流方式的Demo:
public class StreamTest {
public static void main(String[] args) {
List list = new ArrayList<>();
// 从集合创建,stream()返回的是串行流,parallelStream()返回的是并行流
Stream stream = list.stream();
Stream stream1 = list.parallelStream();
// 从数组创建
IntStream stream2 = Arrays.stream(new int[]{1, 2, 3});
// 创建数字流
IntStream intStream = IntStream.of(1, 2, 3);
// 使用random创建包含3个随机数的流
IntStream randomStream = new Random().ints().limit(3);
}
}
中间操作
Stream有很多中间操作,多个中间操作可以连接起来形成一个流水线,每一个中间操作就像流水线上的一个工人,每人工人都可以对流进行加工,加工后得到的结果还是一个流。
怎么理解中间操作?意思是这样的:在上面我们已经能创建出Stream了,我们是对Stream进行操作,对Stream操作返回完返回的还是Stream,那么我们称这个操作为中间操作。
下表罗列了一些比较常用的中间操作函数:
函数 | 含义 |
---|---|
filter | 根据给定规则过滤元素 |
map | 处理并转换元素 |
limit | 限制输出结果的数目 |
skip | 跳过前n个元素不处理 |
sorted | 对流中的元素进行排序 |
distinct | 根据eqals()方法移除重复的元素 |
flatMap | 流格式转换 |
allMatch | 匹配所有 |
anyMathc | 匹配其中一个 |
noneMathc | 全部不匹配 |
Filter
filter 方法用于通过设置的条件过滤出元素。
以下代码片段使用 filter 方法过滤掉空字符串:
List strings = Arrays.asList("A", "", "B", "C", "D");
strings.stream().filter(string -> !string.isEmpty()).forEach(System.out::println);
//A, ,B, C, D
map
map 方法用于映射每个元素到对应的结果。
以下代码片段使用 map 输出了元素对应的平方数:
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().map( i -> i*i).forEach(System.out::println);
//9,4,4,9,49,9,25
limit/skip
limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。
以下代码片段使用 limit 方法保理4个元素:
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().limit(4).forEach(System.out::println);
//3,2,2,3
sorted
sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法进行排序:
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().sorted().forEach(System.out::println);
//2,2,3,3,3,5,7
distinct
distinct主要用来去重,以下代码片段使用 distinct 对元素进行去重:
List numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().distinct().forEach(System.out::println);
//3,2,7,5
flatMap
flatMap用于流转换,将一个流中的每个值都转换为另一个流
List wordList = Arrays.asList("Hello", "World");
List strList = wordList.stream()
.map(w -> w.split(" "))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
map(w -> w.split(" "))的返回值为Stream
元素匹配
提供了三种匹配方式
1.allMatch匹配所有
List integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().allMatch(i -> i > 3)) {
System.out.println("值都大于3");
}
2.anyMatch匹配其中一个
List integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().anyMatch(i -> i > 3)) {
System.out.println("存在大于3的值");
}
3. noneMatch全部不匹配
List integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().noneMatch(i -> i > 3)) {
System.out.println("值都小于3");
}
接下来我们通过一个例子和一张图,来演示下,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会发生什么。
List strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
Stream s = strings.stream().filter(string -> string.length()<= 6).map(String::length).sorted().limit(3)
.distinct();
过程及每一步得到的结果如下图:
最终操作
一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流。终端操作的执行,才会真正开始流的遍历。
Stream的中间操作得到的结果还是一个Stream,那么如何把一个Stream转换成我们需要的类型呢?比如计算出流中元素的个数、将流装换成集合等。这就需要最终操作(terminal operation)
下图总结了一些常用的最终操作:
函数 | 含义 |
---|---|
forEach | 对于每一个元素输出点什么 |
count | 统计当前元素个数 |
collect | 转化为某种集合数据类型 |
forEach
Stream 提供了方法 'forEach' 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
count
count用来统计流中的元素个数。
List strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
System.out.println(strings.stream().count());
//7
collect
collect就是一个归约操作,可以接受各种做法作为参数,将流中的元素累积成一个汇总结果:
List strings = Arrays.asList("Hollis", "HollisChuang", "hollis","Hollis666", "Hello", "HelloWorld", "Hollis");
strings = strings.stream().filter(string -> string.startsWith("Hollis")).collect(Collectors.toList());
System.out.println(strings);
//Hollis, HollisChuang, Hollis666, Hollis
couting
最后一种统计元素个数的方法在与collect联合使用的时候特别有用。
List integerList = Arrays.asList(1, 2, 3, 4, 5);
Long result = integerList.stream().collect(counting());
查找
findFirst
findFirst用于查找第一个
List integerList = Arrays.asList(1, 2, 3, 4, 5);
Optional result = integerList.stream().filter(i -> i > 3).findFirst();
通过findFirst方法查找到第一个大于三的元素并打印
findAny
findAny用于找到任意一个
List integerList = Arrays.asList(1, 2, 3, 4, 5);
Optional result = integerList.stream().filter(i -> i > 3).findAny();
通过findAny方法查找到其中一个大于三的元素并打印,因为内部进行优化的原因,当找到第一个满足大于三的元素时就结束,该方法结果和findFirst方法结果一样。提供findAny方法是为了更好的利用并行流。
reduce
reduce用于将流中的元素组合起来。
下面我们看一个Demo:
int sum = integerList.stream().reduce(0, (a, b) -> (a + b));
int sum = integerList.stream().reduce(0, Integer::sum);
reduce接受两个参数,一个初始值这里是0,一个BinaryOperator
joining
通过joining拼接流中的元素
String result = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));
默认如果不通过map方法进行映射处理拼接的toString方法返回的字符串,joining的方法参数为元素的分界符,如果不指定生成的字符串将是一串的,可读性不强。
获取流中最小最大值
通过min/max获取最小最大值
Optional min = menu.stream().map(Dish::getCalories).min(Integer::compareTo);
Optional max = menu.stream().map(Dish::getCalories).max(Integer::compareTo);
toMap
将结果转化为map。为了介绍用方法,下面我们看一个例子:
UserBo.java
class UserBo{
private int UserId;
private String UserName;
public UserBo(int userId, String userName) {
super();
UserId = userId;
UserName = userName;
}
public int getUserId() {
return UserId;
}
public void setUserId(int userId) {
UserId = userId;
}
public String getUserName() {
return UserName;
}
public void setUserName(String userName) {
UserName = userName;
}
@Override
public String toString() {
return "UserBo [UserId=" + UserId + ", UserName=" + UserName + "]";
}
}
public class ToMapTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
List list = new ArrayList<>();
list.add(new UserBo(100, "Mohan"));
list.add(new UserBo(100, "Sohan"));
list.add(new UserBo(300, "Mahesh"));
Map map = list.stream().collect(Collectors.toMap(UserBo::getUserId, v -> v, (k, v) -> k));
map.forEach((k, v) -> System.out.println("Key: " + k + ", value: " + v));
}
}
输出:
·```java
Key: 100, value: UserBo(UserId=100, UserName=Mohan)
Key: 300, value: UserBo(UserId=300, UserName=Mahesh)
修改代码:
```java
public class ToMapTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
List list = new ArrayList<>();
list.add(new UserBo(100, "Mohan"));
list.add(new UserBo(100, "Sohan"));
list.add(new UserBo(300, "Mahesh"));
Map map = list.stream().collect(Collectors.toMap(UserBo::getUserId, v -> v, (k, v) -> v));
map.forEach((k, v) -> System.out.println("Key: " + k + ", value: " + v));
}
}
输出结果:
Key: 100, value: UserBo(UserId=100, UserName=Sohan)
Key: 300, value: UserBo(UserId=300, UserName=Mahesh)
我们看到toMap接受三个参数,第一个参数是转换后的map的key,第二个参数是转换后的map的value,第三个参数是当key发生冲突的时候是选择留下前面的元素还是候选的元素:其中(k, v) -> k
表示选择留下前面的元素,(k, v) -> v
表示选择留下后面的元素。
最后,我们还是使用一张图,来演示下,前文的例子中,当一个Stream先后通过filter、map、sort、limit以及distinct处理后会,在分别使用不同的最终操作可以得到怎样的结果。
下图,展示了文中介绍的所有操作的位置、输入、输出以及使用一个案例展示了其结果。
前面说了好多Stream流的中间操作和最终操作,由于Stream流相关的接口众多,可能短时间大家全部记住。不过没关系,下图为我们展示的JDK提供的Stream相关的接口方法,返回值为stream的为中间操作,不是stream的为最终操作,这样就比较方便查找了。
下图是Stream类的类结构图,里面包含了大部分的中间和终止操作。
- 中间操作主要有以下方法(此类型方法返回的都是Stream):map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
-
终止操作主要有以下方法:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
总结
本文介绍了本人在学习Java 8 Stream中的一些笔记。
学习使用Stream要分别了解Stream创建、中间操作和最终操作。
Stream的创建有两种方式,分别是通过集合类的stream方法、通过Stream的of方法。
Stream的中间操作可以用来处理Stream,中间操作的输入和输出都是Stream,中间操作可以是过滤、转换、排序等。
Stream的最终操作可以将Stream转成其他形式,如计算出流中元素的个数、将流装换成集合、以及元素的遍历等。
最后,在Java开发中,如果使用了Java 8,那么强烈建议使用Stream。因为Stream的每个操作都可以依赖Lambda表达式,它是一种声明式的数据处理方式,并且Stream提高了数据处理效率和开发效率。
参考资料
- Java8 Stream的总结
- java8的Stream对集合操作飞起来
- Java 8 Stream
- Java 8的Stream代码,你能看懂吗?
- Java 8 中的 Streams API 详解
- 使用 Stream API 优化你的代码)