Stream API

掌握了Lambda表达式和方法引用之后,我们就可以很轻松的 Stream API 了,没有掌握Lambda表达式和方法引用的话,建议去看看我之前写的文章。因为接下来会大量的用到它们。

Stream

什么是Stream
Stream是一组用来处理数组、集合的API,它支持顺序流和并行流的聚合操作。

Stream特性

  • 不是数据结构,没有内部存储。
  • 不支持索引访问。
  • 延迟计算。
  • 支持并行。
  • 很容易生成数组或集合(ListSet)。
  • 支持过滤、查找、转换、汇总、聚合等操作。

备注:
1、在没有执行终止操作之前,它是不会进行计算的。

Stream运行机制

Stream 分为数据源,中间操作,终止操作。
数据源可以是一个数组、一个集合、一个生成器方法、一个I/O通道等等。
一个流可以有零个或者多个中间操作,每一个中间操作都会返回一个新的流,供下一个操作使用。一个流只会有一个终止操作。
Stream 只有遇到终止操作,它的数据源才开始执行遍历操作。

Stream常用的API

中间操作

  • 过滤 filter
  • 去重 distinct
  • 排序 sorted
  • 截取 limitskip
  • 转换 mapflatMap
  • 其他 peek

终止操作

  • 循环 forEach
  • 计算 minmaxcountaverage
  • 匹配 anyMatchallMatchnoneMatchfindFirstfindAny
  • 汇聚 reduce
  • 收集器 toArraycollect

Stream的创建

  • 通过数组来创建。
  • 通过集合来创建。
  • 通过 Stream.generate 方法来创建。
  • 通过 Stream.iterate 方法来创建。
  • 通过其他API来创建(文件、字符串等等)。

这里说明一下,Stream API 在Android使用的话,要求API 24 以上才能使用,也可以导入com.annimon:stream:$lastVersion库来使用它。
了解了一下基本的概念后,下面我们来看看Stream API 实战。

Stream实战

首先我们看看用代码如何来实现Stream的创建。
1、通过数组来创建。

public static<T> Stream<T> of(T... values) {
    ...
}

String[] arr = {"a", "b", "c", "1", "2"};
Stream<String> stream = Stream.of(arr);

使用Stream.of方法将数组作为参数传入,即可得到 Stream

2、通过集合来创建。

default Stream<E> stream() {
        ...
}

List<String> list = Arrays.asList("a", "b", "c", "1", "2");
Stream<String> stream = list.stream();

使用list.stream方法创建得到Stream

3、通过Stream.generate方法来创建。

public static<T> Stream<T> generate(Supplier<T> s) {
    ...
}

Stream<String> stream = Stream.generate(() -> 1);

使用Stream.generate方法创建得到Stream时,传入的是Supplier,它代表一个输出,说明调用Stream.generate方法会不断的输入一个值,放到Stream中。对于Stream.generate方法的使用,一般都会和一些中间操作的API搭配使用,比如使用 limit 截取一部分数据。

4、通过Stream.iterate方法来创建。

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
        ...
}

Stream<Integer> stream = Stream.iterate(1, x -> x + 1);

使用Stream.iterate方法创建得到Stream时,传入的是一个初始值和一个一元运算(输入和输出类型相同)的Lambda表达式。上面的操作解释为输入一个初始值为1,每次的输入结果为上一次输出的值加1,然后不然循环输入。

备注:Stream.generateStream.iterate 方法生成的是一个无止尽的 Stream ,我们在使用的时候需要注意这一点。

5、通过其他API来创建。

String str = "abcd";
IntStream stream = str.chars();

Stream<String> stream = Files.lines(Paths.get("path"));

我们也可以使用一些其他API自带的创建 Stream 的方法。

接下来看看 Stream 常用的API的使用。
1、中间操作符。

Stream<T> filter(Predicate<? super T> predicate);

Stream<T> distinct();

Stream<T> sorted();

Stream<T> limit(long maxSize);

Stream<T> skip(long n);

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

Stream<T> peek(Consumer<? super T> action);

S parallel();

S sequential();

2、终止操作符。

void forEach(Consumer<? super T> action);

Optional<T> min(Comparator<? super T> comparator);

Optional<T> max(Comparator<? super T> comparator);

long count();

OptionalDouble average();

boolean anyMatch(Predicate<? super T> predicate);

boolean allMatch(Predicate<? super T> predicate);

boolean noneMatch(Predicate<? super T> predicate);

Optional<T> findFirst();

Optional<T> findAny();

T reduce(T identity, BinaryOperator<T> accumulator);

Object[] toArray();

<R, A> R collect(Collector<? super T, A, R> collector);

看到这些方法是不是很眼熟???
没错,在讲Lambda表达式和函数式接口的时候,我们就列出过一些函数式接口的表格,我再把它贴出来吧,不是凑字数哦!

