java8-Stream Api流详解
1 Stream API的意义
流的定义:通俗地说,不用再写循环,判断等细节实现的代码,直接以声明式方式编写你的意图。节省了大量的中间容器去存储临时的中间变量。那流怎么来的呢,有这么一个定义:从支持数据处理操作的源生成的元素序列,流处理的是计算,集合处理的是存储
流的特点:
- 流水线:流->流->流,流的结果返回一个新的流,多个操作串联起来就可以达到流水线的效果。
- 内部迭代:
Collection API
for-each
属于外部迭代,作用是为了存储和访问数据。Stream API
属于内部迭代,完全是在API库内部进行的数据处理;主要是为了描述对数据的计算。
示例:
代码语言:javascript复制public static List<String> streamTreatment(List<Order> orders){
return orders.stream()
// 过滤金额大于100的元素
.filter(o -> o.getAmount() > 100)
//根据订单事件倒序
.sorted(comparing(Dish::getCreated).reversed())
//将订单信息转换成其他元素,通过lambda表达式提取购买人姓名
.map(Order::getPayer)
//截断流,指定元素个数不超过0
.limit(10)
//将流转换成集合List,汇总结果
.collect(toList());
}
2 流和集合的关系
相同点:
- 拥有有序迭代的接口,顺序获取元素。
- 只能遍历一次
不同点:
- 二者计算的时间点不同。集合中的数据是计算完成的才能加入集合,可以删除和新增;流中的元素来自于源,不能删除和新增,流的元素是实时按照用户的需求计算产生的,延迟了最终的集合创建的时间。
- 迭代方式不同。集合中提供的迭代接口属于外部迭代。需要开发人员自己for循环并处理其中具体的一些操作;流的迭代属于内部迭代,在
Stream
库内部做了迭代,并且接受一些操作指令帮你把活干了。特别是关于并行,如果是外部迭代,开发人员必须自己做各种并行控制,而Stream
流内部就天然做了并行化的支持,透明地实现并行。
3 流的操作
3.1 初级玩法
filter,map,limit
,这种可以连成一条流水线的流操作,叫做中间操作,这种中间操作不会执行产生任何处理,在终端处操作触发的时候一次性处理。- 像
collection
这种可以触发流水线执行并关闭它,叫做终端操作。终端操作会从流的流水线生成结果。 - 使用步骤:
- 一个数据源来执行查询
- 一串中间操作链,形成流的流水线
- 一个终端操作,执行流水线,并生成结果
示例:
代码语言:javascript复制//流式操作链
List<String> strs = Arrays.asList("12345", "123", "123456", "1234567","12345678");
strs.stream()
.filter(s -> s.length() > 3)
.mapToInt(Integer::parseInt)
.limit(3)
.forEach(System.out::println);
//观察一下流式操作链中的执行顺序
strs.stream()
.filter(s -> {
System.out.println("filter--->" s);
return s.length() > 3;
})
.mapToInt(s->{
System.out.println("mapToInt--->" s);
return Integer.parseInt(s);
})
.limit(3)
.forEach(s->{
System.out.println("forEach-->" s);
});
//执行结果 从结果可以知道,filter和mapToInt以及forEach是成双成对出现的,我们可以认为他们合并到了一次遍历中;并且整个集合中满足条件的不止3个,但根据结果只做了三次遍历打印,说明limit做了类似短路的操作。
/**
filter--->12345
mapToInt--->12345
forEach-->12345
filter--->123
filter--->123456
mapToInt--->123456
forEach-->123456
filter--->1234567
mapToInt--->1234567
forEach-->1234567
**/
3.2 花式玩法
3.2.1 筛选和切片
filter(T->boolean)
操作,接受一个Predicate
接口,并返回所有符合条件的元素的流。distinct()
去重操作,返回一个元素各异的流。主要通过集合中对象的hashCode和equals
方法来判断是否相等。limit(n)
截短操作,返回一个不超过给定长度的流。流有序的话,会顺序返回前N个元素,否则随机返回。skip(n)
跳过操作,跳过前n个元素的流,如果元素为空,则返回一个空流。
3.2.2 映射
map(T->R)
映射操作,将流中的每一个元素映射成一个新元素,转换成另一个对象。 ints.stream() .filter(i -> { System.out.println("filter--->" i); return i> 3; }) .map(i->{ System.out.println("map--->" i); return i*2; }) .limit(3) .distinct() .skip(1) .forEach(s->{ System.out.println("forEach-->" s); });flatMap(T->R)
扁平化操作,将流中的每一个值(特别是数组或者集合种的元素)转换成一个个流,并合并成一个流,多层嵌套瞬间拍平。 //flatMap的作用就是将流 List<Integer> ints = Arrays.asList(1,2,3); List<Integer> ints2 = Arrays.asList(3,4); final List<ArrayList> collect1 = ints.stream() .flatMap(int1 -> ints2.stream() .map(int2 -> new ArrayList() { { add(int1); add(int2); } })).collect(toList()); System.out.println(collect1); // 其中的flatMap如果换成map操作的话,那么整个流式计算的结果就会是Stream<ArrayList> final List<Stream<ArrayList>> collect2 = ints.stream() .map(int1 -> ints2.stream() .map(int2 -> new ArrayList() { { add(int1); add(int2); } })).collect(toList()); final List<String> collect = strs.stream() .map(s -> s.split("")) .flatMap(Arrays::stream) .distinct() .collect(toList());
3.2.3 匹配&查找
anyMatch(Tall->boolean)
匹配流中是否有一个符合要求的元素,如果存在返回true,这是一个终端操作。allMatch(T->boolean)
匹配流中所有元素是否都匹配条件,如果全部满足条件返回true,这是一个终端操作。noneMatch(T->boolean)
匹配流中所有元素是否都匹配条件,如果全部满足条件返回true,这是一个终端操作。findAny()
,结合filter
使用,一旦找到任意一个就直接返回,返回值是Option<T>
。Option<T>
是一种容器类,代表一个值存在还是不存在,这样子就把臭名昭著的Null给包了起来。findFrist()
,结合filter
使用,对于一些顺序的流,直接找到第一个就返回,返回值是Option<T>
。 final Optional<String> any = strs.stream() .filter(s -> s.length() > 3) .findAny(); System.out.println(any); final Optional<String> first = strs.stream() .filter(s -> s.length() > 3) .findFirst(); System.out.println(first);
3.2.4 归约
定义:将流中的所有元素结合计算得到一个值,将流归约成一个值。
reduce(T,T,U->R)
,传入一个初始值,传入一个计算表达式 //元素求和 List<Integer> ints = Arrays.asList(1,2,3); final Integer reduce = ints.stream().reduce(2, (a, b) -> a b); System.out.println(reduce); //考虑如果流中没有元素的话,用optional可以包裹一下 final Optional<Integer> reduce = ints.stream().reduce((a, b) -> a b); System.out.println(reduce); //元素求最大值和最小值 final Optional<Integer> reduce2 = ints.stream().reduce(Integer::max); System.out.println(reduce2); final Optional<Integer> reduce2 = ints.stream().reduce(Integer::min); System.out.println(reduce2); //map-reduce求流中的元素个数 final Optional<Integer> reduce3 = ints.stream().map(i->1).reduce(Integer::sum); System.out.println(reduce3); //内部api 求流中的元素个数 long count= ints.stream().count(); System.out.println(count);
3.2.5 流操作的状态
有状态:比如filter
,map
,从输入流中获取每一个元素并在输出流中输出0或者1个结果,这种没有内部状态。这种就叫做无状态操作
无状态:比如reduce
,max
,min
,需要内部状态来累积结果,但是内部的状态是有界的;再比如sort
,distinct
操作属于中间操作会输出新的流,并且内部状态是无界的,因为需要将中间计算的元素放在内存中存储,给后来的计算使用。这种就叫做有状态操作
操作 | 类型 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream<T> | Predicate<T> | T->boolean |
distinct(有状态-无界) | 中间 | Stream<T> | ||
skip(有状态-有界) | 中间 | Stream<T> | long | |
limit(有状态-有界) | 中间 | Stream<T> | long | |
map | 中间 | Stream<R> | Function<T,R> | T->R |
flatMap | 中间 | Stream<R> | Function<T,Stream<R>> | T->Stream<R> |
sorted(有状态-无界) | 中间 | Stream<T> | Comparator<T> | (T,T)->int |
anyMatch | 终端 | boolean | Predicate<T> | T->boolean |
noneMatch | 终端 | boolean | Predicate<T> | T->boolean |
allMatch | 终端 | boolean | Predicate<T> | T->boolean |
findAny | 终端 | Optional<T> | ||
findFirst | 终端 | Optional<T> | ||
forEach | 终端 | void | Consumer<T> | T->void |
collect | 终端 | R | Collector<T,A,R> | |
reduce(有状态-有界) | 终端 | Optional<T> | BinaryOperator<T> | (T,T)->T |
count | 终端 | long |
3.2.6 数值流
为了减少装箱拆箱的损耗,提供了mapToInt,mapToDouble,mapToLong
三个方法转换成IntStream,DoubleStream,LongStream
,并且这些数值流还提供了很多方便的计算方法。
final IntStream intStream = strs.stream().mapToInt(s -> Integer.parseInt(s));
System.out.println(intStream.sum());
//转换回对象流
final Stream<Integer> boxed = intStream.boxed();
final DoubleStream doubleStream = strs1.stream().mapToDouble(s -> Double.parseDouble(s));
System.out.println(doubleStream.sum());
//转换回对象流
final Stream<Double> boxed1 = doubleStream.boxed();
final LongStream longStream = strs2.stream().mapToLong(s -> Long.parseLong(s));
System.out.println(longStream.sum());
//数值流Optional
final OptionalLong max = longStream.max();
//获取最大值,如果不存在就用默认值10
final long l = max.orElse(10);
//转换回对象流
final Stream<Long> boxed2 = longStream.boxed();
示例:生成勾股定理的流
代码语言:javascript复制final Stream<int[]> stream1 = IntStream.rangeClosed(1, 100).boxed()
.flatMap(a -> IntStream.rangeClosed(a, 100)
.filter(b -> Math.sqrt(a * a b * b) % 1 == 0)
.mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a b * b)})
);
stream1.forEach(arr-> System.out.println(arr[0] "---" arr[1] "-----" arr[2]));
/**
3---4-----5
5---12-----13
6---8-----10
7---24-----25
...
**/
3.3 生成流的几种做法
- 根据值生成流 // Stream.of Stream<String> stream = Stream.of("one", "two", "three", "four"); stream.map(String::toUpperCase).forEach(System.out::println);
- 根据数组生成流 int[] numbers = {2, 3, 5, 7, 11, 13}; System.out.println(Arrays.stream(numbers).sum());
- 根据文件流生成流 long uniqueWords = Files.lines(Paths.get("data.txt"), Charset.defaultCharset()) .flatMap(line -> Arrays.stream(line.split(" "))) .distinct() .count(); System.out.println("There are " uniqueWords " unique words in data.txt");
- 根据函数生成流(无限流) // 生成斐波那契数列数列,如果不用截断,那这个流会无限生成下去,接受初始化值和T->T Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1],t[0] t[1]}) .limit(10) .forEach(t -> System.out.println("(" t[0] ", " t[1] ")")); // 生成无限流,和iterate的区别是它接受的是()->T Stream.generate(Math::random) .limit(10) .forEach(System.out::println);
4 高级归约
收集器Collector
对流中的元素触发一个归约操作,最常见的一个恒等转换stream.collect(Collectors.toList())
,其中借助了Collectors中的静态工厂方法。
4.1 现成的收集器
常见的一些归约汇总的代码如下:
代码语言:javascript复制// 累加数量
final Long collect = bookCorner.stream().collect(counting());
System.out.println(collect);
// 取流中元素 排序之后的最大值
final Optional<Book> collect1 = bookCorner.stream().collect(maxBy(Comparator.comparingDouble(Book::getPrice)));
if (collect1.isPresent()){
System.out.println(collect1.get());
}
// 取流中元素 排序之后的最小值
final Optional<Book> collect2 = bookCorner.stream().collect(minBy(Comparator.comparingDouble(Book::getPrice)));
if (collect1.isPresent()){
System.out.println(collect2.get());
}
// 取平均数
final Double collect3 = bookCorner.stream().collect(averagingDouble(Book::getPrice));
System.out.println(collect3);
// 累加求和
final Double collect4 = bookCorner.stream().collect(summingDouble(Book::getPrice));
System.out.println(collect4);
//字符串拼接
final String collect5 = bookCorner.stream().map(Book::getName).collect(joining(", "));
System.out.println(collect5);
4.2 reducing也能做这些事儿
除了使用现成的一些收集器方法之外,Collectors.reducing
也能做到归约和汇总。接下来实现一下上文的几种便捷操作的几种替代写法。
// 累加数量
final Integer collect6 = bookCorner.stream().collect(reducing(0, price -> 1, (i, j) -> i j));
System.out.println(collect6);
// 取流中元素 排序之后的最大值
final Optional<Double> collect7 = bookCorner.stream().map(Book::getPrice).collect(reducing((p1, p2) -> p1 - p2 > 0 ? p1 : p2));
if (collect7.isPresent()){
System.out.println(collect7.get());
}
// 取流中元素 排序之后的最小值
final Optional<Double> collect8 = bookCorner.stream().map(Book::getPrice).collect(reducing((p1, p2) -> p1 - p2 > 0 ? p2 : p1));
if (collect8.isPresent()){
System.out.println(collect8.get());
}
// 取平均数
// 取平均数
final Double collect3 = bookCorner.stream().collect(averagingDouble(Book::getPrice));
System.out.println(collect3);
// 累加求和
final Double collect9 = bookCorner.stream().collect(reducing(0d,Book::getPrice,Double::sum));
System.out.println(collect9);
//字符串拼接
final String collect10 = bookCorner.stream().collect(reducing("", Book::getName, (s1, s2) -> s1.concat(s2)));
System.out.println(collect10);
4.3 分组操作
使用Collectors.grouping
来进行分组操作,入参中的Function
就是分组函数,通过他去提取分组依据,并将流中的元素分成不同的组,返回值是<K,List<V>>
,K
为分组函数的返回值,List
中装的是各自满足要求的元素。
final Map<Book.Type, List<Book>> collect11 = bookCorner.stream().collect(groupingBy(Book::getType));
System.out.println(collect11);
还可以实现多级分组,通过groupingBy
中的另一个参数可以选择二级分组的规则,并且最终会嵌套在一级分组之内
final Map<Book.Type, Map<String, List<Book>>> collect12 = bookCorner.stream()
.collect(groupingBy(Book::getType, groupingBy(book -> {
if (book.getPrice() > 60d) return "Expensive";
else return "cheap";
})));
System.out.println(collect12);
通过groupingBy
中的另一个参数定制化分组的返回值V
,就像sql语句中的select count(field) from table group field
//返回每组的数量
final Map<Book.Type, Long> collect14 = bookCorner.stream().collect(groupingBy(Book::getType, counting()));
System.out.println(collect14);
//每组最大值
/**
{MATH=Optional[book2], CHINESE=Optional[book1], ENGLISH=Optional[book3]}
**/
final Map<Book.Type, Optional<Book>> collect15 = bookCorner.stream().collect(groupingBy(Book::getType, maxBy(Comparator.comparingDouble(Book::getPrice))));
System.out.println(collect15);
//通过collectingAndThen将收集器的结果转化为另一种类型
/**
{MATH=book2, CHINESE=book1, ENGLISH=book3}
**/
final Map<Book.Type, Book> collect16 = bookCorner.stream()
.collect(groupingBy(Book::getType,
collectingAndThen(maxBy(Comparator.comparingDouble(Book::getPrice)), Optional::get)));
System.out.println(collect16);
配合mapping
收集器玩一些小花样,mapping(Function,Collector)
中Function用来对结果进行转换的函数,Collector将变换的结果收集起来。
//{MATH=[cheap, Expensive], CHINESE=[cheap, Expensive], ENGLISH=[cheap, Expensive]}
final Map<Book.Type, Set<String>> collect17 = bookCorner.stream()
.collect(groupingBy(Book::getType, mapping(book -> {
if (book.getPrice() > 60d) return "Expensive";
else return "cheap";
}, toSet())));
System.out.println(collect17);
4.4 分区-特殊的分组
一种特殊的分组,partitioningBy(T->boolean)
,分区函数返回的是布尔值,所以最终的结果就是Map<Boolean,List<T>>
// {false=[book6], true=[book1, book2, book3, book4, book5, book7, book8, book9]}
final Map<Boolean, List<Book>> collect = bookCorner.stream().collect(partitioningBy(book -> book.getPrice() > 30));
System.out.println(collect);
partitioningBy
同样也可以和groupingBy
配合一起使用,通过groupingBy
收集器会在Boolean作为key的Map中继续生成一个二级map
//{false={MATH=[book6]}, true={ENGLISH=[book3, book4, book9], CHINESE=[book1, book5, book7, book8], MATH=[book2]}}
final Map<Boolean, Map<Book.Type, List<Book>>> collect1 = bookCorner.stream().collect(partitioningBy(book -> book.getPrice() > 30, groupingBy(Book::getType)));
System.out.println(collect1);
综上,总结一下,现有的一些收集器类型:
工厂方法 | 返回类型 | 用于 |
---|---|---|
tolist使用示例:ListBookes = menuStream.collect(toList()); | List | 把流中所有项目收集到一个List |
toSet使用示例:SetBookes = menuStream.collect(toSet()); | Set | 把流中所有项目收集到一个set,删除重复项 |
toCollection使用示例:CollectionBookes=menuStream.collect(toCollection(),ArrayList::new); | Collection | 把流中所有项目收集到给定的供应源创建的集合 |
counting使用示例:long howManyBookes = menuStream.collect(counting()); | Long | 计算流中元素的个数 |
summingInt示例:int totalPrices = menuStream.collect(summingInt(Book::getPrice)); | Integer | 对流中项目的一个整数属性求和 |
averagingInt使用示例:double avgPrices = menuStream.collect(averagingInt(Book::getPrice)); | Double | 计算流中项目Integer属性的平均值 |
summarizingInt使用示例:IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Book:getPrice)); | IntSummaryStatistics | 收集关于流中项目Inteaer属性的统计值,例如最大、最小、总和与平均值 |
joining使用示例:String shortMenu = menuStream.map(Book ::getName).collect(joining(",")); | String | 连接对流中每个项目调用toString 方法所生成的字符串 |
maxBy使用示例:Optional<Book> fattest = menuStream.collect(maxBy(comparinqInt(Book::getPrice))); | Optional<T> | 一个包裹了流中按照给定比较器选出的最大元素的Optional.或如果流为空则为Optional.empty() |
minBy使用示例:Optional<Book> lightest = menuStream.collect(minBy(comparingInt(Book::getPrice))); | Optional<T> | 一个包裹了流中按照给定比较器选出的最小元素的Optional或如果流为空则为Optional.empty() |
reducing使用示例:int totalPrices = menuStream.collect(reducing(0,Book::getPrice,Integer::sum)); | 归约操作产生的类型 | 从一个作为累加器的初始值开始,利用BinaryOperator与流中的元素逐个结合,从而将流归约为单个值 |
collectingAndThen使用示例:int howManyBookes = menuStream.collect(collectingAndThen(toList(),List::size)); | 转换函数返回的类型 | 包裹另一个收集器,对其结果应用转换函数 |
groupingBy使用示例:Map> BookesByType = menuStream.collect(groupingBy(Book::getType)); | Map<K,List<T>> | 根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果Map的键 |
partitioningBy使用示例:Map<Boolean.List<Book> englishBookes =menuStream.collect(partitioningBy(Book::isEnglish)); | Map<Boolean,List<T>> | 根据对流中每个项目应用谓词的结果来对项目进行分区 |
5 Collector接口
代码语言:javascript复制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是收集操作得到的对象的类型
- 比如toList(),他构造的收集器就如下所示 Collector<T, ?, List<T>> toList() { return new CollectorImpl<>(ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }, CH_ID); }
再来看看接口中的五个方法
- 建立新的结果容器,
supplier()
:返回值是一个()->T
,调用时会创建一个空的累加器实例。 - 将元素添加到结果容器,
accumulator()
:返回值是一个(T,U)->void
,方法返回执行归约操作的函数,假设流中的元素有n个,当遍历到流中的第n个元素时,这个函数需要保存归约结果的累加器(已经收集了前n-1个项目)以及第n元素本身俩个参数去执行。 - 对结果容器应用最终转换,
finisher()
:返回值是一个T->R
,方法返回累积过程中最后要调用的函数。当流中所有的元素都遍历完了之后,通过该函数将累加器对象转换为整个集合操作最终的结果。 - 合并两个结果容器,
combiner()
:返回值是一个(T,T)->T
,调用时会返回一个供归约操作使用的函数。定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。 - 定义收集器的行为,
characteristics()
:返回一个不可变的Characteristics集合。定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举。UNORDERED
——归约结果不受流中项目的遍历和累积顺序的影响。CONCURRENT
——accumulator
函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED
,那它仅在用于无序数据源时才可以并行归约。IDENTITY_FINISH
——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。