java8-Stream Api流详解

2023-03-06 21:46:52 浏览数 (2)

java8-Stream Api流详解

1 Stream API的意义

流的定义:通俗地说,不用再写循环,判断等细节实现的代码,直接以声明式方式编写你的意图。节省了大量的中间容器去存储临时的中间变量。那流怎么来的呢,有这么一个定义:从支持数据处理操作的源生成的元素序列,流处理的是计算,集合处理的是存储

流的特点

  1. 流水线:流->流->流,流的结果返回一个新的流,多个操作串联起来就可以达到流水线的效果。
  2. 内部迭代: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 流和集合的关系

相同点:

  1. 拥有有序迭代的接口,顺序获取元素。
  2. 只能遍历一次

不同点:

  1. 二者计算的时间点不同。集合中的数据是计算完成的才能加入集合,可以删除和新增;流中的元素来自于源,不能删除和新增,流的元素是实时按照用户的需求计算产生的,延迟了最终的集合创建的时间。
  2. 迭代方式不同。集合中提供的迭代接口属于外部迭代。需要开发人员自己for循环并处理其中具体的一些操作;流的迭代属于内部迭代,在Stream库内部做了迭代,并且接受一些操作指令帮你把活干了。特别是关于并行,如果是外部迭代,开发人员必须自己做各种并行控制,而Stream流内部就天然做了并行化的支持,透明地实现并行。
3 流的操作
3.1 初级玩法
  • filter,map,limit,这种可以连成一条流水线的流操作,叫做中间操作,这种中间操作不会执行产生任何处理,在终端处操作触发的时候一次性处理。
  • collection这种可以触发流水线执行并关闭它,叫做终端操作。终端操作会从流的流水线生成结果。
  • 使用步骤:
    1. 一个数据源来执行查询
    2. 一串中间操作链,形成流的流水线
    3. 一个终端操作,执行流水线,并生成结果

示例

代码语言: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 流操作的状态

有状态:比如filtermap,从输入流中获取每一个元素并在输出流中输出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,并且这些数值流还提供了很多方便的计算方法。

代码语言:javascript复制
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 生成流的几种做法
  1. 根据值生成流 // Stream.of Stream<String> stream = Stream.of("one", "two", "three", "four"); stream.map(String::toUpperCase).forEach(System.out::println);
  2. 根据数组生成流 int[] numbers = {2, 3, 5, 7, 11, 13}; System.out.println(Arrays.stream(numbers).sum());
  3. 根据文件流生成流 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");
  4. 根据函数生成流(无限流) // 生成斐波那契数列数列,如果不用截断,那这个流会无限生成下去,接受初始化值和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也能做到归约和汇总。接下来实现一下上文的几种便捷操作的几种替代写法。

代码语言:javascript复制
// 累加数量
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中装的是各自满足要求的元素。

代码语言:javascript复制
final Map<Book.Type, List<Book>> collect11 = bookCorner.stream().collect(groupingBy(Book::getType));
System.out.println(collect11);

还可以实现多级分组,通过groupingBy中的另一个参数可以选择二级分组的规则,并且最终会嵌套在一级分组之内

代码语言:javascript复制
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

代码语言:javascript复制
//返回每组的数量
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将变换的结果收集起来。

代码语言:javascript复制
//{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>>

代码语言:javascript复制
// {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

代码语言:javascript复制
//{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); }

再来看看接口中的五个方法

  1. 建立新的结果容器,supplier():返回值是一个()->T,调用时会创建一个空的累加器实例。
  2. 将元素添加到结果容器,accumulator():返回值是一个(T,U)->void,方法返回执行归约操作的函数,假设流中的元素有n个,当遍历到流中的第n个元素时,这个函数需要保存归约结果的累加器(已经收集了前n-1个项目)以及第n元素本身俩个参数去执行。
  3. 对结果容器应用最终转换,finisher():返回值是一个T->R,方法返回累积过程中最后要调用的函数。当流中所有的元素都遍历完了之后,通过该函数将累加器对象转换为整个集合操作最终的结果。
  4. 合并两个结果容器,combiner():返回值是一个(T,T)->T,调用时会返回一个供归约操作使用的函数。定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。
  5. 定义收集器的行为,characteristics():返回一个不可变的Characteristics集合。定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举。
    • UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。
    • CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。
    • IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。

0 人点赞