1. Stream产生的背景
Stream 作为 Java 8 的一功能强大的新特性,它与 java I/O里的 InputStream 和 OutputStream 是完全不同的概念。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现与java8中的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。
2. 传统方式的不足
在java8以前,java对于某些常用的功能或需求的处理方式要么很繁琐、不高效,要么要依赖数据库的操作(如某些聚合操作),如以下场景需求:
在一个批量数据中:
- 求出每月、每周、每日平均值等
- 求出最大值
- 取出n个样本
- 排除无效或不关心的某些数据
等等操作,对于以上操作,如果使用java代码处理,是极其繁琐的,笨拙的,要么就得依赖借助与数据库的聚合操作以快速得到结果。但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。
试举例:假设有一个商品数据集合,要对手机类型的商品进行一个统计分析,计算出销售量最高的手机品牌。
商品实体类:Goods
public class Goods {
private String name;
//假设1代表手机类别
private Integer type;
private Integer brand;
private Integer sellCount;
//constructor、getter、sertter省略
}
java8以前的处理方式
//原始商品数据集合,此处仅模拟代码逻辑,不填充数据
List<Goods> goods = new ArrayList<>();
//遍历商品集合数据,筛选出手机类型数据
List<Goods> phones = new ArrayList<>();
for (Goods g : goods) {
if (g.getType() == 1) {
phones.add(g);
}
}
//对手机商品集合进行排序,选出销售量最大的那个
phones.sort(new Comparator<Goods>() {
@Override
public int compare(Goods g1, Goods g2) {
return g1.getSellCount() - g2.getSellCount();
}
});
//然后从排好序中的数据取出最大值即可
java8使用Stream的处理方式
Optional<Goods> maxSellGood = goods.stream()
.filter((g) -> {return g.getType() == 1;})
.max((g1, g2) -> {
return g1.getSellCount() - g2.getSellCount();
});
//直接得出销售量最大的
Goods phone = maxSellGood.get();
会明显发现Stream所带来的高效与简洁,并且性能极好,接下来就来认识一下什么是Stream!
3. 什么是Stream
Stream的英文翻译是“流”,是的,正如字面意思一样,Stream就是一种流操作的概念。它不是集合元素,也不是数据结构并且不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;而高级版本的 Stream,用户只要给出对元素集合的操作命令,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
Stream可以通过下图来简易理解:
而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。
4. Stream流的特点
1. 单向,不可往复,数据只能遍历一次;
2. 采用内部迭代的方式(即处理过程有流自行完成);
3. 不修改也不影响原始数据(这一点其实很重要,Stream是将原始数据拷贝并转换为流,并不是直接对原始数据进行操作,这就保证了原始数据的安全性与完整性);
5. Stream流的操作种类
流的操作分为两种,分别为中间操作 和 终止操作。
1. 中间操作
当数据源中的数据上了流水线后,这个过程对数据进行的所有操作都称为“中间操作”。
中间操作仍然会返回一个流对象,因此多个中间操作可以串连起来形成一个流水线。
2. 终止操作
当所有的中间操作完成后,若要将数据从流水线上拿下来,则需要执行终止操作。
终止操作将返回一个执行结果,这就是你想要的数据( 终止操作时一次性全部处理,称为“惰性求值”)。
6. Stream流的操作过程
使用Stream需要三步:
1. 准备数据源(集合或数组),转为Stream流对象;
2. 执行中间操作
中间操作可以有多个,多个中间操作串起来就形成了一葛流水线操作;
3. 执行终止操作
终止操作后,本次流处理结束,你将获得一个执行结果。
7. Stream API 详解与使用
7.1 常用的几种创建Stream流的方式
1. 使用集合接口Collection 接口提供的stream创建串行流(这里不讲并行流)
List<String> list = Arrays.asList("Jim", "Tom", "Sam", "Kaven");
Stream<String> stream = list.stream();
2. 使用Arrays提供的stream方法,以数组的形式创建
String[] strings = {"Jim", "Tom", "Sam", "Kaven"};
Stream<String> stream2 = Arrays.stream(strings);
3. 可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。
Stream<String> stream3 = Stream.of("Jim", "Tom", "Sam", "Kaven");
7.2 终止操作
要想得到结果,终止操作必不可少,所以本文先讲解终止操作,再结合终止操作和中间操作来讲解中间操作。
而终止操作有分为以下几种:
- 查找
- 匹配
- 收集
- 归约
(1) 查找
终止操作(查找)之 -- void forEach(Consumer<? super T> action);
解释 : 内部迭代( 用 使用 Collection 接口需要用户去做迭
代,称为 外部迭代 。相反, Stream API 使用内部
迭代 )
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
list.stream()
.forEach((e) -> {System.out.println(e);});
终止操作之(查找) -- Optional<T> min(Comparator<? super T> comparator);解释 : 返回流中最小值
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result = list.stream()
.min((e1, e2) -> {return e1 - e2;})
.get();
System.out.println(result);
终止操作之(查找) -- Optional<T> max(Comparator<? super T> comparator);解释 : 返回流中最大值
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result = list.stream()
.max((e1, e2) -> {return e1 - e2;})
.get();
System.out.println(result);
终止操作之(查找) -- long count();
解释: 返回流中数据总数
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
long result = list.stream()
.count();
System.out.println(result);
终止操作之(查找) -- Optional<T> findAny();
解释 : 返回当前流中的任意元素
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result = list.stream()
.findAny()
.get();
System.out.println(result);
终止操作(查找) -- Optional<T> findFirst();
解释 : 返回当前流中第一个元素
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result = list.stream()
.findFirst()
.get();
System.out.println(result);
注:在此有关查找只举例几个典型的案例,其他的还有很多,但都是类似的变形和用法,读者可在使用时查看相关API文档或者源码即可。
(2)匹配
终止操作之(匹配) -- boolean noneMatch(Predicate<? super T> predicate);
解释 : 检查是否没有匹配所有元素,即流中所有元素都不匹配才会返回true,否则返回false
另外两个类似的方法
a、检查是否匹配所有元素
boolean allMatch(Predicate<? super T> predicate);
b、检查是否至少匹配一个元素
boolean anyMatch(Predicate<? super T> predicate);
这两个方法的用法都差不多,在此就不一一列举了
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
boolean result = list.stream()
.noneMatch((e) -> {return e > 0;});
System.out.println(result);
(3)收集
终止操作(收集)之 -- <R, A> R collect(Collector<? super T, A, R> collector);
解释 : 将流中的元素收集起来,返回一个集合
List<String> list2 = Arrays.asList("Jim", "Tom", "Sam", "Kaven");
//将集合中的元素全部转化为大写
List<String> result = list2.stream()
.map((e) -> {return e.toUpperCase();})
.collect(Collectors.toList());//collect可以做很多操作,因为Collectors的原因,所以它很强大,笔者会结合Collectors来单独讲collect
System.out.println(result.get(0));
注:其中,在collect操作中,Collectors是一个很强大的工具类,专门用来处理Stream流的,笔者会以另外单独的讲解,在此简单提一下该collect方法(4)归约
终止操作(归约)之 -- Optional<T> reduce(BinaryOperator<T> accumulator);
解释 : 可以将流中元素反复结合起来,得到一个值。如本例的将流中的所有元素相加求和;
Map和Reduce操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。
另外,reduce 并不是一个新的操作,你有可能已经在使用它。
SQL中类似 sum()、avg() 、count() 的聚集函数,实际上就是 reduce 操作,它们接收多个值并返回一个值。
流API定义的 reduce() 函数可以接受lambda表达式,并对所有值进行合并。
IntStream这样的类有类似 average()、count()、sum() 的内建方法来做 reduce 操作,
也有mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。
reduce重载一、Optional<T> reduce(BinaryOperator<T> accumulator);
该方法会返回一个Optional<T>,其中Lambda表达式中的
第一个参数是上次该函数(Lambda表达式 ->右边的函数体)执行的返回值(也称为中间结果),第二个参数是stream中的元素
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Optional<Integer> result = list.stream()
.reduce((e1, e2) -> {
System.out.println(1);//执行了8次,即9个数相加,执行了8次
return e1 + e2;
});
System.out.println(result.get());
从输出的8个1结果可以分析得出,上述Lambda表达式的执行相当于以下代码int first = list.get(0);
int second = list.get(1);
int sum = 0, i = 1;
while (i < list.size() - 1) {
sum = first + second;
first = sum;
second = list.get(++i);
}
reduce重载二、T reduce(T identity, BinaryOperator<T> accumulator);
该方法有两参数,第一个是用来指定归约结果的初始值,并且可以发现,第一个参数的类型与返回值类型是相同的,因为指定了初始值,也就不存在null,所以该重载方法不必返回Optional<T>,
第二个参数则是一个累加器
List<Integer> list2 = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
Integer result2 = list2.stream()
.reduce(0, (e1, e2) -> {
System.out.println("*");//执行了9次,即9个数相加,执行了9次
return e1 + e2;
});
System.out.println(result2);
由两个重载的reduce的执行结果可见,指定初始值与未指定初始值的执行情况是不太一样的,变形1,未定义初始值,从而第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素,所以9个数相加执行了8次;
变形2,定义了初始值,从而第一次执行的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素,所以9个数相加执行了9次。
reduce重载三、<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
该重载方法的前两个参数与重载二是一样的,而第三个参数是Stream为支持并发操作的,
为了避免竞争,对于reduce线程都会有独立的result,combiner的作用在于合并每个线程的result得到最终结果。
这也说明了了第三个函数参数的数据类型必须为返回数据类型了。
本文不打算对该重载方法举例
7.3 中间操作
注:中间操作必须和终止操作结合使用才会得到结果,否则中间操作不会执行
而中间操作又可以分为以下几类:
- 筛选与切片
- 映射
- 排序
(1)筛选与切片
中间操作(筛选与切片)之 -- Stream<T> filter(Predicate<? super T> predicate);
解释 : 接收 Lambda , 从流中排除某些元素,筛选出想要的元素
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
//筛选出大于5的数,获取过滤掉小于5的数
list.stream()
.filter((e) -> {return e > 5;})
.forEach((e) -> {System.out.println(e);});
中间操作(筛选与切片)之 -- Stream<T> distinct();
解释 : 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 6, 8, 9, 1);
//筛选出大于5的数,获取过滤掉小于5的数,并且去重
list.stream()
.filter((e) -> {return e > 5;})
.distinct()
.forEach((e) -> {System.out.println(e);});
中间操作(筛选与切片)之 -- Stream<T> limit(long maxSize);
解释 : 截断流,使其元素不超过给定数量
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
//筛选出大于5的数,获取过滤掉小于5的数
list.stream()
.filter((e) -> {return e > 5;})
.limit(3)
.forEach((e) -> {System.out.println(e);});
中间操作(筛选与切片)之 -- Stream<T> skip(long n);
解释 : 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
list.stream()
.skip(5)
.forEach((e) -> {System.out.println(e);});
(2)映射
中间操作(映射)之 -- <R> Stream<R> map(Function<? super T, ? extends R> mapper);
解释 : 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
//两集合中的元素全部过滤映射为自身的两倍大小
list.stream()
.map((o) -> {return o * 2;})
.forEach((e) -> {System.out.println(e);});
List<String> list2 = Arrays.asList("Jim", "Tom", "Sam", "Kaven");
//将集合中的元素全部转化为大写
list2.stream()
.map((e) -> {return e.toUpperCase();})
.forEach((e) -> {System.out.println(e);});
//换句话说,map()就是将流中的元素重新处理,最后返回处理后的新的流,而处理的规则就是自定义的Function<T, R>的函数式接口
//map与filter有本质区别,filter是在原本元素上进行过滤,得到的是原本的元素中已经过滤掉处理后的元素流,而map是根据自定义的映射规则来转换,得到的是新的元素流
注:映射操作的其他方法还有mapToDouble、mapToInt、mapToLong,他们的思想和用法大同小异,本文不再一一列举(3)排序
中间操作(排序)之 -- Stream<T> sorted();
解释 : 产生一个新流,其中按自然顺序排序
List<Integer> list = Arrays.asList(2, 8, 9, 6, 7, 3, 4, 5, 1);
list.stream()
.sorted()
.forEach((e) -> {System.out.println(e);});
注:排序方法还有Stream<T> sorted(Comparator<? super T> comparator);,该方式是允许自定义排序规则,但用法一样,故本文不再举例
8. 总结
2. 本文处理讲解Stream流的基本概念,还讲解了一些常用的Stream API,但是Stream的功能远远不仅与此,本文也没法举例出所以的API例子,建议读者结合API文档或者源码慢慢学习,熟悉掌握Stream的用法。
注:希望本文对读者有帮助,转载请注明出处!