Steram流式计算
一、概述
说到Stream便容易想到 I/O Stream,而实际上,谁规定"流"就一定是"I0流"呢 ?在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
什么是Stream流式计算
大数据∶存储+计算
集合、MySQL
本质就是存储
东西的;
计算
都应该交给流来操作
!例子:
1.1、引言
传统集合的多步遍历代码
几乎所有的集合(如collection接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如∶
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("李白");
arrayList.add("王昭君");
arrayList.add("韩信");
arrayList.add("刘邦");
// foreach 输出
for (String s : arrayList) {
System.out.println(s);
}
}
这是一段非常简单的集合遍历操作∶对集合中的每一个字符串都进行打印输出操作。
循环遍历的弊端
Java 8的Lambda让我们可以更加专注于做什么( What ),而不是怎么做( How ),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现∶
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗? 遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
试想一下,如果希望对集合中的元素进行筛选过滤∶
- 将集合A根据条件一过滤为子集B;
- 然后再根据条件二过滤为子集C。
那怎么办?在Java 8之前的做法可能为∶
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("李白");
arrayList.add("李世民");
arrayList.add("王昭君");
arrayList.add("李信");
arrayList.add("韩信");
arrayList.add("刘邦");
//对arrayList中的元素进行过滤,只要以李开头的。存到一个新集合
ArrayList<String> listA = new ArrayList<>();
for (String s : arrayList) {
if (s.startsWith("李")){
listA.add(s);
}
}
//对listA进行过滤,只要名字长度为3的。
ArrayList<String> listB = new ArrayList<>();
for (String s : listA) {
if (s.length()==3){
listB.add(s);
}
}
//最后对满足条件的集合遍历输出
for (String s : listB) {
System.out.println(s);
}
}
这段代码中含有三个循环,每一个作用不同:
1.首先筛选所有姓李的人;
2.然后筛选名字有三个字的人;
3.最后进行对结果进行打印输出。
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环
。这是理所当然的么? 不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。
那 Lambda 的 衍生物 Stream 能给我们带来怎样更加优雅的写法呢?
Stream的更优写法
下面来看一下借助Java 8的Stream APl,什么才叫优雅∶
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("李白");
arrayList.add("李世民");
arrayList.add("王昭君");
arrayList.add("李信");
arrayList.add("韩信");
arrayList.add("刘邦");
arrayList.stream()
.filter(name->name.startsWith("李"))
.filter(name->name.length()==3)
.forEach(name-> System.out.println(name));
}
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义∶获取流、过滤姓李、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
1.2、流式思想概述
注意 : 请暂时忘记对传统lO流的固有印象!
整体来看,流式思想类似于工厂车间的“生产流水线"。当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个"模型"步骤方案,然后再按照方案去执行它。
这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种"函数模型”。图中的每一个方框都是一个"流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。这里的 filter 、map 、skip都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
备注 :“Stream流" 其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。
Stream(流)是一个来自数据源的元素队列。
- 元素是特定类型的对象,形成一个队列。Java中的Stream并不会存储元素,而是按需计算。
- 数据源 流的来源。可以是集合,数组等。
和以前的Collection操作不同,Stream操作还有两个基础的特征︰
- Pipelining : 中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格( fluentstyle )。这样做可以对操作进行优化,比如延迟执行(Iaziness)和短路( short-circuiting)。
当使用一个流的时候,通常包括三个基本步骤∶获取一个数据源( source )→数据转换→执行操作获取想要的结果,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
1.3、获取流
==java.util.stream.Stream==是Java 8新加入的最常用的流接口
。(这并不是一个函数式接口)
获取一个流非常简单,有以下几种常用的方式∶
- 所有的 collection集合 都可以 通过
stream()
默认方法获取流 。 - Stream接口 的 静态方法
static <T> Stream<T> of( T... values)
可以获取数组对应的流 。
根据Collection获取流
首先,java.util.collection 接口中加入了 default Stream<E> stream()
方法 用来获取流,所以其所有实现类均可获取流。
public static void main(String[] args) {
//单列集合类型,直接把集合转换为Stream流
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();
Set<String> set = new HashSet();
Stream<String> stream1 = set.stream();
//非单列集合类型,通过间接方式
//获取键,存储到一个Set中
Map<String,String> map = new HashMap<>();
Set<String> keySet = map.keySet();
Stream<String> stream2 = keySet.stream();
//获取值,存储到一个集合中。
Collection<String> values = map.values();
Stream<String> stream3 = values.stream();
//获取键值对,存储的一个集合中,
Set<Map.Entry<String, String>> entries = map.entrySet();
Stream<Map.Entry<String, String>> stream4 = entries.stream();
//把数组转换为Stream流
Stream<Integer> system5 = Stream.of(1,2,3,4,5,6);
//可变参数可以传递数组
Integer[] arr = {7,8,9,10,11};
Stream<Integer> stream6 = Stream.of(arr);
String[] arr2 = {"a","bb","ccc"};
Stream<String> stream7 = Stream.of(arr2);
}
备注:of方法的参数其实是一个可变参数,所以支持数组。
1.4、常用方法
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种︰
- 延迟方法∶返回值类型仍然是
stream
接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。) - 终结方法︰返回值类型不再是
stream
接口自身类型的方法,因此不再支持类似StringBuilder
那样的链式调用。本小节中,终结方法包括count
和forEach
方法。
备注 : 本小节之外的更多方法,请自行参考API文档。
逐一处理 : forEach[终结方法]
虽然方法名字叫forEach,但是与 for 循环中的“for-each" 昵称不同
void forEach(Consumer<? super T> actioh ) ;
该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。
复习Consumer接口
java.util.function.Consumer<T>//接口是一个消费型接口
//Consumer接口中包含抽象方法
void accept(T t)//意为消费一个指定泛型的数据
基本使用︰
/**
*@描述: 消费型接口consumer + stream 练习
*消费型接口consumer:
* new Consumer<String>() {
* @Override
* public void accept(String s) {
* 消费操作
* }
* }
* */
public class ConsumerTest {
public static void main(String[] args) {
//构造stream流
Stream<String> stream = Stream.of("张三","李四","王五","赵六");
//利用forEach输出
stream.forEach(name-> System.out.println(name));
}
}
过滤 : filter
可以通过filter方法将一个流转换成另一个子集流。
stream<T> filter(Predicate<? super T> predicate);
该接口接收一个Predicate函数式接口参数(可以是一个Lambda或方法引用 ) 作为筛选条件。
复习Predicate接口
此前我们已经学习过 java.util.stream.Predicate函数式接口,其中唯一的抽象方法为︰
boolean test(T t);
该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true
,那么Stream流的
filter( )方法将会留用元素
; 如果结果为false
,那么filter方法将会舍弃元素
。
基本使用︰
/**
*@描述: 判断接口Predicate + stream 练习
*判断接口Predicate
* new Predicate<String>() {
* @Override
* public boolean test(String s) {
* 判断操作
* }
* }
*/
public class PredicateTest {
public static void main(String[] args) {
//构造stream流
Stream<String> stream = Stream.of("张三","李四","王五","赵六","张良","张飞");
//利用filter()进行筛选
Stream<String> stream1 = stream.filter(name -> name.startsWith("张"));
//利用forEach输出
stream1.forEach(name-> System.out.println(name));
}
}
注意:
Stream流属于管道流,只能被消费(使用)一次
第一个Stream流调用完华方法,数据就会流转到下一个stream上。而这时第一个Stream流已经使用完毕,就会关闭了
所以第一个stream流就不能再调用方法了。
如果再调用方法,会抛出:ilegalstateException: stream has already been operated upon or closed
映射 : map
如果需要将流中的元素映射到另一个流中,向以使用==map( )==方法。
<R> Stream<R> map( Function<? super T, ? extends R> mapper );
该接口需要一个Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
复习Function接口
此前我们已经学习过 java.util.stream.Function函数式接口,其中唯一的抽象方法为
Rapply(T t);
这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。
基本使用︰
/**
*@描述: 转换型接口 function + stream 练习
*转换型接口 function
* new Function<String, Object>() {
* @Override
* public Object Rapply(String s) {
* 转换操作
* }
* }
*/
public class FunctionTest {
public static void main(String[] args) {
Stream<String> stream = Stream.of("1","3","1","4");
Stream<Integer> integerStream = stream.map(s -> Integer.parseInt(s));
integerStream.forEach(num-> System.out.println(num));
}
}
统计个数 : count( )[终结方法]
正如旧集合collection
当中的 ==size( )==方法一样,流提供 ==count( )==方法来数一数其中的元素个数
long count();
该方法返回一个 long值 代表元素个数 (不再像旧集合那样是 int 值)。
取用前几个 : limit( )
==limit( )==方法可以对流进行截取,只取用前n个。
Stream<T> limit( long maxsize);
参数是一个Long型,如果集合当前长度大于参数贝进行截取,否则不进行操作。
limit方法是一个延迟方法 , 只是对流中的元素进行截取 , 返回的是一个新的流,所以可以继续调用Stream流中的其他方法。
跳过前几个 : skip( )
如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流。
Stream<T> skip( long n);
如果流的当前长度大于n,则跳过前 n个;否则将会得到一个长度为0的空流。
组合 : concat( )
如果有两个流,希望合并成为一个流,那么可以使用 stream 接口
的 静态方法concat()
statick<T>Stream<T> concat(Stream<? extends T> a,stream<? extends T> b)
备注 : 这是一个静态方法,与java.lang.string当中的
concat( )
方法是不同的。
基本使用︰
public static void main(String[] args) {
Stream stream1 = Stream.of("李白","王昭君","李信","李世民","韩信","刘邦");
Stream stream2 = Stream.of("唐三","弗兰德","玉小刚","唐昊");
Stream concatStream = Stream.concat(stream1, stream2);
concatStream.forEach(name-> System.out.println(name));
}