# Stream流
[TOC]
## 导学
Stream流的使用主要针对集合和数组等数据容器,Stream流的作用包括对数据容器进行过滤,映射,去重,排序等转换操作,以及将Stream流转为集合或统计等聚合操作。
所以Stream流的出现,就是为了弥补jdk中集合类库的缺陷。以下有些Stream流的使用注意点
1. Stream流不是一种数据结构,不保存数据,它只是在原数据集上定义了一组操作。
2. 这些操作是惰性的,即每当访问到流中的一个元素,才会在此元素上执行这一系列操作。
3. Stream不保存数据,故每个Stream流只能使用一次。
~~~
//stream针对于集合进行操作
List<String> list = new ArrayList<>();
list.add("red");
list.add("orange");
list.add("red");
list.add("blue");
list.add("white");
list.add("black");
list.add("gray");
list.add("red");
list.add("green");
Stream<String> sm1 = list.stream();//Stream<String>和List<String>只是长得像,但Stream不会作为一个数据容器。
Stream<String> sm2 = sm1.distinct();//由sm1转化为sm2流对象的时候,并不会对集合中的元素产生任何的操作
Stream<String> sm3 = sm2.map(str -> str += "123");
List<String> lst = sm3.collect(Collectors.toList());//只有在对sm3流对象进行聚合时。才会真正执行元素聚合操作
//List<String> lst = list.stream().distinct().map(str -> str += "123").collect(Collectors.toList());
System.out.println(list);
System.out.println(lst);
~~~
**关于Stream流的使用策略**
1. 首先需要构建Stream流
2. 每次操作Stream流,不会改变原有的Stream流对象,只会生成新的Stream流对象,而且可以把执行聚合任务之前的Stream流对象认为是用来记录转换操作的。
3. Stream流中很多方法都会返回新Stream流,所以Stream流编程大多是链式编程风格
4. 最后,在操作完Stream流后,基本会对Stream流进行聚合操作(执行真正的计算,并返回新集合等内容)
5. 在执行完聚合操作之后,Stream流就会被关闭,所以对同一个Stream流而言,转换操作可以执行多次,
但聚合操作只能执行一次。
## Stream流的使用
### Stream流对象的创建
Stream流对象的创建主要有两大类,第一种是基于Stream流本身的静态方法,第二种基于数组和集合的`stream()`方法。
1. Stream中的静态方法
1.1 `generate()`方法和`iterate()`方法
![](https://img.kancloud.cn/2e/47/2e472a18720a2da8815a72bfe5b119bb_1442x109.png)
这两个方法都需要自定义元素的生成方式,所以使用的还是比较少的。
~~~
public class Test {
public static void main(String[] args) {
Stream<Integer> sm = Stream.generate(new Add());
// 注意:无限序列必须先变成有限序列再打印:
sm.limit(20).forEach(System.out::println);
}
}
class Add implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
~~~
1.2 `of()`方法
![](https://img.kancloud.cn/53/db/53dbbaa84c7c5aa4e86b32fc063e77cb_1446x109.png)
在Stream中,存在两个重载的静态`of()`方法,这两个方法,一个接收任意类型的可变参数列表作为参数,另一个接收单个任意类型参数。
~~~
public class StreamTest {
public static void main(String[] args) {
Stream<String> s = Stream.of("1","2","3","4");
Stream<String> s1 = Stream.of("2");
}
}
~~~
2. 数组和集合的`stream()`方法
在日常的使用中,Stream主要用于集合和数组操作,所以这里的两个stream方法也是用的比较多的。
~~~
public class StreamTest {
public static void main(String[] args) {
String[] arr = new String[]{"a","b","c"};
Stream<String> s = Arrays.stream(arr);
List<String> list = new ArrayList<>();
Stream<String> s1 = list.stream();
}
}
~~~
### Stream流中的转换操作
Stream流中的转换操作,其实是用来记录Stream对元素的转换操作,本质上并不涉及对元素的直接实质化操作,仅仅是起一个记录的作用,与之相对的是聚合操作。转换操作通常会由一个Stream流对象,返回一个新的Stream流对象,以下是一些常用的Stream对象的常用方法。
* **`forEach()`方法**
`forEach()`方法比较特殊,该方法虽然是一个转换操作,但该方法没有返回值。
~~~
public class StreamTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//asList方法生成的集合不可改动,所以将该集合的元素添加到另一个集合中
list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"}));
list.stream().forEach(System.out::println);
}
}
~~~
* **`map()`方法**
`map()`:将对Stream序列中的每一个元素进行操作,参数为Function接口对象,要求传入的是对元素的执行内容,在map中,Function接口对象的apply方法的参数的类型是Stream流中,元素的类型,因为该方法接收的是Stream流中的元素。
~~~
public class StreamTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"}));
list.stream().map(str -> str += "颜色").forEach(System.out::println);
}
}
~~~
问:执行完map方法后,新生成的stream对象中,元素的个数有没有变化?元素的类型有没有变化?
答:**元素个数不变,因为map只操作元素,不删除元素。元素类型有可能变化。**
* **`flatMap()`方法**
`flatMap()`方法针对原有Stream流对象中的数据容器进行扁平化操作,将每个数据容器转换为小的Stream对象,最终将所有小的Stream流对象合并为新的大的Stream流对象。
~~~
public class StreamTest {
public static void main(String[] args) {
List<String[]> list = new ArrayList<>();
list.addAll(Arrays.asList(new String[][]{{"red","blue"},{"green","yellow","red","black"}}));
list.stream().flatMap(arr -> Arrays.stream(arr)).forEach(System.out::println);
}
}
~~~
* **`filter()`方法**
`filter()`方法将对Stream序列中的每一个元素进行过滤,参数为Predicate接口对象,要求传入的是对元素的过滤规则,在filter中,Predicate接口对象中存在test方法,该方法接收stream中的元素作为参数,返回布尔类型的值。如果test方法返回为true,则stream流中的元素得以保存,如果返回为false,则清除该元素。
~~~
public class StreamTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"}));
list.stream().filter(str -> !"red".equals(str)).forEach(System.out::println);
}
}
~~~
问:执行完filter方法后,新生成的stream对象中,元素的个数有没有变化?元素的类型有没有变化?
答:**元素个数可能变化,因为filter会执行清除元素操作。元素类型没有变化。**
* **`sorted()`方法**
`sorted()`方法对stream流中的元素进行排序,建议使用`Collections.sort()`
* **`distinct()`方法**
`distinct()`方法用于去除重复的元素,对于重复元素的判断,调用的是equals方法。该方法无需参数
* **`skip()`方法 & `limit()`方法**
`skip()`方法用于跳过当前Stream的前N个元素。
`limit()`方法用于截取当前Stream最多前N个元素,两个方法都是接收int类型参数。
~~~
public class StreamTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"}));
list.stream().skip(3).map(str -> str += 1).forEach(System.out::println);
System.out.println("====================");
list.stream().limit(3).map(str -> str += 1).forEach(System.out::println);
}
}
~~~
思考题:如何只针对Stream流中,除首尾之外的其他元素进行处理?
* **`concat()`方法**
`concat()`方法将两个泛型相同的stream流对象,合并为一个stream流对象
* **`allMatch()`方法 & `anyMatch()`方法**
这两个方法用于测试元素是否都满足条件/是否有一个满足条件,这两个方法的返回值都是Boolean类型。
~~~
public class StreamTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"}));
System.out.println(list.stream().anyMatch(str -> "red".equals(str)));
System.out.println(list.stream().allMatch(str -> "red".equals(str)));
}
}
~~~
### 聚合操作
Stream流中的聚合操作,实际上用来真正执行元素的计算任务的,或是可以用来对聚合完的元素进行整合输出。在Stream流中,有很多的聚合操作,在这里我们主要挑出常用的两大类聚合操作来说明:
* **`reduce()`方法**
`reduce()`方法可以用来对Stream中的元素进行聚合,我们可以用`reduce()`方法来进行元素的求和求积等操作,还可以进行一些转换元素类型的高级操作。`reduce()`方法本身具有三种重载方法:
![](https://img.kancloud.cn/56/59/5659f8e26f48d911ce2e822a171d05df_1439x159.png)
**以下,通过一个问题来学习`reduce()`方法,问,如何将Person集合中,各元素的人名取出来组成一个姓名集合?**
* 单参`reduce()`
单参`reduce()`方法,返回值为Optional类型,这是因为在不给定默认值的情况下,Stream本身并不会去确定`reduce()`方法的运行结果,需要调用者自己去确定。
~~~
public class StreamTest {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("张三",23));
list.add(new Person("李四",23));
list.add(new Person("王五",23));
//Optional类是一个容器类,它可以保存模板类型T的值,也可以保存null。
Optional<List<String>> lt = list.stream().map(person -> {
List<String> lst = new ArrayList<>();
lst.add(person.name);
return lst;
}).reduce((lst1, lst2) -> {
lst1.addAll(lst2);
return lst1;
});
if(lt.isPresent()) {
List<String> ls = lt.get();
System.out.println(ls);
}
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
~~~
在上述代码中,我们在`Stream`流中,先将三个`Person`类型的数据,转变为三个`List<String>`类型的数据,然后在将数据使用`reduce()`整合为一个`List`。但是此时并没有指定默认值,所以此时可能出现结果为null的情况,所以`reduce()`方法将返回值设置为可以存储null的`Optional`容器。
参考资料:**Java8新特性 Optional的正确使用姿势** https://www.jianshu.com/p/59db4b2c7543
* 双参`reduce()`
对于双参的方法,我们在使用前,给定了默认值,这个默认值需要和Stream中的元素是同一类型的
~~~
public class StreamTest {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("张三",23));
list.add(new Person("李四",23));
list.add(new Person("王五",23));
List<String> lt = list.stream().map(person -> {
List<String> lst = new ArrayList<>();
lst.add(person.name);
return lst;
}).reduce(new ArrayList<String>(), (lst1, lst2) -> {
lst1.addAll(lst2);
return lst1;
});
System.out.println(lt);
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
~~~
* 三参`reduce()`
实际上,在前两个方法的使用中我们发现,`reduce()`方法有着很多的使用限制,包括返回值类型需要和元素类型相同等问题,实际上,限制了我们对Stream中元素的进一步修改。在三参方法中,第三个参数是一个BinaryOperator类型的表达式。在常规情况下我们可以忽略这个参数,随便指定一个表达式即可,目的是为了通过编译器的检查,因为在常规的Stream中它并不会被执行到,然而, 虽然此表达式形同虚设,可是我们也不是把它设置为null,否则还是会报错。 在并行Stream中,此表达式则会被执行到。
~~~
public class StreamTest {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("张三",23));
list.add(new Person("李四",23));
list.add(new Person("王五",23));
List<String> lt = list.stream().map(person -> person.name).reduce(new ArrayList<String>(), (lst, per) -> {
lst.add(per);
return lst;
}, (lst, per) -> null);
System.out.println(lt);
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
~~~
那么,我们可以看到使用三参的方法,使我们代码少写很多,同样也显得优雅很多。那是否可以以一种更优雅的方式呢?
* **`collect()`方法**
collect()`方法有两个重载方法,但用的最多的还是和`java.util.stream.Collectors`类共同使用的方法。比如我们去改写上面问题的代码
~~~
List<String> lt = list.stream().map(person -> person.name).collect(Collectors.toList());
~~~
这样的代码同样可以去实现`reduce()`方法中一样的效果,其中主要依靠的就是`Collectors`类中自带方法的作用。
* **`toCollection()`方法**
该方法用于将流中的元素全部放置到一个集合中返回,包括List,Set,Queue
~~~
public class StreamTest {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("张三",23));
list.add(new Person("李四",23));
list.add(new Person("王五",23));
list.add(new Person("王五",23));
//List<String> lt = list.stream().map(person -> person.name).collect(Collectors.toCollection(ArrayList::new));
Set<String> lt = list.stream().map(person -> person.name).collect(Collectors.toCollection(HashSet::new));
System.out.println(lt);
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
~~~
* **`toList()`方法 & `toSet()`方法**
这两个方法用于明确将Stream流聚合为List集合或Set集合
~~~
List<String> lt = list.stream().map(person -> person.name).collect(Collectors.toList());
Set<String> lt = list.stream().map(person -> person.name).collect(Collectors.toSet());
~~~
* **`joining()`方法**
用于拼接流中的元素,如果元素不是String类型,将会以String类型的形式被拼接,该方法可以指定分隔符。
~~~
String lt = list.stream().map(person -> person.name).collect(Collectors.joining(","));
~~~
* **`mapping()`方法**
用于对流中所有的元素执行转换,该方法接收两个参数,第一个参数为Function接口,将会执行其中的apply抽象方法,作为元素转换的执行内容。第二个参数为Collector接口的实现类,该实现类可以指定元素的聚合。
`mapping()`方法其实和`map()`方法有些类似,都是用于对元素进行映射操作。但不同的地方在于`mapping()`方法有两个参数。第一个参数用于表示对元素的转换等操作, 第二个参数一般用于表示对元素的聚合操作,所以该方法也属于聚合作用。
~~~
public class StreamTest {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("张三",23));
list.add(new Person("李四",23));
list.add(new Person("王五",23));
list.add(new Person("王五",23));
//List<String> lt = list.stream().map(per -> per.name).collect(Collectors.toList());
List<String> lt = list.stream().collect(Collectors.mapping(per -> per.name, Collectors.toList()));
System.out.println(lt);
}
}
~~~
其实从上述代码可以看出,在`mapping()`方法中,我是把原有的map方法和collect方法结合起来了,那是否就意味着mapping方法有点多此一举呢?其实不是这样,mapping方法多用于在Stream转换完成后,需要最终进行转换并聚合的情况。
* **`collectingAndThen()`方法**
用于对聚合完的数据,进行再次聚合,其实第二次聚合可以认为是已聚合好数据的一种转换。
~~~
int lt = list.stream().map(per -> per.name).collect(Collectors.collectingAndThen(Collectors.toList(), lst -> lst.size()));
~~~
* **`counting()`方法**
用于对聚合完的数据进行统计总个数
~~~
long lt = list.stream().map(per -> per.name).collect(Collectors.counting());
~~~
* **`summingInt()`方法 & `summingLong()`方法 & `summingDouble()`方法**
这几个方法,用于对流中的元素进行求和,该方法的参数作用为将流传过来的参数改为对应的类型
~~~
//int lt = list.stream().map(per -> per.age).collect(Collectors.summingInt(Integer::valueOf));
long lt = list.stream().map(per -> per.age).collect(Collectors.summingLong(Long::valueOf));
~~~
`averagingInt`/`averagingLong`/`averagingDouble`:求平均数,使用方法同上
* **`toMap()`方法**
该方法用于将Stream中的元素转为Map集合,一般而言,我们会尽量使用三参的方法,如果确定key值不会重复,可以使用两参的方法。
在三参方法中,第一个参数用于生成key值,第二个参数用于生成value值,第三个参数用于提供key值相同时的解决方案。
![](https://img.kancloud.cn/46/17/46173b3c3ba394893c2eba6b48f20ee1_1446x182.png)
~~~
Map<String, Integer> lt = list.stream().collect(Collectors.toMap(per -> per.name, per -> per.age, (k1, k2) -> k2));
~~~
* **`groupingBy()`方法**
该方法用于对Stream流的元素进行分组,该方法存在重载方法,主要使用的是单参方法,只需要传入分组依据即可。
~~~
Map<Object, List<Person>> lt = list.stream().collect(Collectors.groupingBy(per -> per.age));
~~~