目录
一、基本用法
1、基本过滤
2、基本转换
3、基本的过滤和转换组合
二、中间操作
1、distinct
2、sorted
3、skip/limit
4、peek
5、mapToLong/mapToInt/mapToDouble
6、flatMap
三、终端操作
1、max/min
2、count
3、allMatch/anyMatch/noneMatch
4、findFirst/findAny
5、forEach
6、toArray
7、reduce
四、容器收集器
1、toSet
2、toCollection
3、toMap
4、字符串收集器
五、 分组
1、基本用法
2、分组计数、找最大/最小元素
3、分组数值统计
4、分组内的map
5、分组结果处理(filter/sort/skip/limit)
6、分区
五、函数式数据处理思维
先创建学生类
public class Student {
private String name;
private Integer score;
private String grade;
}
创建学生列表
Student stu1 = Student.builder().name("张三").grade("一年级").score(92).build();
Student stu2 = Student.builder().name("李四").grade("二年级").score(89).build();
Student stu3 = Student.builder().name("王五").grade("一年级").score(90).build();
Student stu4 = Student.builder().name("赵六").grade("二年级").score(91).build();
Student stu5 = Student.builder().name("陈七").grade("一年级").score(88).build();
Student stu6 = Student.builder().name("周八").grade("二年级").score(86).build();
Student stu7 = Student.builder().name("孙九").grade("一年级").score(99).build();
List stuList = new ArrayList<>();
stuList.add(stu1);
stuList.add(stu2);
stuList.add(stu3);
stuList.add(stu4);
stuList.add(stu5);
stuList.add(stu6);
stuList.add(stu7);
返回学生列表中90分以上的同学
List collect =
stuList.stream()
.filter(s -> s.getScore() > 90)
.collect(Collectors.toList());
collect.forEach(System.out::println);
输出结果:
Student(name=张三, score=92, grade=一年级)
Student(name=赵六, score=91, grade=二年级)
Student(name=孙九, score=99, grade=一年级)
根据学生列表返回名称列表
List nameList =
stuList.stream()
.map(Student::getName)
.collect(Collectors.toList());
nameList.forEach(System.out::println);
输出结果:
张三
李四
王五
赵六
陈七
周八
孙九
返回90分以上的学生名称列表
List collect =
stuList.stream()
.filter(s -> s.getScore() > 90)
.map(Student::getName)
.collect(Collectors.toList());
collect.forEach(System.out::println);
输出结果:
张三
赵六
孙九
distinct返回一个新的Stream,过滤重复的元素,只留下唯一的元素,是否重复是根据equals方法来比较的。
List strList = new ArrayList<>();
strList.add("abc");
strList.add("bcd");
strList.add("cde");
strList.add("abc");
List collect =
strList.stream()
.distinct()
.collect(Collectors.toList());
collect.forEach(System.out::println);
输出结果 :
abc
bcd
cde
有两个sorted方法:
Stream sorted(Comparator super T> comparator);
Stream sorted();
1)第一个方法接受一个自定义的Comparator。 根据分数排序
List collect =
stuList.stream()
.sorted(Comparator.comparing(Student::getSore))
.collect(Collectors.toList());
collect.forEach(System.out::println);
输出结果:
Student(name=周八, score=86, grade=二年级)
Student(name=陈七, score=88, grade=一年级)
Student(name=李四, score=89, grade=二年级)
Student(name=王五, score=90, grade=一年级)
Student(name=赵六, score=91, grade=二年级)
Student(name=张三, score=92, grade=一年级)
Student(name=孙九, score=99, grade=一年级)
2)第二个方法假定元素实现了Comparable接口
学生类实现Comparable接口
public class Student implements Comparable {
private String name;
private Integer sore;
private String grade;
@Override
public int compareTo(Object obj) {
Student stu1 = (Student) obj;
if (stu1.getSore().equals(this.sore)){
return 0;
} else if (stu1.getSore() > this.sore){
return -1;
} else {
return 1;
}
}
}
根据分数排序:
List collect1 = stuList.stream().sorted().collect(Collectors.toList());
collect1.forEach(System.out::println);
输出结果 :
Student(name=周八, score=86, grade=二年级)
Student(name=陈七, score=88, grade=一年级)
Student(name=李四, score=89, grade=二年级)
Student(name=王五, score=90, grade=一年级)
Student(name=赵六, score=91, grade=二年级)
Student(name=张三, score=92, grade=一年级)
Student(name=孙九, score=99, grade=一年级)
Stream skip(long n);
Stream limit(long maxSize);
skip跳过流中的n个元素,如果流中元素不足n个,返回一个空流,limit限制流的长度为maxSize。
比如,将学生列表按照分数从高到低排序,分数一样的按名称排序,返回第3名到第5名,代码为:
List collect =
stuList.stream()
.sorted( Comparator.comparing(Student::getScore)
.reversed()
.thenComparing(Student::getName))
.skip(2)
.limit(3)
.collect(Collectors.toList());
collect.forEach(System.out::println);
输出结果:
Student(name=王五, score=90, grade=一年级)
Student(name=赵六, score=90, grade=二年级)
Student(name=李四, score=89, grade=二年级)
skip和limit都是有状态的中间操作。对前n个元素,skip的操作就是过滤,对后面的元素,skip就是传递给流水线中的下一个操作。limit的一个特点是:它不需要处理流中的所有元素,只要处理的元素个数达到maxSize,后面的元素就不需要处理了,这种可以提前结束的操作称为短路操作。
Stream peek(Consumer super T> action);
它返回的流与之前的流是一样的,没有变化,但它提供了一个Consumer,会将流中的每一个元素传给该Consumer。这个方法的主要目的是支持调试,可以使用该方法观察在流水线中流转的元素,比如:
List collect = stuList.stream()
.peek(System.out::println).map(Student::getName).collect(Collectors.toList());
System.out.println("====================================================");
collect.forEach(System.out::println);
输出结果:
Student(name=张三, score=92, grade=一年级)
Student(name=李四, score=89, grade=二年级)
Student(name=王五, score=90, grade=一年级)
Student(name=赵六, score=90, grade=二年级)
Student(name=陈七, score=88, grade=一年级)
Student(name=周八, score=86, grade=二年级)
Student(name=孙九, score=99, grade=一年级)
====================================================
张三
李四
王五
赵六
陈七
周八
孙九
map函数接受的参数是一个Function
IntStream mapToInt(ToIntFunction super T> mapper);
LongStream mapToLong(ToLongFunction super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction super T> mapper);
DoubleStream/IntStream/LongStream是基本类型特定的流,有一些专门的更为高效的方法。比如,求学生列表的分数总和,代码为:
int sum = stuList.stream().mapToInt(Student::getScore).sum();
Stream flatMap(Function super T, ? extends Stream extends R>> mapper);
它接受一个函数mapper,对流中的每一个元素,mapper会将该元素转换为一个流Stream,然后把新生成流的每一个元素传递给下一个操作。比如:
List strList = new ArrayList<>();
strList.add("abc,bcd");
strList.add("cde,def");
List collect =
strList.stream()
.flatMap(s -> Arrays.stream(s.split(",")))
.collect(Collectors.toList());
collect.forEach(System.out::println);
这里的mapper将一行字符串按空白符分隔为了一个单词流,Arrays.stream可以将一个数组转换为一个流,输出为:
abc
bcd
cde
def
可以看出,实际上,flatMap完成了一个1到n的映射。
中间操作不触发实际的执行,返回值是Stream,而终端操作触发执行,返回一个具体的值,除了collect, Stream API的终端操作还有max、min、count、allMatch、anyMatch、noneMatch、findFirst、findAny、forEach、toArray、reduce等,我们逐个介绍。
Optional max(Comparator super T> comparator);
Optional min(Comparator super T> comparator);
它们返回流中的最大值/最小值,它们的返回值类型是Optional
Optional max = stuList.stream().max(Comparator.comparing(Student::getScore));
Student student = max.get();
System.out.println(student);
输出结果:
Student(name=孙九, score=99, grade=一年级)
返回流中元素的个数
long count = stuList.stream().filter(stu -> stu.getScore() > 90).count();
这几个函数都接受一个谓词Predicate,返回一个boolean值,用于判定流中的元素是否满足一定的条件。它们的区别是:
如果流为空,那么这几个函数的返回值都是true。比如,判断是不是所有学生都及格了(不小于60分),代码可以为:
boolean b = stuList.stream().allMatch(stu -> stu.getScore() > 60);
这几个操作都是短路操作,不一定需要处理所有元素就能得出结果,比如,对于all-Match,只要有一个元素不满足条件,就能返回false。
Optional findFirst();
Optional findAny();
它们的返回类型都是Optional,如果流为空,返回Optional.empty()。findFirst返回第一个元素,而findAny返回任一元素,它们都是短路操作。随便找一个不及格的学生,代码可以为:
Optional any = stuList.stream().filter(stu -> stu.getScore() < 60).findAny();
if (any.isPresent()){
System.out.println( any.get().getName() + "拖出去,打一顿");
}
void forEach(Consumer super T> action);
void forEachOrdered(Consumer super T> action);
它们都接受一个Consumer,对流中的每一个元素,传递元素给Consumer。区别在于:在并行流中,forEach不保证处理的顺序,而forEachOrdered会保证按照流中元素的出现顺序进行处理。
stuList.stream().filter(stu -> stu.getScore() > 90).forEach(System.out::println);
Object[] toArray();
A[] toArray(IntFunction generator);
不带参数的toArray返回的数组类型为Object[],这通常不是期望的结果,如果希望得到正确类型的数组,需要传递一个类型为IntFunction的generator。generator接受的参数是流的元素个数,它应该返回对应大小的正确类型的数组。比如,获取90分以上的学生数组,代码可以为:
Student[] students =
stuList.stream()
.filter(stu -> stu.getScore() > 90)
.toArray(Student[]::new);
Arrays.stream(students).forEach(System.out::println);
输出结果:
Student(name=张三, score=92, grade=一年级)
Student(name=孙九, score=99, grade=一年级)
reduce代表归约或者叫折叠,它是max/min/count的更为通用的函数,将流中的元素归约为一个值。有三个reduce函数:
Optional reduce(BinaryOperator accumulator);
U reduce(U identity,
BiFunction accumulator,
BinaryOperator combiner);
R collect(Supplier supplier,
BiConsumer accumulator,
BiConsumer combiner);
reduce函数虽然更为通用,但比较费解,难以使用,一般情况下应该优先使用其他函数。collect函数比reduce函数更为通用、强大和易用,关于它,我们稍后再详细介绍。
对于collect方法,前面只是演示了其最基本的应用,它还有很多强大的功能,比如与toList类似的容器收集器还有toSet、toCollection、toMap等
toSet的使用与toList类似,只是它可以排重,就不举例了。toList背后的容器是ArrayList, toSet背后的容器是HashSet
toCollection是一个通用的容器收集器,可以用于任何Collection接口的实现类。比如,如果希望排重但又希望保留出现的顺序,可以使用LinkedHashSet, Collector可以这么创建:
List strList = new ArrayList<>();
strList.add("abc");
strList.add("bcd");
strList.add("cde");
strList.add("abc");
LinkedHashSet collect = strList.stream()
.filter(s -> s.length() > 2)
.collect(Collectors.toCollection(LinkedHashSet::new));
collect.forEach(System.out::println);
输出结果:
abc
bcd
cde
Collector> toMap(Function super T, ? extends K> keyMapper,
Function super T, ? extends U> valueMapper);
Collector> toMap(Function super T, ? extends K> keyMapper,
Function super T, ? extends U> valueMapper,
BinaryOperator mergeFunction);
Collector toMap(Function super T, ? extends K> keyMapper,
Function super T, ? extends U> valueMapper,
BinaryOperator mergeFunction,
Supplier mapSupplier)
1)第一个方法示例代码:
Map collect = stuList.stream()
.collect(Collectors.toMap(Student::getName, s -> s));
System.out.println(collect);
注:键如果有重复的,会抛出异常的
s->s是valueMapper,表示值就是元素本身。这个函数用得比较多,接口Function定义了一个静态函数identity表示它。也就是说,上面的代码可以替换为:
Map collect = stuList.stream()
.collect(Collectors.toMap(Student::getName, Function.identity()));
System.out.println(collect);
输出结果:
{
李四 = Student(name = 李四, score = 89, grade = 二年级),
张三 = Student(name = 张三, score = 92, grade = 一年级),
王五 = Student(name = 王五, score = 90, grade = 一年级),
周八 = Student(name = 周八, score = 86, grade = 二年级),
陈七 = Student(name = 陈七, score = 88, grade = 一年级),
赵六 = Student(name = 赵六, score = 91, grade = 二年级),
孙九 = Student(name = 孙九, score = 99, grade = 一年级)
}
2)处理键重复问题:相比前面的toMap,多了一个mapSupplier,它是Map的工厂方法,对于前面的两个toMap,其mapSupplier其实是HashMap::new。我们知道,HashMap是没有任何顺序的,如果希望保持元素出现的顺序,可以替换为LinkedHashMap,如果希望收集的结果排序,可以使用TreeMap。
List strList = new ArrayList<>();
strList.add("abc");
strList.add("bcd");
strList.add("cde");
strList.add("abc");
Map collect =
strList.stream().collect(Collectors.toMap(Function.identity(), s -> s.length(), (oldValue, value) -> value));
System.out.println(collect);
相比前面的toMap,它接受一个额外的参数mergeFunction,它用于处理冲突,在收集一个新元素时,如果新元素的键已经存在了,系统会将新元素的值与键对应的旧值一起传递给mergeFunction得到一个值,然后用这个值给键赋值。
也可以进行处理:
Map collect =
strList.stream().collect(Collectors.toMap(Function.identity(), s -> s.length(), (oldValue, value) -> oldValue + value));
System.out.println(collect);
输出结果:
{bcd=3, abc=6, cde=3}
3)第三个方法相比前面的toMap,多了一个mapSupplier,它是Map的工厂方法,对于前面的两个toMap,其mapSupplier其实是HashMap::new。我们知道,HashMap是没有任何顺序的,如果希望保持元素出现的顺序,可以替换为LinkedHashMap,如果希望收集的结果排序,可以使用TreeMap。
Map collect =
strList.stream().collect(Collectors.toMap(Function.identity(), s -> s.length(), (oldValue, value) -> value, LinkedHashMap::new));
System.out.println(collect);
public static Collector joining();
public static Collector joining(CharSequence delimiter);
public static Collector joining(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix);
第一个就是简单地把元素连接起来,第二个支持一个分隔符,还可以给整个结果字符串加前缀和后缀,比如:
List strList = new ArrayList<>();
strList.add("abc");
strList.add("bcd");
strList.add("cde");
strList.add("abc");
String collect = strList.stream().collect(Collectors.joining(",", "[", "]"));
System.out.println(collect);
输出结果:
[abc,bcd,cde,abc]
Map> collect = stuList.stream()
.collect(Collectors.groupingBy(Student::getGrade));
System.out.println(collect);
输出结果:
{
一年级=[
Student(name=张三, score=92, grade=一年级),
Student(name=王五, score=90, grade=一年级),
Student(name=陈七, score=88, grade=一年级),
Student(name=孙九, score=99, grade=一年级)],
二年级=[
Student(name=李四, score=89, grade=二年级),
Student(name=赵六, score=91, grade=二年级),
Student(name=周八, score=86, grade=二年级)
]
}
// 计数
public static Collector counting();
// 计算最小值
public static Collector> minBy(Comparator super T> comparator);
// 计算最大值
public static Collector> maxBy(Comparator super T> comparator);
1)计数代码示例:
Map collect = stuList.stream()
.collect(Collectors.groupingBy(Student::getGrade, Collectors.counting()));
System.out.println(collect);
输出结果:
{一年级=4, 二年级=3}
2)获取最大值
Map> collect = stuList.stream()
.collect(Collectors.groupingBy(Student::getGrade, Collectors.maxBy(Comparator.comparing(Student::getScore))));
System.out.println(collect);
需要说明的是,这个分组收集结果是Optional
Map collect = stuList.stream()
.collect(
Collectors.groupingBy(
Student::getGrade,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(Student::getScore)),
Optional::get)));
System.out.println(collect);
输出结果:
{
一年级=Student(name=孙九, score=99, grade=一年级),
二年级=Student(name=赵六, score=91, grade=二年级)
}
关于collectingAndThen,我们稍后再进一步讨论。
除了基本的分组计数,还经常需要进行一些分组数值统计,比如求学生分数的和、平均分、最高分、最低分等、针对int、long和double类型,Collectors提供了专门的收集器,比如:
// 求平均值,double和long也有类似方法
public static Collector averagingInt(ToIntFunction super T> mapper);
// 求和,double和long也有类似方法
public static Collector summingInt(ToIntFunction super T> mapper);
// 求多种汇总信息,double和long也有类似方法
// IntSummaryStatistics包括个数、最大值、最小值、和、平均数等多种信息
public static Collector summarizingInt(ToIntFunction super T> mapper);
比如,按年级统计学生分数信息,代码可以为:
Map collect = stuList.stream()
.collect(Collectors.groupingBy(Student::getGrade, Collectors.summarizingInt(Student::getScore)));
System.out.println(collect);
输出结果:
{
一年级=IntSummaryStatistics{count=4, sum=369, min=88, average=92.250000, max=99},
二年级=IntSummaryStatistics{count=3, sum=266, min=86, average=88.666667, max=91}
}
对于每个分组内的元素,我们感兴趣的可能不是元素本身,而是它的某部分信息。在StreamAPI中,Stream有map方法,可以将元素进行转换,Collectors也为分组元素提供了函数mapping,如下所示:
public static Collector mapping(
Function super T, ? extends U> mapper,Collector super U, A, R> downstream);
对学生按年级分组,得到学生名称列表,代码可以为:
Map> collect = stuList.stream().collect(
Collectors.groupingBy(
Student::getGrade, Collectors.mapping(
Student::getName, Collectors.toList())));
System.out.println(collect);
输出结果:
{
一年级=[张三, 王五, 陈七, 孙九],
二年级=[李四, 赵六, 周八]
}
对分组后的元素,我们可以计数,找最大/最小元素,计算一些数值特征,还可以转换(map)后再收集,那可不可以像Stream API一样,排序(sort)、过滤(filter)、限制返回元素(skip/limit)呢?Collector没有专门的收集器,但有一个通用的方法:
public static Collector collectingAndThen(Collector downstream,
Function finisher);
这个方法接受一个下游收集器downstream和一个finisher,返回一个收集器,也就是说,它在下游收集器的结果上又调用了finisher。利用这个finisher,我们可以实现多种功能,下面看一些例子。收集完再排序,可以定义如下方法:
public Collector> collectingAndSort(Collector> downstream, Comparator super T> comparator){
Collector> tListCollector = Collectors.collectingAndThen(downstream, (r) -> {
r.sort(comparator);
return r;
});
return tListCollector;
}
将学生按年级分组,分组内的学生按照分数由高到低进行排序,利用这个方法,代码可以为:
class DemoApplicationTests {
@Test
void contextLoads2() {
// 生成学生列表
List stuList = this.getStuList();
Map> collect = stuList.stream().collect(
Collectors.groupingBy(
Student::getGrade, this.collectingAndSort(
Collectors.toList(),
Comparator.comparing(Student::getScore).reversed())));
System.out.println(collect);
}
public Collector> collectingAndSort(Collector> downstream, Comparator super T> comparator){
Collector> tListCollector = Collectors.collectingAndThen(downstream, (r) -> {
r.sort(comparator);
return r;
});
return tListCollector;
}
List getStuList() {
Student stu1 = Student.builder().name("张三").grade("一年级").score(92).build();
Student stu2 = Student.builder().name("李四").grade("二年级").score(89).build();
Student stu3 = Student.builder().name("王五").grade("一年级").score(90).build();
Student stu4 = Student.builder().name("赵六").grade("二年级").score(91).build();
Student stu5 = Student.builder().name("陈七").grade("一年级").score(88).build();
Student stu6 = Student.builder().name("周八").grade("二年级").score(86).build();
Student stu7 = Student.builder().name("孙九").grade("一年级").score(99).build();
List stuList = new ArrayList<>();
stuList.add(stu1);
stuList.add(stu2);
stuList.add(stu3);
stuList.add(stu4);
stuList.add(stu5);
stuList.add(stu6);
stuList.add(stu7);
return stuList;
}
}
分组的一个特殊情况是分区,就是将流按true/false分为两个组,Collectors有专门的分区函数:
public static Collector>> partitioningBy(Predicate super T> predicate);
public static Collector> partitioningBy(Predicate super T> predicate, Collector super T, A, D> downstream);
第一个函数的下游收集器为toList(),第二个函数可以指定一个下游收集器。比如,将学生按照是否及格(大于等于60分)分为两组,代码可以为:
Map> collect = stuList.stream().collect(Collectors.partitioningBy(s -> s.getScore() >= 60));
System.out.println(collect);
输出结果:
{
false=[],
true=[
Student(name=张三, score=92, grade=一年级),
Student(name=李四, score=89, grade=二年级),
Student(name=王五, score=90, grade=一年级),
Student(name=赵六, score=91, grade=二年级),
Student(name=陈七, score=88, grade=一年级),
Student(name=周八, score=86, grade=二年级),
Student(name=孙九, score=99, grade=一年级)
]
}
按是否及格分组后,计算每个分组的平均分,代码可以为:
Map collect = stuList.stream().collect(
Collectors.partitioningBy(
s -> s.getScore() >= 60, Collectors.averagingInt(Student::getScore)));
System.out.println(collect);
输出结果:
{false=0.0, true=90.71428571428571}
可以看出,使用Stream API处理数据集合,与直接使用容器类API处理数据的思路是完全不一样的。流定义了很多数据处理的基本函数,对于一个具体的数据处理问题,解决的主要思路就是组合利用这些基本函数,以声明式的方式简洁地实现期望的功能,这种思路就是函数式数据处理思维,相比直接利用容器类API的命令式思维,思考的层次更高。