1、流的基本概念
含义
流是从给定的数据源生成的、支撑数据处理操作的元素序列。
解释:1)流会使用一个提供数据的源,如集合、数组或者输入/输出资源;2)数据处理操作:流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,比如filter、map、sort等;3)元素序列:序列和集合的含义、作用是有明显不同的,集合的目的是存储和访问元素,流的目的在于表达计算。
流与集合的比较
1)含义上的比较
集合是一个内存中的数据结构,也就是说集合的全体是存储在内存中的,意味着集合的容量不可能是无穷无尽的。从时间和空间的角度来讲,集合是空间(计算机内存)中分布的一组值,集合对象创建完成之后,在任意时间点都能访问到集合。
流中的元素是按需计算的。对于全体的质数,不可能把全体的质数存入集合然后放在内存中,但是可以构建一个质数流,需要的时候就从这个流中提取出需要的值,这些值只在需要的时候生成的。这是一种生产者-消费者的关系。从时间和空间的角度来讲,流是在时间中分布的一组值。
以电影举例来说,Java 8中的集合就像是存在DVD上的电影,必须从DVD中将整个电影文件读取到电脑中,才能播放电影。Java 8中的流就像是用在线流媒体看电影,不需要整个电影文件都缓存好了才能播放,缓存好了一小段就能观看了。
2)遍历数据的方式的比较
使用Collection接口需要用户去做迭代,这称为外部迭代。相反,Streams库使用内部迭代,即在API的内部做迭代。
内部迭代的好处:采用更优化的顺序进行迭代;并行处理。外部迭代只是简单的以集合中的位置访问元素,没有过多考虑元素实际的存储位置,采用外部迭代需要用户自己去管理所有的并行问题。
2、流的基本使用
流的基本使用包括如下几类:
- 筛选和切片:filter()、distinct()、limit()、skip()
- 映射:map()、flatMap()
- 查找和匹配:allMatch()、anyMatch()、noneMatch()、findFirst()、findAny()
- 规约:reduce()
流的使用示例如下:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Dish> menu = Arrays.asList(
new Dish("pork", false, 800, Type.MEAT),
new Dish("beef", false, 700, Type.MEAT),
new Dish("chicken",false, 400, Type.MEAT),
new Dish("french fries", true, 530, Type.OTHER),
new Dish("rice", true, 350, Type.OTHER),
new Dish("season fruit", true, 120, Type.OTHER),
new Dish("pizza", true, 550, Type.OTHER),
new Dish("prawns",false, 300, Type.FISH),
new Dish("salmon", false, 450, Type.FISH) );
List<String> threeHighCaloricDishNames = menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(Collectors.toList());
System.out.println(threeHighCaloricDishNames);
}
}
菜肴类:
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return name;
}
public boolean isVegetarian() {
return vegetarian;
}
public int getCalories() {
return calories;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return name + "";
}
}
菜肴类型枚举类:
public enum Type {
MEAT,
FISH,
OTHER
}
解释:首先对menu这个集合调用stream方法,得到一个流(流的数据源就是menu这个集合),接下来对这个流应用一系列数据处理操作(filter、map、limit和collect)。collect之前的操作都是接收一个流,经过处理之后,返回另一个流,形成了一条流水线,最后collect操作开始处理流水线,并返回结果。会返回另一个流的操作成为中间操作,从流水线返回结果的操作成为终端操作。
Stream的操作步骤总结:
- 创建Stream:从一个数据源(如集合、数组)中获取流。
- 中间操作:对数据源的数据进行操作,返回Stream本身。
- 最终操作:返回特定类型的计算结果。
3、收集数据汇总结果
收集器用于将流中的元素累积成一个汇总结果。使用收集器(这类函数式编程,相比于指令式编程)来得到结果,只用专注于希望的结果,而不用关心中间的执行过程,因此代码可读性强,易于维护;另一个好处是高效地复合,进行多级分组、分区和归约。
收集器可以分为预定义收集器和自定义收集器。预定义收集器是那些可以从Collectors类提供的工厂方法创建的收集器。
预定义收集器主要提供了三大功能:
- 归约和汇总:计数
counting()
、最大值maxBy()
、最小值minBy()
、求和summingInt()
、求平均averagingInt()
、连接字符串joining()
。 - 元素分组:
groupingBy()
- 元素分区:
partitionBy()
分组和分区的练习
// 首先按照菜肴的类别分组,在每个类别里面再按照卡路里等级分组
Map<Type, Map<CaloricLevel, List<Dish>>> dishedByTypeCaloricLevel =
menu.stream().collect(groupingBy(Dish::getType,
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
})));
System.out.println(dishedByTypeCaloricLevel);
// 结果:{FISH={DIET=[prawns], NORMAL=[salmon]},
// MEAT={FAT=[pork], DIET=[chicken], NORMAL=[beef]},
// OTHER={DIET=[rice, season fruit], NORMAL=[french fries, pizza]}}
// 首先按照是否是素食进行分区,然后按照按照卡路里是否大于500进行分区
Map<Boolean, Map<Boolean, List<Dish>>> result =
menu.stream().collect(partitioningBy(Dish::isVegetarian,
partitioningBy(d -> d.getCalories() > 500)));
System.out.println(result);
// 结果:{false={false=[chicken, prawns, salmon], true=[pork, beef]},
// true={false=[rice, season fruit], true=[french fries, pizza]}}
卡路里等级枚举类:
public enum CaloricLevel {
DIET,
NORMAL,
FAT
}
4、收集器接口
收集器接口的定义如下:
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
}
其中T表示流中元素的类型,A表示累加器的类型,R是收集操作得到的对象的类型。
方法说明:
- supplier方法:在调用时创建一个空的累加器实例(A),供数据收集过程使用。
- accumulator方法:将流中的元素添加到结果容器。
- combiner方法:合并两个结果容器。并行归约:将流分为多个子部分进行并行处理,然后再合并各个子部分归约所得的累加器。
- finisher方法:将累加器对象转换为收集操作的最终结果。
- characteristics方法:定义了收集器的行为有哪些优化选项。
- UNORDERED:归约结果不受流中项目的遍历和累积顺序的影响;
- CONCURRENT:accumulator方法可以从多个线程同时调用,且该收集器可以并行归约流;
- IDENTITY_FINISH:表明finisher方法返回的函数是一个恒等函数,意味着累加器对象就是最终的结果。
创建自定义收集器:
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new; // 创建一个空的累加器
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add; // 累积遍历过的项目,原位修改累加器
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2); // 合并累加器
return list1;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity(); // 恒等函数
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
Collector.Characteristics.IDENTITY_FINISH, Collector.Characteristics.CONCURRENT));
}
}
使用自定义收集器:
List<Dish> dishes = menu.stream().collect(new ToListCollector<>());
System.out.println(dishes);
// 结果:[pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
参考资料:《Java 8实战》