如果说前面几章是函数式编程的方法论,那么 Stream 流就应该是 JAVA8 为我们提供的最佳实践。

Stream 流的定义

Stream 是支持串行和并行操作的一系列元素。流操作会被组合到流管道中(Pipeline)中,一个流管道必须包含一个源(Source),这个源可以是一个数组(Array),集合(Collection)或者 I/O Channel,会有一个或者多个中间操作,中间操作的意思就是流与流的操作,流还会包含一个中止操作,这个中止操作会生成一个结果。

Stream 流的作用

以函数式编程的方式更好的操作集合。完全依赖于函数式接口。在 java.util.stream 包中。

流的创建方式

  • 使用数组的方式
//第一种方式,使用 Stream.of 方法
Stream stream1 = Stream.of("hello","world","hello world");

String[] myArray = new String[]{"hello","world","hello world"};
Stream stream2 = Stream.of(myArray);

//第二种方式,使用 Arrays.stream()
Stream stream3 = Arrays.stream(myArray);
  • 使用集合的方式
    Stream 的作用是以函数式编程的方式操作集合,所以对于集合类,一定有更好更方便的方法去创建 Stream 流。
List<String> list = Arrays.asList(myArray);
Stream stream4 = list.stream();

对于集合类,直接调用 stream 方法就可以获得这个集合对应的 Stream 流。通过查看源码我们发现这个方法直接定义在 Collection 接口中,并且是一个默认方法。所以所有 Collection 的子类都可以直接调用这个方法。这也是最为常用的方法。

default Stream<E> stream() {
  return StreamSupport.stream(spliterator(), false);
}
  • 使用文件流(基本不会使用,简单了解即可)
    下面是一个直接读取文件中的内容并且转化为 Stream 流,最后输出的过程。
//文件流
private static Stream<String> fileStream(){
  Path path = Paths.get("C:\\Users\\abs\\a.txt");
  try(Stream<String> lines = Files.lines(path)){
    lines.forEach(System.out::println);
    return lines;
  }catch(IOException e){
    throw new RuntimeException(e);
  }
}
  • 其他方式
    最后的方式是用于创建无限流,无限流的意思是如果你不加任何限制,流中的数据是无限。用于创建无限流的方法有 iterator 和 generate。
    generate 方法需要传入一个 Supplier 类型的函数式接口,这个函数式接口用于产生无限流中所需要的数据。
//全是数字 1 的无限流
Stream.generate(()->1);
//随机数字的无限流
Stream.generate(Math::random);

iterator 方法需要传入两个参数,第一个给定一个初始值,第二个参数是一个函数式接口 UnaryOperator,这个函数式接口就是输入和输出相同的 Function 接口。

Stream.iterate(0,n->n+1).limit(10).forEach(System.out::println);

输出结果:

0
1
2
3
4
5
6
7
8
9

上面的 limit 是避免无限流一直产生,到达指定个数就停止。

Steam 流的优势

下面我们通过一个简单的例子来了解一下使用 Stream 流到底有哪些好处。等我们学完 Stream API 后会给大家提供更多的例子,让大家真正了解它。

比如我们给定一个 List 集合,里面放了很多数字,我们想要得到数字的平方然后求和。

以前的写法:

List<Integer> l = Arrays.asList(1,2,3,4,5,6,7);
int res = 0;
for(int i=0;i<l.size();i++){
  res += i*i;
}

使用 Stream 后的写法只需要一行代码:

int r = l.stream().map(i->i+i).reduce(0,Integer::sum);

大家现在可能不明白 map 或者 reduce 的作用,我们稍后会详细讲解这一部分,这里只是想让大家看看区别,以及认识到 Stream 对于函数式编程的使用和好处。

Stream 流的特性和原理

流不存储值,通过管道的方式获取值。对流的操作会生成一个结果,不过并不会修改底层的数据源。集合可以作为流的底层数据源,也可以通过 generate/iterator 方法来生成数据源。

Stream java 对比 java stream.of_JAVA8

得到流之后,我们可以对流中的数据进行很多操作,比如过滤,映射,排序等等,处理完之后的数据可以再次被收集起来转化为我们需要的数据类型。

Stream java 对比 java stream.of_List_02

从上面的图我们可以看出,一个完成的流操作过程是包含两种类型的,一个是中间操作,一个是终止操作。中间操作指的是过滤,排序和映射等中间处理过程的方法,终止操作指的是我们将流处理完毕后返回结果的操作,比如 collect,reduce 和 count 等等。

中间操作:一个流后面可以跟随零个或者多个中间操作。其目的只要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用,这些操作都是延迟的,就是说仅仅调用到这些类的方法,并没有真正开始流的遍历。

终止操作:一个流只能有一个终止操作,当这个操作执行后,流就被使用光了,无法再被操作。所以这必定是流的最后一个操作。终止操作的执行,才会是真正开始流的遍历,并且会生成一个结果。

