java8 — Stream篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/h_xiao_x/article/details/79723142

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. 总结

1. 通过本文,或许你已经了解到了Stream的强大之处了,那么建议读者在自己日后的代码中,若有适合场景,尽量用上更为高效、简洁,性能更好的Stream流;

2. 本文处理讲解Stream流的基本概念,还讲解了一些常用的Stream API,但是Stream的功能远远不仅与此,本文也没法举例出所以的API例子,建议读者结合API文档或者源码慢慢学习,熟悉掌握Stream的用法。

注:希望本文对读者有帮助,转载请注明出处!

没有更多推荐了,返回首页