说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是"IO流”呢?在java8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
传统集合的多步遍历代码
几乎所有的集合(如Collection 接口或Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。
循环遍历的弊端
Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下代码,可以发现:
为什么使用循环?
因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
试想一下,如果希望对集合中的元素进行筛选过滤:
传统方式:
import java.util.ArrayList;
import java.util.List;
public class DemoList {
public static void main(String[] args) {
//创建一个Lis集合,存储姓名
List list=new ArrayList<>();
list.add("cici");
list.add("vivi");
list.add("kaka");
list.add("vavia");
list.add("jack");
//对集合元素进行遍历,以v开头的都存储在集合里
List listA=new ArrayList<>();
for (String s : list) {
if (s.startsWith("v")){
listA.add(s);
}
}
//对listA集合进行过滤,只要姓名长度为4,再存储到新集和
List listB=new ArrayList<>();
for (String s : listA) {
if (s.length()==4){
listB.add(s);
}
}
//遍历ListB,依次打印
for (String s : listB) {
System.out.println(s);
}
}
}
输出:
vivi
使用Stream流的方式:
import java.util.ArrayList;
import java.util.List;
public class DemoStream {
public static void main(String[] args) {
//创建一个Lis集合,存储姓名
List list=new ArrayList<>();
list.add("cici");
list.add("vivi");
list.add("kaka");
list.add("vavia");
list.add("jack");
//对集合元素进行过滤,以v开头的都存储在集合里
//对listA集合进行过滤,只要姓名长度为4,再存储到新集合
//遍历ListB,依次打印
list.stream()
.filter(name->name.startsWith("v"))
.filter(name->name.length()==4)
.forEach(name-> System.out.println(name));
}
}
输出:
vivi
注意:请暂时忘记对传统IO流的固有印象!
整体来看,流式思想类似于工厂车间的”生产流水线”。
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型"步骤方案,然后再按照方案去执行它。
这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种”函数模型"。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。
这里的filter、map、skip 都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
备注:”Stream流“其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)
Stream(流)是一个来自数据源的元素队列
和以前的Collection操作不同,Stream操作还有两个基础的特征:
当使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source) ->数据转换->执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
Stream流属于管道流,只能被消费(使用)一次
第一个Stream流调用完毕方法,数据就会流转到下一个Stream上
而这时第一个Stream流已经使用完毕,就会关闭了
所以第一个Stream流就不能再调用方法了
IllegalStateException: stream has already been operated upon or closed
import java.util.stream.Stream;
public class DemoStream_filter {
public static void main(String[] args) {
//获取一个Stream流
Stream stream = Stream.of("cici", "vivi", "kaka", "mima", "vack");
//对Stream流中的元素进行过滤,只要开头是v的人
Stream stream2 = stream.filter(name -> name.startsWith("v"));
//遍历stream2
stream2.forEach(name-> System.out.println(name));
stream.forEach(name-> System.out.println(name));
}
}
输出:
java.util.stream.Stream
获取一个流非常简单,有以下几种常用的方式:
1.所有的Collection集合都可以通过Stream默认方法获取流
default Stream
2.Stream接口的静态方法of 可以获取数组对应的流
static
参数是一个可变参数,那么我们就可以传递一个数组
import java.util.*;
import java.util.stream.Stream;
public class DemoGetStream {
public static void main(String[] args) {
//把集合转换为Stream流
List list=new ArrayList<>();
Stream stream1 = list.stream();
Set set=new HashSet<>();
Stream stream2 = set.stream();
Map map=new HashMap<>();
//获取键,存储到set集合中
Set keySet=map.keySet();
Stream stream3 = keySet.stream();
//获取值,存储到Collection集合中
Collection values = map.values();
Stream stream4 = values.stream();
//获取键值对,(键与值映射关系)entrySet
Set> entries = map.entrySet();
Stream> stream5 = entries.stream();
//把数组转换成Stream流
Stream stream6 = Stream.of(1, 2, 3, 4, 5);
//可变参数,可以传递数组
Integer[] arr={1,2,3,4,5};
Stream stream7 = Stream.of(arr);
String[] arr2={"a","b","c","d"};
Stream stream8 = Stream.of(arr2);
}
}
1.4 常用方法
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
备注:本小节之外的更多方法,请自行参考API文档
虽然方法名字叫 forEach,但是与for循环中的“for-each”昵称不同。
void forEach(Consumer super T> action);
该方法接收一个Consumer 接口函数,会将每一个流元素交给该函数进行处理。
复习Consumer接口
java.util.function.Consumer
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据
简单记:
forEach方法,用来遍历流中的数据是一个终结方法,遍历之后就不能续调用Stream流中的其他方法。
import java.util.stream.Stream;
public class DemoStream_forEach {
public static void main(String[] args) {
//获取一个Stream流
Stream stream = Stream.of("cici", "vivi", "kaka", "mima", "jack");
//使用stream流中的方法forEach对Stream流中的数据进行遍历
// stream.forEach((String name)->{
// System.out.println(name);
// });
//Lambda优化
stream.forEach(name-> System.out.println(name));
}
}
输出:
可以通过 filter 方法将一个流转换成另一个子集流。方法签名:
Stream
该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
复习Predicate接口:
此前我们已经学习过 java.uti.stream.Predicate 函数式接口,其中唯一的抽象方法为:
boolean test(T t);
该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素; 如果结果为false,那么 fiter 方法将会舍弃元素。
import java.util.stream.Stream;
public class DemoStream_filter {
public static void main(String[] args) {
//获取一个Stream流
Stream stream = Stream.of("cici", "vivi", "kaka", "mima", "vack");
//对Stream流中的元素进行过滤,只要开头是v的人
Stream stream2 = stream.filter(name -> name.startsWith("v"));
//遍历stream2
stream2.forEach(name-> System.out.println(name));
}
}
如果需要将流中的元素映射到另一个流中,可以使用 map 方法。方法签名:
该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
复习Function接口
此前我们已经学习过 java.util.stream.Function 函数式接口,其中唯一的抽象方法为:
R apply(T t);
这可以将一种T类型转换成为R类型,而这种转换的动作,就称为”映射”。
import java.util.stream.Stream;
public class DemoStream_map {
public static void main(String[] args) {
//获取一个Stream流
Stream stream = Stream.of("1", "2", "3", "4", "5");
//使用map方法把字符串类型整数转换为Integer类型整数
Stream stream2 = stream.map(name -> Integer.parseInt(name));
//遍历stream2流
stream2.forEach(name-> System.out.println(name));
}
}
输出:
Stream流中的常用方法 count:用于统计stream流中元素的个数
long count();
count方法是一个终结方法,返回值是一个long类型的整数
所以不能再续调用Stream流中的其他方法了
import java.util.ArrayList;
import java.util.stream.Stream;
public class DemoStream_count {
public static void main(String[] args) {
//获取一个Stream流
ArrayList list=new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
Stream stream = list.stream();
long count = stream.count();
System.out.println(count);
}
}
输出:
7
limit 方法可以对流进行截取,只取用前n个。方法签名:
Stream
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。
limit方法是一个延迟方法,它只对流中元素进行截取,返回的是一个新的流,所以可以继续调用Stream流中的其他方法。
基本使用:
import java.util.stream.Stream;
public class DemoStream_limit {
public static void main(String[] args) {
//获取一个Stream流
String[] arr={"cici", "vivi", "kaka", "mima", "vack"};
Stream stream = Stream.of(arr);
//使用limit方法对Stream流中的元素进行截取,只要前三个元素
Stream stream2 = stream.limit(3);
stream2.forEach(name-> System.out.println(name));
}
}
输出:
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:
stream
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
基本使用:
import java.util.stream.Stream;
public class DemoStream_skip {
public static void main(String[] args) {
//获取一个Stream流
String[] arr={"cici", "vivi", "kaka", "mima", "vack"};
Stream stream = Stream.of(arr);
//使用skip方法跳过前3个元素
Stream stream2 = stream.skip(3);
stream2.forEach(name-> System.out.println(name));
}
}
如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat。
static
备注:这是一个静态方法,与java.lang.String 当中的 concat 方法是不同的。
该方法的基本使用代码如下:
import java.util.stream.Stream;
public class DemoStream_concat {
public static void main(String[] args) {
//获取一个Stream流
Stream stream = Stream.of("1", "2", "3", "4", "5");
//获取一个Stream流
String[] arr={"cici", "vivi", "kaka", "mima", "vack"};
Stream stream2 = Stream.of(arr);
//把以上两个流组合为一个流
Stream concat = Stream.concat(stream, stream2);
concat.forEach(name-> System.out.println(name));
}
}
输出:
题目
现在有两个ArrayList 集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤:
1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
2.第一个队伍筛选之后只要前3个人,存储到一个新集合中。
3.第二个队伍只要姓张的成员姓名,存储到一个新集合中
4.第二个队伍筛选之后不要前2个人;存储到一个新集合中。
5.将两个队伍合并为一个队伍;存储到一个新集合中。
6.根据姓名创建 Person 对象;存储到一个新集合中
7.打印整个队伍的Person对象信息
两个队伍(集合)的代码如下:
import java.util.ArrayList;
public class DemoStream {
public static void main(String[] args) {
//第一支队伍
ArrayList one = new ArrayList<>();
one.add("迪西热巴");
one.add("宋远桥");
one.add("苏星河");
one.add("石破天");
one.add("石中玉");
one.add("老子");
one.add("庄子");
one.add("洪七公");
//1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
ArrayList one1=new ArrayList<>();
for (String name : one) {
if (name.length()==3){
one1.add(name);
}
}
//2.第一个队伍筛选之后只要前3个人,存储到一个新集合中。
ArrayList one2=new ArrayList<>();
for (int i = 0; i < 3; i++) {
one2.add(one1.get(i));
}
//第二支队伍
ArrayList two = new ArrayList<>();
two.add("古力娜扎");
two.add("张无忌");
two.add("赵西颖");
two.add("张二丰");
two.add("尼古拉斯赵四");
two.add("张天爱");
two.add("张二狗");
//3.第二个队伍只要姓张的成员姓名,存储到一个新集合中
ArrayList two1=new ArrayList<>();
for (String name : two) {
if (name.startsWith("张")){
two1.add(name);
}
}
//4.第二个队伍筛选之后不要前2个人;存储到一个新集合中。
ArrayList two2=new ArrayList<>();
for (int i = 2; i < two1.size(); i++) {
two2.add(two1.get(i));
}
//5.将两个队伍合并为一个队伍;存储到一个新集合中。
ArrayList all=new ArrayList<>();
all.addAll(one2);
all.addAll(two2);
//6.根据姓名创建 Person 对象;存储到一个新集合中
ArrayList list=new ArrayList<>();
for (String name : all) {
list.add(new Person(name));
}
//7.打印整个队伍的Person对象信息
for (Person person : list) {
System.out.println(person);
}
}
}
输出 :
题目
将上一题当中的传统for循环写法更换为Stream流式处理方式。两个集合的初始内容不变,Person 类的定义也不变。
解答
等效的Stream流式处理代码为:
public class DemoStream1 {
public static void main(String[] args) {
//第一支队伍
ArrayList one = new ArrayList<>();
one.add("迪西热巴");
one.add("宋远桥");
one.add("苏星河");
one.add("石破天");
one.add("石中玉");
one.add("老子");
one.add("庄子");
one.add("洪七公");
//1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
Stream one1 = one.stream().filter(name -> name.length() == 3);
//2.第一个队伍筛选之后只要前3个人,存储到一个新集合中。
Stream one2 = one1.limit(3);
//第二支队伍
ArrayList two = new ArrayList<>();
two.add("古力娜扎");
two.add("张无忌");
two.add("赵西颖");
two.add("张二丰");
two.add("尼古拉斯赵四");
two.add("张天爱");
two.add("张二狗");
//3.第二个队伍只要姓张的成员姓名,存储到一个新集合中
Stream two1 = two.stream().filter(name -> name.startsWith("张"));
//4.第二个队伍筛选之后不要前2个人;存储到一个新集合中。
Stream two2 = two1.skip(2);
//5.将两个队伍合并为一个队伍;存储到一个新集合中。
Stream concat = Stream.concat(one2, two2);
//6.根据姓名创建 Person 对象;存储到一个新集合中
//7.打印整个队伍的Person对象信息
concat.map(name->new Person(name)).forEach(p-> System.out.println(p));
}
}
输出:
在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
来看一个简单的函数式接口以应用Lambda表达式
@FunctionalInterface
public interface Printable {
void print(String str);
}
在printable 接口当中唯一的抽象方法 print 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单:
分析:
Lambdo表达式的目的,打印参数传递的字符串
把参数s,传递给了System.out对象,调用out对象中的方法println对字符串进行了输出
注意:
1.System.out对象是已经存在的
2.println方法也是已经存在的
所以我们可以使用方法引用来优化Lambda表达式
可以使用System.out方法直接引用(调用)println方法
@FunctionalInterface
public interface Printable {
void print(String str);
}
public class DemoPrintable {
//定义一个方法,参数传递Printable接口,对字符串进行打印
public static void printString(Printable p){
p.print("helloWorld");
}
public static void main(String[] args) {
//调用printString方法,方法参数是一个函数式接口,可以传递Lambda表达式
printString((s)->{
System.out.println(s);
});
printString(System.out::println);
}
}
双冒号:: 为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者.
语义分析
例如上例中,System.out 对象中有一个重载的 println(String)方法恰好就是我们所需要的。那么对于printString 方法的函数式接口参数,对比下面两种写法,完全等效:
第一种语义是指:拿到参数之后经Lambda之手,继而传递给 ystem.out.println 方法去处理
第二种等效写法的语义是指:直接让System.out 中的 println 方法来取代Lambda。两种写法的执行效果完全样,而第二种方法引用的写法复用了已有方案,更加简洁。
注:Lambda 中 传递的参数 一定是方法引用中 的那个方法可以接收的类型,否则会抛出异常
推导与省略
如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式一一它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。
函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟
@FunctionalInterface
public interface Printable {
void print(String str);
}
public class MethondReferanceObject {
//定义一个成员方法,传递字符串,把字符串按照大写输出
public void printUpperCaseString(String str){
System.out.println(str.toUpperCase());
}
}
import JavaSE.Day20.DemoMethodReferance.Printable;
/**
* 通过对象名引用成员方法
* 前提:
* 对象名已经存在
* 成员方法也已经存在
* 就可以使用对象名引用成员方法
*/
public class DemoObjectMethodReferance {
//定义一个方法,参数传递Printable接口
public static void printString(Printable p){
p.print("hello");
}
public static void main(String[] args) {
printString((s)->{
//创建methodReferanceObject对象
MethondReferanceObject obj=new MethondReferanceObject();
//调用对象中的成员方法
obj.printUpperCaseString(s);
});
/**
* 使用方法引用优化Lambda
* 对象是已经存在的MethodReferanceObject
* 成员方法也是已经存在的
* 所以可以使用对象名来引用成员方法
*/
MethondReferanceObject obj=new MethondReferanceObject();
printString(obj::printUpperCaseString);
}
}
通过类名引用静态成员方法
类已经存在,静态成员方法也已经存在
就可以通过类名直接引用静态成员方法
public class DemoStaticMethodReferance {
//定义一个方法。方法的参数传递要计算绝对值的整数和函数式接口calcable
public static int method(int number,Calcable c){
return c.calAbs(number);
}
public static void main(String[] args) {
//调用mathod方法。传递计算绝对值得整数,和Lambda表达式
int num=method(-10,(n)->{
//对参数进行绝对值的计算并返回结果
return Math.abs(n);
});
System.out.println(num);
/**
* 使用方法引用优化Lambda表达式
* Math类是存在的
* abs计算绝对值的静态方法也是已经存在的
* 所以我们可以直接通过类名引用静态方法
*/
int num2 = method(-10, Math::abs);
System.out.println(num2);
}
}
输出:
//定义见面的函数式接口
@FunctionalInterface
public interface Greetable {
//定义一个见面的方法
public void greet();
}
/**
* 定义父类
*/
public class Human {
//定义一个sayHello的方法
public void sayHello(){
System.out.println("hello,我是Human");
}
}
//定义子类
public class Man extends Human{
//子类重写父类sayHello的方法
@Override
public void sayHello() {
System.out.println("hello,我是man");
}
//定义一个方法,参数传递Greetable接口
public void method(Greetable greetable){
greetable.greet();
}
public void show(){
//调用method方法,方法参数Greetable是函数式接口,所以可以传递Lambda表达式
// method(()->{
// //创建父类Human对象
// Human h=new Human();
// //调用父类的sayHello方法
// h.sayHello();
//
// });
//因为有子父类关系,所以存在一个关键字super代表父类,所以可以直接使用super调用父类成员方法
// method(()->{
// super.sayHello();
// });
//使用super引用父类的成员方法
//super是已经存在的,父类成员方法也是已经存在的
//所以可以使用super引用父类成员方法
method(super::sayHello);
}
public static void main(String[] args) {
new Man().show();
}
}
输出:
hello,我是Human
//定义一个富有的函数式接口
@FunctionalInterface
public interface Richable {
//定义一个想买就买什么的方法
void buy();
}
/**
* 通过this引用本类的成员方法
*/
public class Husband {
//定义一个买房子的方法
public void buyHouse(){
System.out.println("北京二环内买一套四合院。");
}
//定义一个结婚的方法,参数传递Richable接口
public void marry(Richable r){
r.buy();
}
//定义一个非常高兴的方法
public void soHappy(){
//调用结婚的方法,方法的参数是函数式接口,可以传递Lambda表达式
// marry(()->{
// //使用this.成员方法调用本类买房子的方法
// this.buyHouse();
// });
//使用方法引用优化Lambda表达式
/**
* this已经存在,本类的成员方法buyHouse也已经存在
* 所以可以直接使用this引用本类的成员方法buyHouse
*/
marry(this::buyHouse);
}
public static void main(String[] args) {
new Husband().soHappy();
}
}
输出:
北京二环内买一套四合院。
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
/**
* 定义一个创建Person对象的函数式接口
*
*/
@FunctionalInterface
public interface PersonBuider {
//定义一个方法,根据传递的姓名创建Person对象并返回
Person buildPerson(String name);
}
/**
* 类的构造器引用,构造器也叫构造方法
*/
public class Demo {
//定义一个方法,参数传递姓名和PersonBuilder接口,方法中通过姓名创建Person对象
public static void printName(String name, PersonBuider pb){
Person person=pb.buildPerson(name);
System.out.println(person.getName());
}
public static void main(String[] args) {
//调用printName方法,方法参数PersonBuilder接口是函数式接口,可以传递Lambda表达式
printName("cici",(String name)->{
return new Person(name);
});
/**
* 使用方法引用优化Lambda表达式
*构造方法new Person(String name)已知
* 创建对象的方式已知new
* 可以使用Person引用new创建对象
*/
printName("cici",Person::new);//使用Person类的带参构造方法,通过传递的姓名创建对象
}
}
/**
* 定义一个创建数组的函数式接口
*/
@FunctionalInterface
public interface ArrayBuilder {
//定义一个创建int类型数组的方法,参数传递数组长度,返回创建好的int类型数组
int[] builderArray(int length);
}
import java.util.Arrays;
/**
* 数组的构造器引用
*/
public class Demo {
//定义一个方法,方法的参数传递创建数组的长度和arrayBuilder接口,方法内部根据传递的长度使用ArrayBuilder中的方法创建数组并返回
public static int[] createArray(int length,ArrayBuilder ab){
return ab.builderArray(length);
}
public static void main(String[] args) {
/**
* 调用createArray方法传递数组长度和Lambda表达式
*/
int[] arr1=createArray(10,(len)->{
//根据数组的长度创建数组并返回
return new int[len];
});
System.out.println(arr1.length);
/**
* 使用方法引用优化Lambda表达式
* 已知创建的就是int类型的数组,数组的长度也是已知的
* 所以可以使用方法引用
* int[]引用new,根据参数传递的长度创建数组
*/
int[] arr2=createArray(10,int[]::new);
System.out.println(Arrays.toString(arr2));
System.out.println(arr2.length);
}
}