还有一个重要的概念流是惰性的在数据源上的计算只有数据在被终止的时候才会被执行。也就是说所有的中间操作都是惰性求值,不遇到终止操作,中间操作的代码是不会执行的。

举个例子:

我们对于一个数字结合进行一个 map 中间操作,将元素乘以 2,同时我们有一个 System.out 语句用于查看代码是否执行了。

List<Integer> l = Arrays.asList(1,2,3,4,5,6,7);        
l.stream().map(i->{
  i = i*2;
  System.out.println(i);
  return i;
});

最终的执行结果是什么也没打印。

那如果我们给他加一个终止操作那,结果如下:

中间操作:2
终止操作:2
中间操作:4
终止操作:4
中间操作:6
终止操作:6
中间操作:8
终止操作:8
中间操作:10
终止操作:10
中间操作:12
终止操作:12
中间操作:14
终止操作:14

这时中间操作和终止操作都执行了,这证明中间操作是惰性的。

还有一个需要注意的点就是,Stream 其实与 IO Stream 的概念是一致的,是不能重复使用的,关闭(执行终止操作后就关闭了)后也是不能使用的。

//用集合生成一个流并进行过滤,过滤后返回一个 stream s1。
List<Integer> l = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
Stream s1 = l.stream().filter(item -> item > 2);

//因为 filter 是中间操作,流并没有被关闭,所以还可以执行其他操作,distnict 是一个终止操作,执行完毕后流就关闭了
s1.distinct();

//流已经关闭了,再执行操作就会抛出异常 
s1.forEach(System.out::println);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
	at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
	at com.paul.framework.chapter7.StreamCreate.main(StreamCreate.java:48)

Stream 流的 API 采用了建造者设计模式,这就意味着我们可以在一句代码中连续调用 Stream 的 API。

中间操作 API

  • filter
    顾名思义,filter 就是过滤的意思。参数需要我们传入一个 Predicate 类型的函数式接口。不符合 Predicate 函数式接口的条件的流将被过滤出去。
Stream<T> filter(Predicate<? super T> predicate);

我们需要筛选出分数大于 60 分的学生:

public static void main(String[] args) {
  List<Student> lists = new ArrayList<>();
  lists.add(new Student("wang",80,"Female"));
  lists.add(new Student("li",95,"Male"));
  lists.add(new Student("zhao",100,"FeMale"));
  lists.add(new Student("qian",54,"Male"));

	// filter 是一个中间操作,返回过滤后的 Stream 流。forEach 是一个终止操作,对 filter 过滤之后的流进行处理。
  lists.stream().filter(s->s.getMark()>60).forEach((s)-> System.out.println(s.getName()));
}

//上一个例子我们对过滤后的流进行了打印操作,我们其实也可以把过滤后的流整理成一个集合
List<Student> l = lists.stream().filter(s->s.getMark()>60).collect(Collectors.toList());
l.forEach(s-> System.out.println(s.getName()));

两次打印的结果是相同的:

//第一次打印的结果
wang
li
zhao

//第二次打印的结果
wang
li
zhao

filter 函数为我们提供了最为简单的方法去过滤集合,避免了重复代码,逻辑也更易懂。

  • map
    通过流的方式对集合中的元素进行匹配操作。参数需要我们传入一个 Function 类型的函数式接口。Function 类型的函数式接口需要一个输入和一个输出,对应映射之前需要的元素和映射之后得到的元素。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

比如集合中的元素是学生类,我们最终想要得到的结果是学生的分数,就可以使用 map 方法。

List<Student> lists = new ArrayList<>();
lists.add(new Student("wang",80,"Female"));
lists.add(new Student("li",95,"Male"));
lists.add(new Student("zhao",100,"FeMale"));
lists.add(new Student("qian",54,"Male"));
lists.stream().map(s->s.getMark()).collect(Collectors.toList()).forEach(System.out::println);

将 list 转换为 stream 后,通过 map 方法将学生类转换成学生成绩的 int 类型,然后通过 collect 方法将流转换为集合,最后通过 forEach 方法将学生成绩打印出来。

打印的结果:

80
95
100
54

比如我们想将集合中的字符串转换成大写字母。

List<String> list = Arrays.asList("hello","world","helloworld","test");
list.stream().map(String::toUpperCase).collect(Collectors.toList()).forEach(System.out::println);

map 方法里我们通过方法引用(将字符串转为大写的方法 String 类已经定义好了,所以我们直接使用方法引用,而不是写一个匿名函数的 Lambda 表达式)将集合中的字符串转换成大写,然后通过 collect 将流转换为集合,最终使用 forEach 方法打印转换后的字符串。

打印结果:

