什么是Stream流
Stream
被翻译为流,它的工作过程像将一瓶水导入有很多过滤阀的管道一样,水每经过一个过滤阀,便被操作一次,比如过滤,转换等,最后管道的另外一头有一个容器负责接收剩下的水。
Stream
作为Java 8的一大亮点,它专门针对集合的各种操作提供各种非常便利,简单,高效的API,Stream API
主要是通过Lambda
表达式完成,极大的提高了程序的效率和可读性,同时Stram API
中自带的并行流使得并发处理集合的门槛再次降低,使用Stream API
编程无需多写一行多线程的大门就可以非常方便的写出高性能的并发程序。使用Stream API
能够使你的代码更加优雅。流的另一特点是可无限性,使用
Stream
,你的数据源可以是无限大的。
如何使用流
- 获取流
- 对流操作
- 结束对流操作
- 获取流
获取流的方式有多种,对于常见的容器(
Collection
)可以直接.stream()
获取 例如:
Collection.stream()
Collection.parallelStream()
Arrays.stream(T array) or Stream.of()
对于
IO
,我们也可以通过lines()
方法获取流:
java.nio.file.Files.walk()
java.io.BufferedReader.lines()
最后,我们还可以从无限大的数据源中产生流:
Random.ints()
值得注意的是,
JDK
中针对基本数据类型的昂贵的装箱和拆箱操作,提供了基本数据类型的流:
IntStream
LongStream
DoubleStream
这三种基本数据类型和普通流差不多,不过他们流里面的数据都是指定的基本数据类型。
1
2 Intstream.of(new int[]{1,2,3});
Intstream.range(1,3);
1 | 这边有个parallelStream和stream的区别 |
获取流的方式有多种,对于常见的容器(
Collection
)可以直接.stream()
获取 例如:
Collection.stream()
Collection.parallelStream()
Arrays.stream(T array) or Stream.of()
对于
IO
,我们也可以通过lines()
方法获取流:
java.nio.file.Files.walk()
java.io.BufferedReader.lines()
最后,我们还可以从无限大的数据源中产生流:
Random.ints()
值得注意的是,
JDK
中针对基本数据类型的昂贵的装箱和拆箱操作,提供了基本数据类型的流:
IntStream
LongStream
DoubleStream
这三种基本数据类型和普通流差不多,不过他们流里面的数据都是指定的基本数据类型。
1
2 Intstream.of(new int[]{1,2,3});
Intstream.rang(1,3);
1 | 这边有个parallelStream和stream的区别 |
- 对流操作
对于中间操作,所有的
API
的返回值基本都是Stream<T>
,因此以后看见一个陌生的API
也能通过返回值判断它的所属类型。map/flatMap
map
顾名思义,就是映射,map
操作能够将流中的每一个元素映射为另外的元素。
1 <R> Stream<R> map(Function<? super T, ? extends R> mapper);可以看到
map
接受的是一个Function
,也就是接收参数,并返回一个值。比如:
1
2
3 //提取 List<Student> 所有student 的名字
List<String> studentNames = students.stream().map(Student::getName)
.collect(Collectors.toList());上面的代码等同于以前的:
1
2
3
4 List<String> studentNames=new ArrayList<>();
for(Student student:students){
studentNames.add(student.getName());
}
再比如:将List中所有字母转换为大写:
1
2
3 List<String> words=Arrays.asList("a","b","c");
List<String> upperWords=words.stream().map(String::toUpperCase)
.collect(Collectors.toList());
flatMap
顾名思义就是扁平化映射,它具体的操作是将多个stream
连接成一个stream
,这个操作是针对类似多维数组的,比如容器里面包含容器等。
1
2
3
4 List<List<Integer>> ints=new ArrayList<>(Arrays.asList(Arrays.asList(1,2),
Arrays.asList(3,4,5)));
List<Integer> flatInts=ints.stream().flatMap(Collection::stream).
collect(Collectors.toList());可以看到,相当于降维。
filter
1
2 filter`顾名思义,就是过滤,通过测试的元素会被留下来并生成一个新的`Stream
Stream<T> filter(Predicate<? super T> predicate);同理,我们可以
filter
接收的参数是Predicate
,也就是推断型函数式接口,接收参数,并返回boolean
值。比如:
1
2
3 //获取所有大于18岁的学生
List<Student> studentNames = students.stream().filter(s->s.getAge()>18)
.collect(Collectors.toList());
distinct
distinct
是去重操作,它没有参数
1 Stream<T> distinct();
sorted
1
2
3
4 sorted`排序操作,默认是从小到大排列,sorted方法包含一个重载,使用sorted方法,如果没有传递参数,那么流中的元素就需要实现Comparable<T>方法,也可以在使sorted方法的时候传入一个`Comparator<T>
Stream<T> sorted(Comparator<? super T> comparator);
Stream<T> sorted();值得一说的是这个
Comparator
在Java 8
之后被打上了@FunctionalInterface
,其他方法都提供了default
实现,因此我们可以在sort
中使用Lambda
表达式例如:
1
2
3 //以年龄排序
students.stream().sorted((s,o)->Integer.compare(s.getAge(),o.getAge()))
.forEach(System.out::println);;然而还有更方便的,
Comparator
默认也提供了实现好的方法引用,使得我们更加方便的使用:例如上面的代码可以改成如下:
1
2
3 //以年龄排序
students.stream().sorted(Comparator.comparingInt(Student::getAge))
.forEach(System.out::println);;或者:
1
2
3 //以姓名排序
students.stream().sorted(Comparator.comparing(Student::getName)).
forEach(System.out::println);是不是更加简洁。
peek
peek
有遍历的意思,和forEach
一样,但是它是一个中间操作。
peek
接受一个消费型的函数式接口。
1 Stream<T> peek(Consumer<? super T> action);例如:
1
2
3 //去重以后打印出来,然后再归并为List
List<Student> sortedStudents= students.stream().distinct().peek(System.out::println).
collect(Collectors.toList());
limit
limit
裁剪操作,和String::subString(0,x)
有点先沟通,limit
接受一个long
类型参数,通过limit
之后的元素只会剩下min(n,size)
个元素,n
表示参数,size
表示流中元素个数
1 Stream<T> limit(long maxSize);例如:
1
2 //只留下前6个元素并打印
students.stream().limit(6).forEach(System.out::println);
skip
skip
表示跳过多少个元素,和limit
比较像,不过limit
是保留前面的元素,skip
是保留后面的元素
1 Stream<T> skip(long n);例如:
1
2 //跳过前3个元素并打印
students.stream().skip(3).forEach(System.out::println);
- 终结操作
一个流处理中,有且只能有一个终结操作,通过终结操作之后,流才真正被处理,终结操作一般都返回其他的类型而不再是一个流,一般来说,终结操作都是将其转换为一个容器。
forEach
forEach
是终结操作的遍历,操作和peek
一样,但是forEach
之后就不会再返回流
1 void forEach(Consumer<? super T> action);例如:
1
2 //遍历打印
students.stream().forEach(System.out::println);上面的代码和一下代码效果相同:
1
2
3 for(Student student:students){
System.out.println(sudents);
}
toArray
toArray
和List##toArray()
用法差不多,包含一个重载。默认的
toArray()
返回一个Object[]
,也可以传入一个
IntFunction<A[]> generator
指定数据类型一般建议第二种方式。
1
2
3 Object[] toArray();
<A> A[] toArray(IntFunction<A[]> generator);例如:
1 Student[] studentArray = students.stream().skip(3).toArray(Student[]::new);
max/min
max/min
即使找出最大或者最小的元素。max/min
必须传入一个Comparator
。
1
2
3 Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);
count
count
返回流中的元素数量
1 long count();例如:
1 long count = students.stream().skip(3).count();
reduce
1 reduce为归纳操作,主要是将流中各个元素结合起来,它需要提供一个起始值,然后按一定规则进行运算,比如相加等,它接收一个二元操作 `BinaryOperator`函数式接口。从某种意义上来说,`sum,min,max,average`都是特殊的reduce
reduce
包含三个重载:
1
2
3
4
5
6
7 T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);例如:
1
2
3 List<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
long count = integers.stream().reduce(0,(x,y)->x+y);以上代码等同于:
1 long count = integers.stream().reduce(Integer::sum).get();
reduce
两个参数和一个参数的区别在于有没有提供一个起始值,如果提供了起始值,则可以返回一个确定的值,如果没有提供起始值,则返回
Opeational
防止流中没有足够的元素。
anyMatch\ allMatch\ noneMatch
测试是否有任意元素\所有元素\没有元素匹配表达式
他们都接收一个推断类型的函数式接口:
Predicate
1
2
3
4
5 boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate)例如:
1 boolean test = integers.stream().anyMatch(x->x>3);
findFirst、 findAny
获取元素,这两个
API
都不接受任何参数,findFirt
返回流中第一个元素,findAny
返回流中任意一个元素。
1
2
3 Optional<T> findFirst();
Optional<T> findAny();也有有人会问
findAny()
这么奇怪的操作谁会用?这个API
主要是为了在并行条件下想要获取任意元素,以最大性能获取任意元素例如:
1 int foo = integers.stream().findAny().get();
collect
collect
收集操作,这个API
放在后面将是因为它太重要了,基本上所有的流操作最后都会使用它。我们先看
collect
的定义:
1
2
3
4 <R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);可以看到,
collect
包含两个重载:一个参数和三个参数,
三个参数我们很少使用,因为
JDK
提供了足够我们使用的Collector
供我们直接使用,我们可以简单了解下这三个参数什么意思:
Supplier
:用于产生最后存放元素的容器的生产者accumulator
:将元素添加到容器中的方法combiner
:将分段元素全部添加到容器中的方法前两个元素我们都很好理解,第三个元素是干嘛的呢?因为流提供了并行操作,因此有可能一个流被多个线程分别添加,然后再将各个子列表依次添加到最终的容器中。
↓ - - - - - - - - -
↓ — — —
↓ ———
如上图,分而治之。
例如:
1 List<String> result = stream.collect(ArrayList::new, List::add, List::addAll);
接下来看只有一个参数的
collect
一般来说,只有一个参数的
collect
,我们都直接传入Collectors
中的方法引用即可:
1 List<Integer> = integers.stream().collect(Collectors.toList());
Collectors
中包含很多常用的转换器。toList()
,toSet()
等。
1 Collectors`中还包括一个`groupBy()`,他和`Sql`中的`groupBy`一样都是分组,返回一个`Map例如:
1
2
3 //按学生年龄分组
Map<Integer,List<Student>> map= students.stream().
collect(Collectors.groupingBy(Student::getAge));
groupingBy
可以接受3个参数,分别是
- 第一个参数:分组按照什么分类
- 第二个参数:分组最后用什么容器保存返回(当只有两个参数是,此参数默认为
HashMap
)- 第三个参数:按照第一个参数分类后,对应的分类的结果如何收集
有时候单参数的
groupingBy
不满足我们需求的时候,我们可以使用多个参数的groupingBy
例如:
1
2
3 //将学生以年龄分组,每组中只存学生的名字而不是对象
Map<Integer,List<String>> map = students.stream().
collect(Collectors.groupingBy(Student::getAge,Collectors.mapping(Student::getName,Collectors.toList())));
toList
默认生成的是ArrayList
,toSet
默认生成的是HashSet
,如果想要指定其他容器,可以如下操作:
1
2
3
4 students.stream().collect(Collectors.toCollection(TreeSet::new));
Collectors`还包含一个`toMap`,利用这个`API`我们可以将`List`转换为`Map
Map<Integer,Student> map=students.stream().
collect(Collectors.toMap(Student::getAge,s->s));值得注意的一点是,
IntStream
,LongStream
,DoubleStream
是没有collect()
方法的,因为对于基本数据类型,要进行装箱,拆箱操作,SDK并没有将它放入流中,对于基本数据类型流,我们只能将其toArray()