Java 中的Stream
Stream API从Java 8开始引入,通常用来处理集合类对象。一个Stream对象由一个序列的其他对象组成,将其包装成stream对象使得可以使用流水线的方式对这些对象施加各种处理从而得到想要的结果。
Java Stream的特点有:stream不是一种数据结构,它以各种集合类、数组或者IO流作为输入;
stream 不改变原有的数据结构,它为各个流水线方法提供输入对象,随后返回一个新的结果,但输入的数据不会被改变;
每个中间操作都会被延迟执行,中间操作同样会返回stream对象以作为下一步流水线方法的输入对象。最终操作标记stream处理的结束,并返回最终结果。
Stream 支持的操作
中间操作(Intermediate Operations)
map : map方法的返回值是一个stream对象,可以被下一个流水线操作处理。map方法将会对stream对象中的每个元素执行给定的方法,相当于将一个集合的元素通过一个函数进行映射,返回的是映射结果集合组成的stream对象。List number = Arrays.asList(2,3,4,5);
// 将number中的每个元素都乘以2
List square = number.stream().map(x->x*x).collect(Collectors.toList());filter : filter方法用来对所给的元素按照某种条件筛选,只留下符合条件的元素List names = Arrays.asList("Reflection","Collection","Stream");
// 筛选出S开头的单词
List result = names.stream().filter(s->s.startsWith("S")).collect(Collectors.toList());sorted : 用来对stream中的元素排序List names = Arrays.asList("Reflection","Collection","Stream");
//从小到大对单词排序
List result = names.stream().sorted().collect(Collectors.toList());
最终操作(Terminal Operations)collect : collect方法用于收集中间操作的结果,将stream对象还原成原有数据结构或者转为其他数据结构后返回。List number = Arrays.asList(2,3,4,5,3);
Set square = number.stream().map(x->x*x).collect(Collectors.toSet());forEach : forEach方法可以迭代Stream中的所有元素,执行给定的操作(如打印或写数据流)List number = Arrays.asList(2,3,4,5);
number.stream().map(x->x*x).forEach(y->System.out.println(y));reduce : reduce 方法用来将stream对象中的元素进行累计操作,最终变成一个值,并将这个值返回。如返回stream所有元素的累加值或者累积值等。List number = Arrays.asList(2,3,4,5);
// 先用filter将number中的偶数筛选出来,然后把所有的筛选结果加起来作为返回值。reduce的一个参数是累计的初始值,第二个参数指定累计的操作。
int even = number.stream().filter(x->x%2==0).reduce(0,(ans,i)-> ans+i);
这里ans变量初始值为0,i 表示number中的元素,将number中的所有元素与ans相加并返回ans。
示例程序//a simple program to demonstrate the use of stream in java
import java.util.*;
import java.util.stream.*;
class Demo
{
public static void main(String args[])
{
// create a list of integers
List number = Arrays.asList(2,3,4,5);
// demonstration of map method
List square = number.stream().map(x -> x*x).
collect(Collectors.toList());
System.out.println(square);
// create a list of String
List names =
Arrays.asList("Reflection","Collection","Stream");
// demonstration of filter method
List result = names.stream().filter(s->s.startsWith("S")).
collect(Collectors.toList());
System.out.println(result);
// demonstration of sorted method
List show =
names.stream().sorted().collect(Collectors.toList());
System.out.println(show);
// create a list of integers
List numbers = Arrays.asList(2,3,4,5,2);
// collect method returns a set
Set squareSet =
numbers.stream().map(x->x*x).collect(Collectors.toSet());
System.out.println(squareSet);
// demonstration of forEach method
number.stream().map(x->x*x).forEach(y->System.out.println(y));
// demonstration of reduce method
int even =
number.stream().filter(x->x%2==0).reduce(0,(ans,i)-> ans+i);
System.out.println(even);
}
}
重要提示一个Stream由 数据输入 + 0个或多个中间操作函数 + 1个最终操作函数。它是一个流水线操作。
Stream用来对一系列元素做流水线操作,但它不会改变原来输入的数据。
关于Stream的懒加载机制
参考: 详解Java 8中Stream类型的“懒”加载
Stream之所以“懒”的秘密在于每次在使用Stream时,都会连接多个中间操作,并在最后附上一个结束操作。 像map()和filter()这样的方法是中间操作,在调用它们时,会立即返回另一个Stream对象。而对于reduce()及findFirst()这样的方法,它们是终结操作,在调用它们时才会执行真正的操作来获取需要的值。
从一个例子出发:#
比如,当我们需要打印出第一个长度为3的大写名字时:public class LazyStreams {
private static int length(final String name) {
System.out.println("getting length for " + name);
return name.length();
}
private static String toUpper(final String name ) {
System.out.println("converting to uppercase: " + name);
return name.toUpperCase();
}
public static void main(final String[] args) {
List names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", "Mike", "Susan", "George", "Robert", "Julia", "Parker", "Benson");
final String firstNameWith3Letters = names.stream()
.filter(name -> length(name) == 3)
.map(name -> toUpper(name))
.findFirst()
.get();
System.out.println(firstNameWith3Letters);
}
}
你可能认为以上的代码会对names集合进行很多操作,比如首先遍历一次集合得到长度为3的所有名字,再遍历一次filter得到的集合,将名字转换为大写。最后再从大写名字的集合中找到第一个并返回。这也是经典情况下Java Eager处理的角度。如果以Eager的视角来阅读上述代码,它也许会执行15步操作:
可是实际情况并不是这样,不要忘了Stream可是非常“懒”的,它不会执行任何多余的操作。实际上,只有当findFirst方法被调用时,filter和map方法才会被真正触发。而filter也不会一口气对整个集合实现过滤,它会一个个的过滤,如果发现了符合条件的元素,会将该元素置入到下一个中间操作,也就是map方法中。所以实际的情况是这样的:
对于Stream操作,更好的代码阅读顺序是从右到左,或者从下到上。Stream每一个操作都只会做到恰到好处。
控制台的输出是这样的:getting length for Brad
getting length for Kate
getting length for Kim
converting to uppercase: Kim
KIM
为了更好理解上述过程,我们将Lambda表达式换为经典的Java写法,即匿名内部类的形式:final String firstNameWith3Letters = names.stream()
.filter(new Predicate{
public boolean test(String name){
return length(name)==3;
}
})
.map(new Function{
public String apply(String name){
return toUpper(name);
}
})
.findFirst()
.get();
关于 Predicate的理解
执行的见下图:
很容易得出之前的结论:只有当findFirst方法被调用时,filter和map方法才会被真正触发。而filter也不会一口气对整个集合实现过滤,它会一个个的过滤,如果发现了符合条件的元素,会将该元素置入到下一个中间操作,也就是map方法中。
当终结操作获得了它需要的答案时,整个计算过程就结束了。如果没有获得到答案,那么它会要求中间操作对更多的集合元素进行计算,直到找到答案或者整个集合被处理完毕。
JDK会将所有的中间操作合并成一个,这个过程被称为熔断操作(Fusing Operation)。因此,在最坏的情况下(即集合中没有符合要求的元素),集合也只会被遍历一次,而不会像我们想象的那样执行了多次遍历,也许这就回答了官方文档中为什么说"Processing streams lazily allows for significant efficiencies"了。
为了看清楚在底层发生的事情,我们可以将以上对Stream的操作按照类型进行分割:Stream namesWith3Letters = names.stream()
.filter(name -> length(name) == 3)
.map(name -> toUpper(name));
System.out.println("Stream created, filtered, mapped...");
System.out.println("ready to call findFirst...");
final String firstNameWith3Letters = namesWith3Letters.findFirst().get();
System.out.println(firstNameWith3Letters);
输出结果Stream created, filtered, mapped...
ready to call findFirst...
getting length for Brad
getting length for Kate
getting length for Kim
converting to uppercase: Kim
KIM
根据输出的结果,我们可以发现在声明了Stream对象上的中间操作之后,中间操作并没有被执行。只有当真正发生了findFirst()调用之后,才会执行中间操作。
关于 Predicate 在编程中的理解:
在中文里有人将其翻译为 “谓词”,不算很好的翻译。
Predicate功能判断输入的对象是否符合某个条件。官方文档解释到:Determines if the input object matches some criteria.
Java 8 中有Predicate接口,可以直接使用:
使用接口方法test,可以使用匿名内部类提供test()方法的实现,也可以使用lambda表达式实现test()。
体验一下Predicate的函数式编程,使用lambda实现。其测试代码如下:@Test
public void testPredicate(){
java.util.function.Predicate boolValue = x -> x > 5;
System.out.println(boolValue.test(1));//false
System.out.println(boolValue.test(6));//true
}