HELLO
WORLD
HELLOWORLD
TEST
  • mapTo*
    如果我们的 map 方法返回值是 int,long 或者 double 的话,我们可以直接使用 Stream API 为我们提供了 mapToInt,mapToLong,mapToDouble 方法。这几个方法返回的是 IntStream,LongStream 和 DoubleStream。
    mapToInt, mapToLong 和 mapToDouble 是为了避免自动拆装箱带来的性能损耗。大家应该知道,像 int,long,double 这种基本数据类型是不能使用面向对象相关操作的,为此 Java 引入了自动拆装箱的功能,能够在需要使用面向对象的特性时帮我们将基本数据类型 int,long 和 double 转换为 Integer,Long 和 Double 等包装类型。在需要使用基本数据类型时(比如计算),又可以将包装类型 Integer,Long 和 Double 转换为基本数据类型 int,long 和 double。
    如果我们使用的不对,就会有一些自动拆装箱的性能损耗。
    如果我们需要得到基本数据类型的结果,就可以使用 mapToInt, mapToLong 和 mapToDouble,这样的到的是基本数据类型的流,可以方便我们进行计算等等操作。
int sum = lists.stream().mapToInt(s->s.getMark()).sum();
System.out.println(sum);
  • flatMap
    flat 的意思是扁平化,这个函数式的作用是将我们 map 之后的集合或者数组等等元素打散成我们想要的元素。
    flatMap 方法需要返回一个 Stream 数据类型。T 是输入的集合类型元素,R 是打散之后的元素类型,是 Stream 类型。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

来看一个例子:

比如我们的流中以前有三个 ArrayList,map 之后依然后会有三个 ArrayList,flatMap 会将三个 ArrayList 合并到一个 ArrayList 中。

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1),ArrayList.asList(2,3), ArrayList.asList(4,5,6));

// 将 stream 里面的每一个 list 再次转化为 stream<Integer>,然后在进行 map 操作。
stream.flatMap(theList->theList.stream()).map(item->item*item).forEach(System.out::println);

这个例子中,List 代表打散之前的元素,Integer 代表我们打散之后的元素类型。

Stream java 对比 java stream.of_System_03

在看另外一个例子,字符串去重复。

List<String> list = Arrays.asList("hello welcome","world hello","hello world hello","hello welcome");

//错误的写法, 这是对 String[] 的 distinct
List<String[]> result = list.stream().map(item->item.split(" ")).distinct().collect(Collectors.toList());

split 方法输入的是字符串,返回的是一个字符串数组,所以最后返回的是 String 数组流 Stream<String[]>。

我们使用 flatMap 将 String 数组打散成 String。

//正确的写法,要用 flatmap 将 String[] 打散成 String
List<String> result = list.stream().map(item->item.split(" ")).flatMap(Arrays::stream).distinct().collect(Collectors.toList());

Stream java 对比 java stream.of_Stream java 对比_04

  • flatMapTo*
    flatMapTo* 也有许多具体的实现实现,和 mapTo* 用法类似,这里就不再赘述了。
  • limit
    limit 方法可以对流中需要返回的元素加以限制,因为流中元素的方法执行是严格按照顺序进行的,limit 方法就相当于取前几个元素。
    我们通过下面这个例子来了解 limit 和无限流。
IntStream.iterate(0, i->(i+1)%2).distinct().limit(6).forEach(System.out::println);

IntStream.iterate(0, i->(i+1)%2) 不断产生 0,1,0,1,0,1..... 这样的无限流,distinct 方法去除重复,limit 方法虽然限制流中只有 6 个元素,但是 distinct 方法先执行它会对无限流一致执行去复操作,所以方法永远不会结束。这个 limit 在这里也失去了作用。

Stream java 对比 java stream.of_List_05

执行结果虽然只显示了 0,1。但是方法一直不会结束。

正确的写法:

IntStream.iterate(0, i->(i+1)%2).limit(6).distinct().forEach(System.out::println);

先调用 limit 方法,限制流中只有 6 个元素,然后去重,结果打印 0,1。程序结束。

Stream java 对比 java stream.of_System_06

  • skip
    skip 方法和 limit 方法的用法类似,可以跳过流中的前几个元素。
IntStream.iterate(0, i->i+1).limit(10).skip(3).forEach(System.out::println);

首先通过 iterate 和 limit 产生 10 Integer 个元素的流,通过 skip 跳过前三个。最终的结果如下:

3
4
5
6
7
8
9
  • sort
    sort 方法有两个实现,一个是不需要传入参数的,另一个是需要我们传入 Comparator。
//根据自然顺序排序
Stream<T> sorted();
//根据 Comparator 的规则进行排序
Stream<T> sorted(Comparator<? super T> comparator);

我们以前对集合排序时通常会使用 JDK 中 Collection 接口的 sort 方法:

List<String> names = Arrays.asList("java8","lambda","method","class");

//以前的写法
Collections.sort(names, new Comparator<String>() {
  @Override
  public int compare(String o1, String o2) {
    return o2.compareTo(o1);
  }
});

使用 Lambda 表达式对上面的写法进行一下改进:

Collections.sort(names,(o1,o2)-> o2.compareTo(o1));

现在我们还可以使用 Stream API 中的 sorted 方法:

names.stream().sorted().forEach(System.out::println);
names.stream().sorted((o1,o2)-> o2.compareTo(o1)).forEach(System.out::println);

结果:

class
java8
lambda
method