JDK1.8新特性之Stream
1 JDK1.8中新增的Stream用法
- Stream有什么用?有哪些使用场景?
1.1 什么是Stream
流是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
注意:
- Stream自己不会存储元素
- Stream不会改变原对象。相反,他们会返回一个持有结果的新Stream。
- Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
1.1.1 Stream操作的三个步骤
- 创建Stream
一个数据源(如:集合、数组),获取一个流 - 中间操作
一个中间操作链,对数据源的数据进行处理 - 终止操作(终端操作)
一个终止操作,执行中间操作链,并产生结果。
1.1.1.1 创建Stream的几种方式
- 可以通过Collection系列集合提供的stream()或parallelStream()
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
}
- 通过Arrays中的静态方法stream()获取数组流
public static void main(String[] args) {
Apple[] apples = new Apple[10];
Stream<Apple> stream = Arrays.stream(apples);
}
- 通过Stream类中的静态方法of()
Stream<String> stream = Stream.of("aa","bb","cc");
- 创建无限流(有两种方式:迭代和生成)
//迭代
Stream<Integer> stream = Stream.iterate(0,(x) -> x + 2);
stream.limit(10).forEach(System.out::println);
//生成
Stream.generate(() -> Math.random()).limit(5).forEach(System.out::println);
1.1.1.2 Stream的中间操作
1.1.1.2.1 筛选与切片
filter ----- 接收 Lambda,从流中排除某些元素
limit ----- 截断流,使其元素不超过给定数量
skip(n) ----- 跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)互补
distinct ------ 筛选,通过流所生成元素的hashCode()和equals()去除重复元素
注意:多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。
1.1.1.2.1.1 filter的使用
filter方法当中的参数是Predicate函数式接口,当中是boolean test(T t);适合用来判断过滤
下面使用例子:筛选年龄大于35岁的员工
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三",18),
new Employee("李四",58),
new Employee("王五",26),
new Employee("赵六",36),
new Employee("田七",12)
);
public static void main(String[] args) {
//中间操作
Stream<Employee> stream = employees.stream()
.filter((e) -> {
System.out.println("Stream API的中间操作");
return e.getAge() > 35;
});
//终止操作:一次性执行全部内容,即“惰性求值”
stream.forEach(System.out::println);
}
}
运行结果如下:
Stream API的中间操作
Stream API的中间操作
Employee{name='李四', age=58}
Stream API的中间操作
Stream API的中间操作
Employee{name='赵六', age=36}
Stream API的中间操作
1.1.1.2.1.2 limit的使用
依然是上面的代码加上limit的使用代码如下:
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三",18),
new Employee("李四",58),
new Employee("王五",26),
new Employee("赵六",36),
new Employee("田七",12)
);
public static void main(String[] args) {
//中间操作
employees.stream()
.filter((e) -> {
System.out.println("Stream API的中间操作");
return e.getAge() > 35;
}).limit(1).forEach(System.out::println);
}
}
执行结果如下
Stream API的中间操作
Stream API的中间操作
Employee{name='李四', age=58}
我们从结果可以看出加上limit的使用后stream在遍历后当满足limit中的指定的数量后便停止了后面的遍历操作。因此在某种情况是可以提供一定的性能的。
1.1.1.2.1.3 skip的使用
skip(n) ----- 跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)互补。
使用例子:
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三",18),
new Employee("李四",58),
new Employee("王五",26),
new Employee("赵六",36),
new Employee("田七",41)
);
public static void main(String[] args) {
//中间操作
employees.stream()
.filter((e) -> {
System.out.println("Stream API的中间操作");
return e.getAge() > 35;
}).skip(1).forEach(System.out::println);
}
}
执行结果如下:
Stream API的中间操作
Stream API的中间操作
Stream API的中间操作
Stream API的中间操作
Employee{name='赵六', age=36}
Stream API的中间操作
Employee{name='田七', age=41}
这里skip(n)就表示先去除满足filter条件的前n条。
1.1.1.2.1.4 distinct的使用
distinct的作用是去重,但需要将元素的hashCode()和equals()重写。否则起不到去重作用。
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三",18),
new Employee("李四",58),
new Employee("王五",26),
new Employee("赵六",36),
new Employee("田七",41),
new Employee("田七",41),
new Employee("田七",41)
);
public static void main(String[] args) {
//中间操作
employees.stream()
.filter((e) -> {
System.out.println("Stream API的中间操作");
return e.getAge() > 35;
}).skip(1).distinct().forEach(System.out::println);
}
}
运行结果
Stream API的中间操作
Stream API的中间操作
Stream API的中间操作
Stream API的中间操作
Employee{name='赵六', age=36}
Stream API的中间操作
Employee{name='田七', age=41}
Stream API的中间操作
Stream API的中间操作
1.1.1.2.2 映射
映射:map ----- 接收Lambda,将元素转换成其他形式或提取信息。接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
flatMap ----- 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
map方法中的参数是Function函数式接口类型,说明他是实现功能作用的。
下面是使用map方法实现链表中的字符串转大写
public static void main(String[] args) {
List<String> list = Arrays.asList("aaa","bbb","ccc","ddd","eee");
list.stream().map((str) -> str.toUpperCase()).forEach(System.out::println);
}
实现结果:
AAA
BBB
CCC
DDD
EEE
1.1.1.2.3 排序
排序:sorted() ---- 自然排序(Comparable)
sorted(Comparator com) ----- 定制排序(Comparator)
下面是根据Employee对象的名字排序,如果名字一样按年龄排序。
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三","18"),
new Employee("李四","58"),
new Employee("王五","26"),
new Employee("赵六","36"),
new Employee("田七","41")
);
public static void main(String[] args) {
List<String> list = Arrays.asList("ccc","aaa","bbb","ddd","eee");
list.stream().sorted().forEach(System.out::println);
System.out.println("------------------------------------");
employees.stream().sorted((e1,e2) -> {
if(e1.getName().equals(e2.getName())){
return e1.getAge().compareTo(e2.getAge());
}else{
return e1.getName().compareTo(e2.getName());
}
}).forEach(System.out::println);
}
}
1.1.1.3 Stream的终止操作
1.1.1.3.1 查找与匹配
allMatch ----- 检查是否匹配所有元素
anyMatch ---- 检查是否至少匹配一个元素
noneMatch ----- 检查是否没有匹配所有元素
findFirst ------ 返回第一个元素
findAny ------ 返回当前流中的任意元素
count ------ 返回流中元素的总个数
max ------ 返回流中最大值
min ------ 返回流中最小值
使用操作如下代码:
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三",18,9999, Employee.Status.FREE),
new Employee("李四",58,5555,Employee.Status.BUSY),
new Employee("王五",26,3333,Employee.Status.VOCATION),
new Employee("赵六",36,6666, Employee.Status.FREE),
new Employee("田七",41,8888, Employee.Status.BUSY)
);
public static void main(String[] args) {
//查询是否所有员工都在BUSY状态
boolean b1 = employees.stream().allMatch((e) -> e.getStatus().equals(Employee.Status.BUSY));
System.out.println(b1);
//查询是否存在至少有一个员工在BUSY状态
boolean b2 = employees.stream().anyMatch((e) -> e.getStatus().equals(Employee.Status.BUSY));
System.out.println(b2);
//查询是否没有员工在BUSY状态
boolean b3 = employees.stream().noneMatch((e) -> e.getStatus().equals(Employee.Status.BUSY));
System.out.println(b3);
//查询员工工资最低的一个
//这个Optional主要是为了防止返回为空所用的容器对象
Optional<Employee> op = employees.stream().sorted((e1, e2) -> Double.compare(e1.getSalary(),e2.getSalary())).findFirst();
System.out.println(op.get());
//查询状态为FREE的任意一个员工,这里采用parallelStream是并行流是多线程状态下操作流
Optional<Employee> op2 = employees.parallelStream().filter((e) -> e.getStatus().equals(Employee.Status.FREE)).findAny();
System.out.println(op2.get());
//返回流中元素个数
Long count = employees.stream().count();
System.out.println(count);
//查询工资最高的员工
Optional<Employee> op1 = employees.stream().max((e1,e2) -> Double.compare(e1.getSalary(),e2.getSalary()));
System.out.println(op1.get());
//查询最低工资是多少
Optional<Double> op5 = employees.stream().map(Employee::getSalary).min(Double::compare);
System.out.println(op5.get());
}
}
下面是Employee类
public class Employee {
private String name;
private Integer age;
private double salary;
private Status status;
public Employee(String name, Integer age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
public Employee(String name, Integer age, double salary, Status status) {
this.name = name;
this.age = age;
this.salary = salary;
this.status = status;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public void setAge(Integer age) {
this.age = age;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
", status=" + status +
'}';
}
public enum Status{
FREE,
BUSY,
VOCATION;
}
}
1.1.1.3.2 归约
reduce(T identity, BinaryOperator) / reduce(BinaryOperator) ----可以将流中元素反复结合起来,得到一个值。
需求:对一个Integer类型的数组求和
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9);
Integer sum = list.stream().reduce(0,(x,y) -> x + y);
System.out.println(sum);
}
reduce方法的第一个参数表示从0开始,而第二个参数是BinaryOperator<T> accumulator类型的函数式接口,它的执行过程:先把第一个参数作为起始值,x先作为起始值0进行程序执行,然后从流中取出元素作为y(这里首先取第一个1)进行计算,再将x+y的结果作为x依次进行执行。
再举个使用例子:计算员工工资总和。
思路:我们应该先使用map方法将流中用户的薪水取出再进行归约计算
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三",18,9999, Employee.Status.FREE),
new Employee("李四",58,5555,Employee.Status.BUSY),
new Employee("王五",26,3333,Employee.Status.VOCATION),
new Employee("赵六",36,6666, Employee.Status.FREE),
new Employee("田七",41,8888, Employee.Status.BUSY)
);
public static void main(String[] args) {
Optional<Double> op = employees.stream().map(Employee::getSalary).reduce(Double::sum);
System.out.println(op.get());
}
}
这种map和reduce的连接通常称为map-reduce模式,因Google用它来进行网络搜索而出名。
1.1.1.3.3 收集
collect ----- 将流转换为其他形式。接收一个Collector接口的实现,用于给Stream中元素做汇总的方法,该方法的实现决定了如何对流执行收集操作(如收集到List、Set、Map)。但是Collectors实现类提供了很多静态方法,可以方便地创建常见收集器实例。
实例:收集员工的名字
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三",18,9999, Employee.Status.FREE),
new Employee("李四",58,5555,Employee.Status.BUSY),
new Employee("王五",26,3333,Employee.Status.VOCATION),
new Employee("赵六",36,6666, Employee.Status.FREE),
new Employee("田七",41,8888, Employee.Status.BUSY)
);
public static void main(String[] args) {
List<String> list = employees.stream().map(Employee::getName).collect(Collectors.toList());
list.forEach(System.out::println);
}
}
还可以指定收集到不同集合类型中
下面是收集到HashSet中
public class Main {
static List<Employee> employees = Arrays.asList(
new Employee("张三",18,9999, Employee.Status.FREE),
new Employee("李四",58,5555,Employee.Status.BUSY),
new Employee("王五",26,3333,Employee.Status.VOCATION),
new Employee("赵六",36,6666, Employee.Status.FREE),
new Employee("田七",41,8888, Employee.Status.BUSY)
);
public static void main(String[] args) {
HashSet<String> hs = employees.stream().map(Employee::getName).collect(Collectors.toCollection(HashSet::new));
hs.forEach(System.out::println);
}
}
1.2 并行流与顺序流
1.2.1 什么是并行流
并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
Java8中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API可以声明性地通过parallel()与sequential()在并行流与顺序流之间进行切换。在使用并行流之前先要对Fork/Join框架有个认识和理解。
1.2.1.1 了解Fork/Join框架
那么Fork/Join框架和传统多线程池的区别在哪呢?
1.2.1.2 Fork/Join框架和传统多线程池的区别
简单来说就是传统的线程池的“CPU利用率不高”,当某个线程工作完成后就“闲”着等其他线程完成工作了(阿姨你快点嘛阿姨,别磨磨蹭蹭的。。),而在Fork/Join中某一线程完成后会对其他还未完成工作的线程手中未完成的工作“偷”一些来帮助整体工作的进行。这一机制就叫“工作窃取”模式。
那么ForkJoin为什么在1.8之前人们使用不多呢?因为使用起来麻烦。下面手写使用ForkJoin进行累加操作。体验ForkJoin的效率。