接口名 参数 返回值 用途 含义
Predicate T boolean 断言 代表一个输入
Consumer T void 消费 代表一个输入
Function<T,R> T R 函数 代表一个输入,一个输出(一般输入和输出是不同类型的)
BiFunction<T,U,R> (T,U) R 函数 代表两个输入,一个输出(一般输入和输出是不同类型的)
Supplier None T 工厂方法 代表一个输出
UnaryOperator T T 逻辑非、迭代器 代表一个输入,一个输出(输入和输出是相同类型的)
BinaryOperator (T,T) T 二元操作 代表两个输入,一个输出(输入和输出是相同类型的)

不知道有没有人和我一样,第一次学习Lambda表达式的时候,根本不理解这些函数式接口的用途代表什么意思。比如什么是“断言”,什么是“消费”等等。

所以,我们还是有必要了解一下这些概念的,这对于我们之后的学习和学术讨论很有帮助。

断言:其实是防止程序意外出错的一种宏,如果其参数计算为假,则程序发出警告,且退出。
最常见的用法就是在函数入口处保证输入参数的正确性。

assert(object != NULL) ;

消费:是指程序在运行过程中,资源的消耗和使用。
最常用的用法就是将元素传递给其他函数处理,并且不给与返回值。

stream.forEach(System.out::println)

函数:也称函数关系式,是指给定元素,经过一些行为、处理后得到其他的元素的关系表达,它的核心就是对应关系。
函数的用途就很广了,尤其是我们学习函数式编程之后,才能真正体会它的强大。

stream.map {
    x -> x.toString
}

工厂方法:我们知道工厂是生成东西的工具,那么我们是不是可以理解为工厂方法是生成方法的工具呢?是的,说明白一点就是提供一些生成对象的方法给我们使用,它本身是不创建东西的,我们只需要关心方法如何使用

stream.collect(Collectors.toList())

可以看到 Collectors 类含有很多生成其他对象的方法,比如 toCollection()toSet()toList()toMap() 等等。

逻辑非:顾名思义,就是取一个有效值的反值。
使用场景多用于输入和输出的类型相同。

Stream.iterate(1, x -> !x)

二元操作:即为输入两个参数和输出的类型相同,多用于合并 Stream 的场景。

stream.reduce((value1, value2) ->
    value1 + value2
)

了解这些函数式接口之后,我们在使用 Stream API 的时候就很容易了,接下来我们来看看这些操作符的用法吧。

filter 过滤

filter中的条件,返回true的才会保留

// 过滤基数值,保留偶数值,最后遍历
Arrays.asList(1, 2, 3, 4, 5).stream().filter(x -> x%2 == 2).forEach(System.out::println);

distince 去重

distinct是调用元素的hashCode和equals方法判断重复的值或者对象,从而实现去重功能。
对于实体类去重时,需要重写实体类的 equals() 以及 hashCode() 方法
也可以使用 filtercollect 操作符去重。

//去重,barry存在2个,去重后只有一个
Stream.of("wally", "barry", "rose", "barry").distinct().forEach(System.out::println);
// filter 实体类字段去重
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> seen.add(keyExtractor.apply(t));
}
stre.filter(distinctByKey(User::getName)).forEach(System.out::println);
// collect 实体类字段去重
stream.collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList::new)).forEach(System.out::println);

sorted 排序

默认按照自然顺序,从小到大排序。

Stream.of(1, 4, 8, 2, 0, -1).sorted().forEach(System.out::println);
// 倒序排序
Stream.of(1, 4, 8, 2, 0, -1).sorted((a, b) -> b - a).forEach(System.out::println);

limit 限制

限制取出 Stream 的个数。

Stream.iterate(1, x -> x + 1).limit(50).forEach(System.out::println);

skip 步长

跳过步长大小取值。

Stream.iterate(1, x -> x + 1).skip(5).limit(50).forEach(System.out::println);

map 数据转换

// 将Stream的每个元素进行大写转换
Stream.of("wally", "barry", "rose", "barry").map(String.toUpperCase).forEach(System.out::println);

flatMap 摊平

组合多个流为一个流

Stream.of(Arrays.asList("wally", "barry", "rose"), Arrays.asList("jack", "ben")).flatMap(names -> names.stream).forEach(System.out::println);

peek 查看

这个操作符主要目的是用于调试,查看 Stream 中的数据经过每个操作符后的状态。

Stream.of("wally", "barry", "rose", "barry")
    .peek(System.out::println)
    .map(String.toUpperCase)
    .peek(System.out::println)
    .forEach(System.out::println);

需要注意一点,peek是中间操作符,直接使用是无效的,不会打印任何东西。

parallel 并行

使用该操作符,会把当前 Stream 用并行方式处理。
设置并行 Stream 的线程数(包含主线程)。

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "5")

