Java-Stream学习笔记(上)
这篇博客仅谈论Stream的基础使用,不涉及Stream效率的问题以及并行流的讲解。等时机成熟,再更下一篇。
1.基础概念
1.1Stream的作用
Stream能针对集合,数组等有限的数据,甚至是无限的数据进行操作。这种操作可以是多种多样的,比如过滤掉集合中不满足条件的数据,或是将集合转换为另外一种形式,而这种操作仅仅是一行的代码就能解决,简洁且优雅。
比如:
从一系列Employee对象中过滤工资低于2000的员工,然后按照工资降序排列,最后打印输出。
常规代码:
List<Employee> employeeList = Arrays.asList(
new Employee("a", 20, 1000),
new Employee("b", 30, 3000),
new Employee("c", 35, 2500),
new Employee("d", 22, 7000),
new Employee("e", 37, 10000)
);
List<Employee> employeeListAfterFiltering = new ArrayList<>();
for (Employee employee: employeeList) {
if(employee.getSalary()>=2000)
employeeListAfterFiltering.add(employee);
}
employeeListAfterFiltering.sort((x,y) ->y.getSalary() -x.getSalary());
employeeListAfterFiltering.forEach(System.out::println);
Stream代码:
Stream.of(
new Employee("a", 20, 1000),
new Employee("b", 30, 3000),
new Employee("c", 35, 2500),
new Employee("d", 22, 7000),
new Employee("e", 37, 10000)
).filter(x->x.getSalary()>=2000).sorted((x, y)-> y.getSalary()-x.getSalary()).forEach(System.out::println);
1.2Stream的使用步骤
Stream使用步骤分为三个步骤:
- 创建一个流。
- 指定一个流转换为另外一个流的中间操作,此操作可以有多个。
- 终止操作,此操作会执行强制之前的中间操作(中间操作是惰性操作,后面会解释)。从此以后,该流就不能够再使用。
需要注意两点:
1.中间操作都是将一个流转换为另一个流的操作。
2.终止操作一旦执行,最开始创建的流就不能再使用,否则会抛出IllegalStateException
异常。验证如下:
测试代码:
@Test
public void testBug(){
List<Employee> employeeList = Arrays.asList(
new Employee("a", 20, 1000),
new Employee("b", 30, 3000),
new Employee("c", 35, 2500),
new Employee("d", 22, 7000),
new Employee("e", 37, 10000)
);
Stream<Employee> stream = employeeList.stream();
stream.filter(x->x.getAge()>20).forEach(System.out::println);
//第二次使用流会报错
stream.filter(x->x.getAge()>20).forEach(System.out::println);
}
测试结果:
1.3Stream的特性
-
Stream不会存储元素,这些元素要么是存储在底层的集合中,要么是按需生成的。比如无限流就是一个比较极端的例子,它不可能存储在Stream里面。
-
流的操作并不会修改数据源,即流的操作并不会影响到用于产生流的集合,数组等等。
-
流的操作是尽可能惰性执行,只要当终止操作执行时,才会强制执行中间操作。
这里面我个人理解有两层意思:
其一,体现惰性操作的优越性。充分分析中间操作后,再对中间操作进行批处理。比如针对无限流的一个例子,首先的操作是采用peek进行打印测试,然后再通过limit限制无限流个数,显然按照正常的想法来,程序必然在peek步骤死循环,但是,实际情况如下:
测试代码:
@Test public void testInertiaOperation(){ Stream.iterate(0,x->x+2).peek(System.out::print).limit(5).forEach(x->{ }); }
结果:
02468
其二,**体现惰性操作的存在性。**因为中间操作都是将一个流转化为另一种流,根据这个特性设计如下代码:
测试代码:
@Test public void testInertiaOperation1(){ Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5).peek(System.out::println); System.out.println("我猜测我先于中间操作执行"); stream.forEach(x->{ }); }
结果:
我猜测我先于中间操作执行
1
2
3
4
5这表明惰性操作是的确存在的。
2.创建流
根据数据源的不同以及涉及到的类的不同,分为如下三种创建类的方式。
- 通过集合创建
- 通过数组创建
- 通过Stream创建
当然,创建流的方式不止上面三种,仅挑常用的来讲。
2.1通过集合创建流
Collection及其子类都有一个实例方法stream()
,利用该方法就能通过集合创建流。
测试代码:
Collection<Integer> collection = new HashSet<>();
Stream<Integer> stream = collection.stream();
2.2通过数组创建流
Arrays有一个静态方法stream(T[])
可以把数组转为流。
测试代码:
Integer [] arr = {
1,2,3,4};
Stream<Integer> integerStream2 = Arrays.stream(arr);
2.3通过Stream创建流
事实上,Stream可以通过可变参数列表创建流;创建仅一个元素的流;创建无限流;创建空流。
-
通过可变参数列表创建流
Stream.of(T ...values) : Stream<T>
测试代码:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
-
创建一个元素的流
Stream.of(T t) : Stream<T>
测试代码:
Stream<Integer> integerStream = Stream.of(1);
-
通过静态方法创建无限流
测试代码:
/** * 提供一个Supplier产生无限流 * Stream.generate( Supplier<T> s) : Stream<T> * 模拟创建一个 范围在0--100的无限流 */ Stream<Integer> integerStream1 = Stream.generate(() -> new Random().nextInt(101)); /** 提供一个seed 即初始值,和一个UnaryOperator产生无限流,UnaryOperator继承自Function<T,T> * Stream.iterate(final T seed, final UnaryOperator<T> f) : Stream<T> * 可以将f理解为一个函数,初始化值(第一个值)就是seed ,第二个值就是f(seed),第三个值就是 * f((seed)),如此类推。 * * initial(即seed) , f(x) * 1: initial * 2: f(initial * 3: f(f(initial)) * ...... */ Stream<Integer> integerStream3 = Stream.iterate(0, x -> x + 2);
-
通过静态方法创建空流
测试代码:
/** * Stream的静态方法创建空流 */ Stream<Object> empty = Stream.empty();
3.中间操作
为了方便演示中间操作,提前讲一个终止操作 void forEach(Consumer<? super T> action)
,该终止操作会对流中的每个元素都执行一个action操作。比如打印输出。
除此以外,为了方便演示,写了一个实体类(包含姓名,年龄,工资属性)
Employee:
package com.lordbao.entity;
import java.util.Objects;
/**
* @Author Lord_Bao
* @Date 2020/10/5 11:31
* @Version 1.0
*/
public class Employee {
private String username;
private int age;
private int salary;
public Employee() {
}
public Employee(String username, int age, int salary) {
this.username = username;
this.age = age;
this.salary = salary;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" +
"username='" + username + '\'' +
", age=" + age +
", salary=" + salary +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.age &&
salary == employee.salary &&
Objects.equals(username, employee.username);
}
@Override
public int hashCode() {
return Objects.hash(username, age, salary);
}
}
3.1Filter过滤操作
Stream<T> filter(Predicate<? super T> predicate)
通过向filter方法传入一个predicate参数,以此过滤掉那些不满足条件的元素,并返回一个满足条件的元素的新流。
场景描述:
取出集合中工资大于3000的员工并打印输出
测试代码:
@Test
public void testFilter(){
List<Employee> employeeList = Arrays.asList(
new Employee("a", 20, 1000),
new Employee("b", 30, 3000),
new Employee("c", 35, 2500),
new Employee("d", 22, 7000),
new Employee("e", 37, 10000)
);
// 取出集合中工资大于3000的并打印输出
employeeList.stream().filter(x->x.getSalary()>3000).forEach(System.out::println);
}
测试结果:
Employee{username=‘d’, age=22, salary=7000}
Employee{username=‘e’, age=37, salary=10000}
3.2Map映射操作
<R> Stream<R> map(Function<? super T,? extends R> mapper)
可以将Stream中的元素转换为其他的元素,并返回包含新元素的流。
场景描述:
取出集合中工资大于3000的员工的名字并打印输出
测试代码:
@Test
public void testMap(){
List<Employee> employeeList = Arrays.asList(
new Employee("a", 20, 1000),
new Employee("b", 30, 3000),
new Employee("c", 35, 2500),
new Employee("d", 22, 7000),
new Employee("e", 37, 10000)
);
// 取出集合中工资大于3000的名字并打印输出
employeeList.stream().filter(x->x.getSalary()>3000).map(Employee::getUsername).forEach(System.out::println);
}
测试结果:
d
e
3.3FlatMap映射并拆解操作
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
。此方法是将每个元素映射成一个流,然后将这些流的中的元素都取出来合并,然后返回合并后的新流。
按照这个逻辑将Stream中的每个元素都映射为Stream,然后将每个Stream都拆解,将拆解后的元素组装成
新的Stream。貌似看起来有点模糊,不如Map好理解啊。
其实这里可以这么分析,什么元素能够映射为Stream呢?换个问题就是Stream是怎么创建的?大多数是通过集合,数组呗。那么我可不可以理解为本来的数据源就是集合或是数组,而它存储的元素恰好也是集合或是数组呢?
按照这个思路,我设计了如下的例子。
测试代码:
@Test
public void testFlatMap2(){
//1.二维数组
Employee [] [] employeeArr1 ={
{
new Employee("a", 20, 1000),new Employee("c", 35, 2500)},
{
new Employee("b", 30,