一、Stream流
1.Stream解决集合类库的弊端
Java8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
几乎所有的集合(如Collection接口或Map接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必须的添加、删除、获取外,最典型的就是集合遍历。
循环遍历的弊端:
Java8中的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),此前已经结合内部类进行了对比说明。
对于采用for循环遍历集合的代码,可以分析:
for循环的语法就是“怎么做”
for循环的循环体就是“做什么”
使用循环的目的是为了进行遍历,但循环并不是遍历的唯一方式。
遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。
前者是目的,后者是方式。
Example:使用传统的方式,遍历集合,对集合中的数据进行过滤。
过程分析:
一共使用了3个循环,作用分别为:
1.首先筛选所有姓张的人
2.然后筛选名字有三个字的人
3.最后进行对结果进行打印输出
弊端分析:
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这并不是唯一的方法。
循环是做事情的方式,并不是目的。另一方面,使用线性循环意味着只能遍历一次。如果希望多次遍历, 只能在使用另一个循环从头开始。
解决方法:
使用Lambda的衍生物Stream,优化代码
2.流式思想
使用Stream流的方式,遍历集合,对集合中的数据进行过滤。
Stream流是JDK1.8之后出现的,关注的是做什么,而不是怎么做.
流式思想:
原理和传统IO流有所不同,整体来看,流式思想类似于工厂车间的“生产流水线”,
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,
我们应该首先拼接好一个 “模型”步骤方案,然后再按照方案去执行它。
3.流模型
对于list集合,有过滤、过滤、打印等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。调用指定的方法,可以从一个【流模型】转换为另一个流模型。
stream方法中有一个【filter】方法,filter方法的参数是Predicate接口,是一个函数式接口,可以传递Lambda表达式.。
stream方法中有一个【forEach】方法,参数是Consumer接口,是一个函数式接口,可以传递Lambda表达式
这里的filter、filter都是在对函数模型进行操作,集合中的元素【并没有真正被处理】。只有当终结方法——打印输出执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的【延迟执行】特性,即满足条件才会执行,不满足条件就不执行。
注意事项:
“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储
任何元素(或其地址值)
Stream(流)是一个来自数据源的元素队列:
1.元素是特定类型(例如String)的对象,形成一个队列(filter、filter、soutp排成一个队列)。
Java中的Stream并不会存储元素(例如第一个filter,并没有真正筛选出姓张的元素),
而是按需计算。
2.数据源:流的来源。可以是集合,数组等。
区别于Collection,Stream操作还有两个基础的特征:
1.Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式
风格(fluent style)。这样做可以对操作进行优化,比如延迟执行(laziness)和短路(short-circuiting)。
2.内部迭代:以前对集合遍历都是通过Iterator或者增强for的方式,显示的在集合外部进行迭代,
叫做外部迭代(即先有集合,在创建迭代器进行遍历)。Stream提供了内部迭代的方式,流可以
直接调用遍历方法(即stream.forEach)。
当使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)-> 数据转换 -> 执行操作获取想要的结果。
每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换),
这就允许对其操作可以像链条一样排列,变成一个管道。
4.两种获取Stream流的方式
Java.util.stream.Stream是Java8新加入的最常用的流接口。(这并不是一个函数式接口,因为不止一个抽象方法)
获取一个流非常简单,有以下几种常用的方式:
1.所有的Collection集合(单列集合)都可以通过stream默认方法获取流
default Stream stream()
2.Stream接口的静态方法of可以获取数组对应的流
static Stream of (T... values)
参数是一个可变参数,那么就可以传递一个数组(可变参数底层就是一个数组)
注意事项:
Collection集合可以【直接转换】为Stream流,
Map集合可以【间接先转换】为Collection集合或者Set集合,在转换为Stream流。
对于数组,可以使用Stream接口的【静态方法】of
5.Stream流中的常用方法
流模型中的常用方法,可以分为两种:
1.延迟方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。(除了终结方法外,
其余方法均为延迟方法)
2.终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的
链式调用
Stream流属于管道流,只能使用(消费)一次:
第一个Stream流调用完毕方法,数据就会流转到下一个Stream流上
而这时第一个Stream流已经使用完毕,就会关闭了
所以第一个Stream流就不能再调用方法
5.1 forEach
Stream流中的方法——forEach:用来遍历流中的数据
void forEach(Consumer super T> action);
该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。
Consumer接口是一个【消费型】的函数式接口,所以可以传递Lambda表达式,消费数据
简单记:
forEach方法,用来遍历流中的数据
是一个【终结方法】,遍历之后,就不能继续调用Stream流中的其他方法
5.2 filter
Stream流中的方法——filter:用于对Stream流中的数据进行过滤
Stream filter(Predicate super T> predicate);
filter方法的参数Predicate是一个函数式接口,所以可以传递Lambda表达式,对数据进行过滤
Predicate中的抽象方法:
boolean test(T t);
5.3 map
如果需要将流中的元素映射到另一个流中,可以使用map方法。
Stream map(Function super T, ? extends R> mapper);
该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
Function中的抽象方法:
R apply(T t);
这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。
5.4 count
Stream流中的方法——count:同旧集合Collection当中的size方法一样,count方法用来统计流中元素的个数
long count();
该方法返回值是一个long类型,不再是旧集合的int类型。
count方法是一个【终结方法】,所以不能再继续调用流中的其他方法了。
5.5 limit
Stream流中的方法——limit:可以对流进行截取,只取用前n个
Stream limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。
limit方法是一个【延迟方法】,只是对流中的元素进行截取,返回的是一个新的流,所以可以继续调用流中的其他方法。
5.6 skip
Stream流中的方法——skip:如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流
Stream skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
5.7 concat
Stream流中的方法——concat:如果有两个流,希望合并为一个流,那么可以使用Stream接口的静态方法concat
static Stream concat(Stream extends T> a, Stream extends T> b)
这是一个静态方法,与java.lang.String中的concat方法是不同的。
二、方法引用
1.双冒号::
双冒号::写法,被称为"方法引用",双冒号是一种新的语法,被称为引用运算符,其所在的表达式被称为“方法引用”。
如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
Lambda表达式写法:s->System.out.println(s);
方法引用写法:System.out ::println
以上两种写法是等效的,但又有所区别:
1.第一种的语义是指,拿到参数之后经Lambda之手,继而传递给System.out.println方法去处理
2.第二种的语义是指,直接让System.out中的方法println方法来取代Lambda。
两种写法的执行效果一样,但第二种方法引用的写法复用了已有方案,更加简洁。
注:Lambda中传递的参数一定是方法引用中的那个方法可以接收的类型,否则会抛出异常
2.通过对象名引用成员方法
使用前提:
1.对象名是已经存在的
2.成员方法时已经存在的
代码示例:
printString((String s)->{
System.out.println(s);
});
/*
分析:
Lambda表达式的目的:打印参数传递的字符串
把参数s,传递给了System.out对象,调用out对象中的方法println,对字符串进行了输出
注意:
1.System.out对象是【已经存在】的
2.println方法也是【已经存在】的
所以可以使用【方法引用】来优化Lambda表达式,即使用方法引用的前提:方法或对象已经存在
可以使用System.out方法直接引用(调用)println方法
*/
printString(System.out ::println); //对象引用方法,双冒号::为引用运算符。
3.通过类名引用静态成员方法
使用前提:
1.类已经存在
2.静态成员方法也已经存在
4.使用super引用父类的成员方法
使用前提:
1.super是已经存在的,super代表父类对象
2.父类的成员方法是已经存在的
所以,可以使用super来引用父类的成员方法
5.通过this引用本类的成员方法
使用方法引用优化Lambda表达式,前提条件:
1.this是已经存在的,this指代当前对象
2.本类的成员方法也是存在的
所以,可以直接使用this引用本类的成员方法
6.类的构造器(构造方法)引用
通过构造方法引用new,即通过类调用其带参构造方法。
使用方法引用优化Lambda表达式的前提:
1.构造方法new Person(String name)是已知的
2.创建对象是已知的:new
所以就可以使用Person引用new创建对象,实际上就是Person类调用其带参构造方法,
通过传递的姓名创建对象。
7.数组的构造器引用
通过数组引用new。
使用方法引用优化Lambda表达式的前提:
1.已知创建的就是int[]数组
2.数组的长度也是已知的
所以,可以使用方法引用:int[]引用new,根据参数传递的长度来创建数组