sequential 串行(默认)

使用该操作符,会把当前 Stream 用串行方式处理。

forEach 遍历

Stream.of("wally", "barry", "rose", "barry").forEach(System.out::println);

min 取最小

int min = Stream.of(1, 2, 3, 4, 5, 6).min(Integer::compareTo).orElse(-1);

max 取最大

int max = Stream.of(1, 2, 3, 4, 5, 6).max(Integer::compareTo).orElse(-1);

count 计数

统计元素个数

long count = Stream.of(1, 2, 3, 4, 5, 6).count();

average 取平均

取平均只有IntStream、DoubleStream、LongStream中可以使用,也可以使用collect取平均值

Double averagingValue = Stream.of(1, 2, 3, 4, 5, 6).collect(Collectors.averagingInt(value -> value));

anyMatch 找任意匹配条件的值或对象

判断,只要一个匹配,就返回true

// 匹配长度大于或者等于 5,只要有一个匹配,即使 rose 的长度为 4 不符合也返回 true。
boolean result = Stream.of("wally", "barry", "rose").anyMatch(s -> s.length() >= 5);

allMatch 找所有匹配条件的值或对象

判断,所有都匹配,才返回true

// 匹配长度大于或者等于 5,因为 rese 的长度 4,所以返回false
boolean result = Stream.of("wally", "barry", "rose").allMatch(s -> s.length() >= 5);

noneMatch 找不符合匹配条件的值或对象

// 匹配长度大于或者等于 5,因为 wally、barry 的长度符合,所以返回false
boolean result = Stream.of("wally", "barry", "rose").noneMatch(s -> s.length() >= 5);

findFirst 找第一个值或对象

Optional<Integer> first = Stream.of(1, 2, 3, 4, 5, 6).findFirst();

findAny 找任意的值或对象

Optional<Integer> any = Stream.of(1, 2, 3, 4, 5, 6).findAny();

reduce 累积、结合

根据一定的规则将 Stream 中的元素进行计算后返回一个唯一的值。
有三种方法可以调用,分别是:

  • Optional<T> reduce(BinaryOperator<T> accumulator);
  • T reduce(T identity, BinaryOperator<T> accumulator);
  • <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
    需要注意的是,在使用三个参数的方法时,如果 Stream 是非并行的,combiner 不会生效,计算过程和两个参数的方法是一致的。
// 方法一,二元运算处理,常用于求和、求最小值或最大值。
Optional<Integer> reduce = Stream.of(1, 2, 3, 4, 5, 6).reduce(Integer::sum);
// 方法二,在方法一的基础上,增加初始值。常用于将一个 String 类型的 Stream 中的所有元素连接到一起并在最前面添加 value 后返回。
String reduce = Stream.of("Hello", " ", "World").reduce("Log:", (s1, s2) -> s1+ s2);
// 方法三,并行时,将 identity 参数与序列中的每一个元素进行 accumulator 转换,得到N个结果,然后使用 combiner 对N个结果进行汇总。
// 使用函数表示就是:(4+1) * (4+2) * (4+3) = 210;
int reduce = Stream.of(1, 2, 3).reduce(4, (v1, v2) -> v1 + v2, (v1, v2) -> v1 * v2);

reduce即使是并行情况下,也不会创建多个初始化对象,combiner接收的两个参数永远是同一个对象。

// array1 == array2 永远为True
Predicate<String> predicate = t -> t.contains("a");
        ArrayList<String> list = Stream.of("aa", "ab", "c", "ad").parallel().reduce(new ArrayList<>(),
                (array, s) -> {
                    if (predicate.test(s)) array.add(s);
                    return array;
                },
                (array1, array2) -> {
                    System.out.println(array1 == array2);
                    array1.addAll(array2);
                    return array1;
                });

toArray 转换成数组

有两种方法可以调用,分别是:

  • Object[] toArray();
  • <A> A[] toArray(IntFunction<A[]> generator);
Integer[] array = Stream.of(1, 2, 3, 4, 5, 6).toArray(Integer[]::new);

collect 收集

collect含义与reduce有点相似,区别是reduce即使是并行情况下,也不会创建多个初始化对象,combiner接收的两个参数永远是同一个对象
有两种方法可以调用,分别是:

  • <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
  • <R, A> R collect(Collector<? super T, A, R> collector);
// 方法一,和 reduce 的三个参数方法用法类似。
Predicate<String> predicate = t -> t.contains("a");
ArrayList<String> list = Stream.of("aa", "ab", "c", "ad").parallel().collect(ArrayList::new,
    (array, s) -> {
        if (predicate.test(s)) array.add(s);
    },
    ArrayList::addAll);
// 使用 Collectors 提供的 API
stream.collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList::new)).forEach(System.out::println);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351