自主学习报告第一周
流 Stream
什么是流?流和集合有什么区别?
在Java中,流是对数据操作的一种方式,它和集合不尽相同,却又完全不同。主要区别在于:
1. 流不会自己存储数据,它是对数据操作的一种方式,而不是存储数据的容器,因此流的数据可能以其他方式存储在底层的集合中。
2. 流不会改变源对象,相反它会通过流转换生成新的流,每一个流都会在使用过后,不论最终有没有终止操作,流都会被自动关闭。
3. 流的执行可能是延迟的,流经过中间转换形成新的流,只要没有进行终止操作,流里面定义的代码不会被执行,直到流的终止操作,流开始执行内部定义的代码。
4. 相对于集合,流可以通过并行流被并行操作,因此在执行一些操作时比集合更高效。
流的三种操作
1. 创建流
2. 流的转换
3. 终止操作
创建流的几个方法
通过静态方法Stream.of(Array)创建流
Stream<String> stream = Stream.of("first", "second", "third");
通过Arrays.Stream(Array, From, To)动态地创建流
String[] strArr = {"first", "second", "third"};
Stream<String> stream = Arrays.Stream(strArr, 1, 2);
通过静态方法Stream.empty()创建一个空流。我不太明白创建空流的意义,这里先提出这个疑问,假如后来能够解决这个疑问,我会重新修改这篇文章
Stream<String> stream = Stream.empty();
还有两个可以创建无限流的方法,第一个是通过generate方法
//通过这种方式可以创建一个包含"ssr"字段的无限流
Stream<String> stream1 = Stream.generate(() -> return "ssr");
//通过这种方式可以创建一个生成随机数字的无限流
Stream<Double> stream2 = Stream.generate(Math::random);
上例generate方法接收的是一个supplier函数式接口,该接口不需要参数,输出任意一个泛型结果。因此通过该方法不仅仅能创建数值类型和String类型的流,其他类型的流也能创建,包括引用类型,创建引用类型的流的时候,流中的每一个元素都是对原类型的引用,即指向同一片内存空间。
还可以通过迭代器创建一个规律递增的流
Stream<BigInteger> stream = Stream.iterate(BigInteger.ZERO, x -> x.add(BigInteger.ONE));
其中第一个参数时种子seed,第二个参数接受一个函数式接口UnaryOperator。UnaryOperator接收一个T类型的参数,返回一个相同类型的结果。UnaryOperator和Comsumer有一点类似,假如接收的参数是一个引用类型的话,两者能达到相同的结果,如果是基本类型的话,UnaryOperator能返回一个改变后的基本类型,而Consumer对参数的操作不会被带出Lambda作用域。
对流的转换操作
对流的转换操作是指对流进行一系列中间操作,使其生成一个新的流,任何一个流被进行中间操作后同时关闭原始流,因此称为为流的转换。
filter方法可以对流进行过滤,它接收一个predicate接口,该接口主要用于逻辑运算,接收一个任意参数,返回一个布尔值。
Stream<String> stream = Stream.of("windows", "unix", "linux", "os");
stream.filter(x -> x.length() > 4).forEach(System.out::println);
map方法用于对流元素进行特定操作后返回一个新的流,比如将对字符创进行特定操作
stream.map(String::toUpperCase).forEach(System.out::println);
map方法接受一个function接口,该接口接收一个任意泛型参数,返回一个结果。map方法把调用者的每一个元素取出来,应用于实现function接口的表达式,然后对所有元素重新生成一个新的流。
假如我们的操作并不是产生一个包含字符串的流而是生成一条包含一系列子流的流时,用map方法我们得到的只是一条包含各个流的流,想要将这些流中的元素展开并整合成一个新流,我们可以考虑使用flatMap
public static Stream<String> streamAsString(String str){
return Arrays.asList(str.split("")).stream();
}
Stream.of("android", "ios", "synban").flatMap(LambdaDemo::streamAsString).forEach(System.out::print);
LambdaDemo是我的类,这里方法引用为我们缩减了一些不必要的代码。flatMap会将方法应用于每一个元素,调用者本身成为了参数,通过这种方法产生一系列流,最后展开每个元素,将所有元素收集到新流里。
对流的组合操作主要有几个方法是:
- limit(n)取流中前n位元素
- skip(n)跳过n位元素
- concat(s1, s2)可以组合两个流
- peek用于复制一个具有相同元素的流
concat可以组合两个流,在某种情况下,flatMap也能实现这个功能,flatMap不仅能组合两个流,还能组合更多的流。下面是两个方法的效率的比较。
//Test concat Vs flatMap
//____________________________________________________________________________________________________
String str1 = "Hello world java8 ni hao jiuu goo jinn ";
String str2 = "one two three four five six seven eight night";
for(int i=0; i<20; i++){
str1 += str1;
str2 += str2;
}
Stream<String> s1 = Stream.of(str1.split(" "));
Stream<String> s2 = Stream.of(str2.split(" "));
System.out.print("使用concat方式的时间损耗:");
long t1 = System.nanoTime();
Stream.concat(s1, s2);
long t2 = System.nanoTime();
System.out.println(t2 - t1);
System.out.println();
System.out.print("使用flatMap方式的时间损耗:");
t1 = System.nanoTime();
Stream.of(s1, s2).flatMap(x -> x);
t2 = System.nanoTime();
System.out.println(t2 - t1);
//_______________________________________________________________________________________________________
测试了几次,结果都如下所示
证实concat的效率大约比flatMap快一半,但是上帝给你关了一扇门的同时还给你关了一扇窗!之后我又尝试对多个流进行组合,事实证明,flatMap并不适合用来组合流,因为它的效率远小于concat,而它所带来的好处仅仅是少写一两行代码。
下面是使用limit和skip的例子
Stream.generate(Math::random).limit(10).forEach(System.out::println);
Stream.of("my name is chencj yo".split(" ")).skip(3).forEach(System.out::println);
我一直在思考peek方法的意义,我在想peek方法能不能达到这样的目的呢?就是我每一次调用流的peek方法都能对流的每一个元素进行一次操作,操作完毕之后原封不动把原始流赋值给一个新流,这样我就能达到流的复用的目的。事实证明这样是不行的,因为会陷进一个悖论:每次我调用peek复制原始流的时候,可以将流的元素作为参数传进去并操作元素,但是要完成这种操作我不得不调用一个终止操作符,而调用终止操作符后流会被关闭,关闭后的流就不能被赋值给一个新流。虽然对流的复用无法实现,但是我们能够通过peek方法对流的元素进行多次操作,不过这种操作不能被返回出来,也就是只能进行某种没有返回值的终止操作或者当赋值流中每个元素被操作的时候显示元素的状态。下面是对peek的应用。
int[] count ={0};
Stream<String> s = Stream.of("my name is chen cj".split(" ")).peek(x -> System.out.println("执行第" + ++count[0] + "次操作"));
s.forEach(System.out::println);
结果如下:
这是愚者我想到的peek存在的其中一个意义,拓展开来,当我们读取文件的时候就可以通过这种方式显示我们读取了几个文件,当前正在读取第几个文件。
Stream<String> ss = Stream.of("go to school to study".split(" ")).peek(x -> System.out.println(x.toUpperCase())).peek(x -> System.out.println(x.toLowerCase()));
System.out.println(ss.count());
输出结果为
通过这种方式可以实现实际意义上的流的复用,不过这种复用是每次操作都会应用到每一个元素上的。当然,这种用法仅仅是缺少正式项目经验的我所推想出来的,实际在项目开发中这种方法应该可以有更恰当的应用。
在这里要补充三点,是我之前的内容所遗漏的:
Stream是流,流是对数据的操作,但不会改变原始数据,例如之前工资的例子中用到consumer改变元素内部的值,这种改变不会被写进堆中,只是在流中数据被改变了(准确的说,是生成包含改变后的数据的新流)。包括上例的count[0],当我们直接通过count[0]访问的时候count[0]没有改变,依然是1,这种改变只存在流中,因此只能通过流来访问改变后的数据,这个大概就是别人说的闭包吧。把数据捕获,然后和周围环境筑起高高的围墙。
流假如没有声明泛型的类型,实际可以存储多种不同类型的数据。但是我觉得这种用法实际没有多大用处,因为流的用处是对数据进行操作,假如不知道数据类型的话就无从下手操作数据。
网上很多说法说流在终止操作之后被关闭,实际不然,流只要被使用到就会被关闭。下面是验证代码。
Stream<String> s = Stream.of("a", "b");
s.peek(System.out::print);
//z.forEach(System.out::print);
上述代码对s流仅仅是进行了转换操作,注释掉forEach语句执行一次没有输出结果说明该操作不是终止操作。去掉注释再执行,编译器报了stream has already been operated upon or closed,说明流已经被关闭,通过这一点说明流在转换过程中原始流就已经被关闭了。