stream流
1. 简述
流是有关算法和计算的,允许你以声明性方式处理数据集合,可以看成遍历数据集的的高级迭代器。
此外,和迭代器不同,流还可以并行处理。数据可以分成多段,其中每一个都在不同的线程中执行,最后将结果一起输出。
当我们使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。
图 1. 流管道 (Stream Pipeline) 的构成流有三大特点:
- 流不储存元素;
- 流不会修改其数据源;
- 流执行具有延迟特性;
2. 流的创建
2.1 从数组或集合
- Arrays.stream(T array) or Stream.of()
- Collection.stream()
- Collection.parallelStream()
@Test
public void testArrayStream() {
//1.通过Arrays.stream
//1.1基本类型
Integer[] arr = new Integer[]{1,2,3,4,5,6,7};
//通过Arrays.stream
Stream stream1 = Arrays.stream(arr);
stream1.forEach(System.out::print);
//2.通过Stream.of
Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6,7);
stream2.forEach(System.out::print);
}
@Test
public void testCollectionStream(){
List<Integer> strs = Arrays.asList(1,2,3,4,5,6,7);
//创建普通流
Stream<Integer> stream = strs.stream();
//创建并行流
Stream<Integer> stream1 = strs.parallelStream();
}
2.2 创建无限流
@Test
public void testUnlimitStream() {
Random seed = new Random();
Supplier<Integer> random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.out::println);
//Another way
IntStream.generate(() -> (int) (System.nanoTime() % 100)).limit(10).forEach(System.out::println);
}
2.3 创建规律流
@Test
public void testUnlimitStream1(){
Stream.iterate(0,x -> x+1).limit(10).forEach(System.out::println);
Stream.iterate(0,x -> x).limit(10).forEach(System.out::println);
}
3. 流的操作
流操作分为中间操作和终端操作,中间操作会返回另外一个流,可以继续执行下一个中间操作。
终端操作是返回结果,终端操作触发流执行中间操作,终端操作,到此流的生命结束。
3.1 中间操作
filter
filter对原始Stream进行过滤,符合条件的原数被留下来生成新的流。
@Test
public void testFilter() {
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens = Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);
}
sorted
sorted对原始Stream中的元素进行排序,排序后生成新的流。
Integer[] sixNums = {1, 2, 4, 3, 5, 6};
Integer[] orderedNums = Stream.of(sixNums).sorted(Comparator.comparing(x -> x)).toArray(Integer[]::new);
distinct
去掉重复值
@Test
public void testDistinct() {
Integer[] sixNums = {1, 2, 4, 3, 5, 5, 6};
Integer[] distinctedNums = Stream.of(sixNums).distinct().toArray(Integer[]::new);
System.out.println(distinctedNums);
}
map
map将现有流转换为新的流。map接收一个函数作为参数,该函数作用于流中的每一个元素,并将其映射成一个新的元素。
@Test
public void testMap() {
String[] arr = new String[]{"yes", "YES", "no", "NO"};
Arrays.stream(arr).map(x -> x.toLowerCase()).forEach(System.out::println);
}
flatMap
将流扁平化,流中的每一个元素都被拆解成一个新的流。使用flatMap需要提前知道原来的流中的元素类型。
@Test
public void testFlapMap1() {
String[] words = {"Hello", "World"};
Stream.of(words)
.map(word -> word.split(""))
.flatMap(Arrays::stream).forEach(System.out::println);
}
limit
截断流,该方法返回一个不超过给定长度的流。
3.2 终端操作
遍历流中的每一个元素。当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为。
@Test
public void testFlapMap1() {
String[] words = {"Hello", "World"};
Stream.of(words)
.map(word -> word.split(""))
.flatMap(Arrays::stream).forEach(System.out::println);
}
findFirst, findAny
两者功能类似,findAny不要求顺序,使用并行流有优势。
@Test
public void testFindFirser() {
String[] arr = new String[]{"yes", "YES", "no", "NO"};
Arrays.stream(arr).filter(str -> Objects.equals(str, "yes")).findFirst().ifPresent(System.out::println);
}
allMatch, noneMatch, anyMatch
Stream提供了三个match方法,allMatch要求流中所有元素满足条件才返回true, anyMatch中只要有一个元素符合就返回true, noneMatch与allMatch相反,所有元素都不符合,返回true
@Test
public void testMatch() {
String[] arr = new String[]{"yes", "YES", "no", "NO"};
System.out.println(Arrays.stream(arr).noneMatch( str -> str.length() > 2));
System.out.println(Arrays.stream(arr).anyMatch( str -> str.length() > 2));
System.out.println(Arrays.stream(arr).allMatch( str -> str.length() > 2));
}
reduce
这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于
Integer sum = integers.reduce(0, (a, b) -> a+b);
或
Integer sum = integers.reduce(0, Integer::sum);
@Test
public void testReduce() {
// 字符串连接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F").filter(x -> x.compareTo("Z") > 0).reduce("", String::concat);
}
count
计算流中元素数量。
collect
这个在实际项目中使用非常多,通过collect生成结果。可以生成各种形式,Array, List, Set, Map。另外,还能进行分组。
public class Student {
private String name;
private Integer score;
//-----getters and setters-----
Student(String name, Integer score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public Integer getScore() {
return score;
}
public String toString() {
return "name: " + name
+ " score: " + score;
}
}
Student[] students;
@Before
public void init(){
students = new Student[10];
for (int i = 0; i <= 3; i++){
Student student = new Student("user", i);
students[i] = student;
}
for (int i = 3; i <= 6; i++){
Student student = new Student("user" + i, i + 1);
students[i] = student;
}
for (int i = 6; i < 10; i++){
Student student = new Student("user" + i, i + 2);
students[i] = student;
}
}
@Test
public void testCollect1(){
/**
* 生成List
*/
List<Student> list = Arrays.stream(students).collect(Collectors.toList());
list.forEach((x) -> System.out.println(x));
/**
* 生成Set
*/
Set<Student> set = Arrays.stream(students).collect(Collectors.toSet());
set.forEach((x)-> System.out.println(x));
/**
* 生成Map,如果包含相同的key,则需要提供第三个参数,否则报错, 这里用了覆盖旧value
*/
Map<String,Integer> map = Arrays.stream(students).collect(Collectors.toMap(Student::getName, Student::getScore, (v1, v2) -> v2));
map.forEach((x, y) -> System.out.println(x + "->" + y));
}
/**
* 生成数组
*/
@Test
public void testCollect2(){
Student[] s = Arrays.stream(students).toArray(Student[]::new);
for (int i=0; i< s.length; i++)
System.out.println(s[i]);
}
groupingBy
groupingBy能够将流按照某个条件进行进行分组和分片,条件为key,分组之后的元素组成新的流,对新的流可以进行中间操作和终端操作,默认生成List。最后生成Map的类型,key就是分组条件,value取决于对分组后的流进行操作后生成的类型。生成Map会有重复key的问题,使用groupingBy将重复key的value值放到一个List, 同样能够解决此问题。
如果想要按照多个条件进行分组,一种方法是groupingB嵌套使用groupBy,多次进行分组;另一种方法是按照多个分组条件进行拼接成一个key,按照这个拼接后的key进行分组。
@Test
public void testGroupBy1(){
Map<String,List<Student>> map = Arrays.stream(students).collect(Collectors.groupingBy(Student::getName));
map.forEach((x,y)-> System.out.println(x+"->"+y));
}
/**
* 如果只有两类,使用partitioningBy会比groupingBy更有效率
*/
@Test
public void testPartitioningBy(){
Map<Boolean,List<Student>> map = Arrays.stream(students).collect(Collectors.partitioningBy(x -> x.getScore() > 5));
map.forEach((x, y)-> System.out.println(x+ "->" + y));
}
/**
* downstream指定类型
*/
@Test
public void testGroupBy2(){
Map<String,Set<Student>> map = Arrays.stream(students).collect(Collectors.groupingBy(Student::getName, Collectors.toSet()));
map.forEach((x, y)-> System.out.println(x + "->"+ y));
}
/**
* downstream 聚合操作
*/
@Test
public void testGroupBy3(){
/**
* counting
*/
Map<String,Long> map1 = Arrays.stream(students).collect(Collectors.groupingBy(Student::getName, Collectors.counting()));
map1.forEach((x, y)-> System.out.println(x + "->" + y));
/**
* summingInt
*/
Map<String,Integer> map2 = Arrays.stream(students).collect(Collectors.groupingBy(Student::getName, Collectors.summingInt(Student::getScore)));
map2.forEach((x,y) -> System.out.println(x + "->" + y));
/**
* maxBy
*/
Map<String,Optional<Student>> map3 = Arrays.stream(students).collect(Collectors.groupingBy(Student::getName, Collectors.maxBy(Comparator.comparing(Student::getScore))));
map3.forEach((x, y)-> System.out.println(x + "->" + y));
/**
* mapping, 这种也用的比较多,可以用元素的变量作为value, 也可以创建新的对象作为value
*/
Map<String,Set<Integer>> map4 = Arrays.stream(students).collect(Collectors.groupingBy(Student::getName, Collectors.mapping(Student::getScore, Collectors.toSet())));
map4.forEach((x, y)-> System.out.println(x + "->" + y));
/**
* mapping, 使用元素多个变量进行分组
*/
Map<String, Map<Integer, List<Student>>> map5 = Arrays.stream(students).collect(Collectors.groupingBy(Student::getName, Collectors.groupingBy(Student::getScore)));
map5.forEach((x, y)-> System.out.println(x + "->" + y));
/**
* mapping, 使用元素多个变量进行分组, 第二种方法,将多个变量进行拼接, 个人推荐使用这种
*/
Map<String, List<Student>> map6 = Arrays.stream(students).collect(Collectors.groupingBy(student -> student.getName() + "_" + student.getScore()));
map6.forEach((x, y)-> System.out.println(x + "->" + y));
}
总结
总之,Stream 的特性可以归纳为:
- 不是数据结构
- 它也绝不修改自己所封装的底层数据结构的数据。
- 很容易生成数组或者List
- 惰性化
很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要。 - 并行能力
当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。 - 可以是无限的
集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。
参考
[1].https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/
[2]. https://www.cnblogs.com/andywithu/p/7404101.html