java8--Stream流

转载自:冰橘柠檬
https://blog.csdn.net/weixin_39124328/article/details/83065123

为什么需要 Stream
Java8中的stream与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念,也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream,它是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。

Stream的特性
在集合和其他数据集上运行函数式查询。Stream可以认为是一个高级版本的Iterator,它代表着数据流,流中的数据元素的数量可以是有限的,也可以是无限的。其差别在于:
  无存储:Stream是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
  函数式编程:对Stream的任何修改都不会修改背后的数据源,比如对Sream的filter操作并不会删除被过滤掉的元素,而是生成一个不含被过滤元素的新的Stream。
  延迟执行:Stream由一个或多个中间操作(intermediate operation)和一个结束操作(terminal operation)两部分组成。只有执行了结束操作,Stream定义的中间操作才会依次执行,这就是Stream的延迟特性。
  可消费性:Stream只能被“消费”一次,一旦遍历过就会失效。就像容器的迭代器那样,想要再次遍历必须重新生成一个新的Stream。

流管道剖析
JDK 中的流来源

方法 描述
Collection.stream() 使用一个集合的元素创建一个流
Stream.of(T…) 使用传递给工厂方法的参数创建一个流
Stream.of(T[]) 使用一个数组的元素创建一个流

中间流操作

操作 描述
filter(Predicate) 与预期匹配的流的元素(过滤流中的元素)
map(Function<T, U>) 将提供的函数应用于流的元素的结果
flatMap(Function<T, Stream> 将提供的流处理函数应用于流元素后获得的流元素
distinct() 去除流中的重复元素(使用元素的equal方法判断)
sorted() 按自然顺序排序的流元素
Sorted(Comparator) 按提供的比较符排序的流元素
limit(long) 截断至所提供长度的流元素
skip(long) 去除流中的重复元素(使用元素的equal方法判断)
takeWhile(Predicate) (仅限 Java 9)在第一个提供的预期不是 true 的元素处截断的流元素
dropWhile(Predicate) (仅限 Java 9)丢弃了所提供的预期为 true 的初始元素分段的流元素

中间操作始终是惰性的:调用中间操作只会设置流管道的下一个阶段,不会启动任何操作。重建操作可进一步划分为无状态 和有状态 操作。无状态操作(比如 filter() 或 map())可独立处理每个元素,而有状态操作(比如 sorted() 或 distinct())可合并以前看到的影响其他元素处理的元素状态。

终止流操作

操作 描述
forEach(Consumer action) 将提供的操作应用于流的每个元素。
toArray() 使用流的元素创建一个数组。
reduce(…) 将流的元素聚合为一个汇总值。
collect(…) 将流的元素聚合到一个汇总结果容器中。
min(Comparator) 通过比较符返回流的最小元素。
max(Comparator) 通过比较符返回流的最大元素。
count() 返回流的大小。
{any,all,none}Match(Predicate) 返回流的任何/所有元素是否与提供的预期相匹配。
findFirst() 返回流的第一个元素(如果有)。
findAny() 返回流的任何元素(如果有)。

附加信息
大多数流操作都要求传递给它们的拉姆达表达是互不干扰和无状态的。互不干扰意味着它们不会修改流来源;无状态意味着它们不会访问(读或写)任何可能在流操作寿命内改变的状态。对于缩减操作(例如计算 sum、min 或 max 等汇总数据),传递给这些操作的拉姆达表达式必须是具有结合性的(或遵守类似的要求)。

如下使用有状态拉姆达表达式的流管道(不要这么做!)

HashSet<Integer> twiceSeen = new HashSet<>();
int[] result
    = elements.stream()
              .filter(e -> {
                  twiceSeen.add(e * 2);
                  return twiceSeen.contains(e);
              })
              .toArray();

如果并行执行,此管道会生成错误的结果,原因有两个。首先,对 twiceSeen 集的访问是从多个线程进行的,没有进行任何协调,因此不是线程安全的。第二,因为数据被分区了,所以无法确保在处理给定元素时已经处理了该元素前面的所有元素。

聚合操作
累加器反模式
示例一: 使用 Streams 声明性地计算聚合值


int totalSalesFromNY
    = txns.stream()
          .filter(t -> t.getSeller().getAddr().getState().equals("NY"))
          .mapToInt(t -> t.getAmount())
          .sum();

示例二:用传统方法计算聚合值

int sum = 0;
for (Txn t : txns) {
    if (t.getSeller().getAddr().getState().equals("NY"))
        sum += t.getAmount();
}

对比:
lambda表达式的优点:代码更清晰,简单地被构造为一些简单操作的组合;通过声明进行表达;随着声明表达条件的复杂化,可以干净的拓展。
传统方式缺点:示例二为累加器反模式,首先声明并初始化一个可变累加器变量 (sum),然后继续在循环中更新累加器。为什么这样做是不正确的?首先,此代码样式难以并行化,没有协调(比如同步),对累加器的每次访问都导致一次数据争用;另一个原因是,它在太低的级别上建模计算 — 在各个元素的级别上,而不是在整个数据集的级别上。与 “逐个依次迭代交易金额,将每笔金额添加到一个已初始化为 0 的累加器” 相比,“所有交易金额的总和” 是目标的更抽象、更直接的陈述。

缩减
缩减(也称为折叠)是一种来自函数编程的技术,它抽象化了许多不同的累加操作。给定一个类型为 T,包含 x 个元素的非空数列 X1, x2, …, xn 和 T 上的二元运算符(在这里表示为 *), T下的 X 的缩减 被定义为:

(x1 * x2 * …* xn)

当使用普通的加法作为二元运算符来应用于某个数列时,缩减就是求和。但其他许多操作也可以使用缩减来描述。如果二元运算符是 “获取两个元素中较大的一个”(这在 Java 中可以使用拉姆达表达式 (x,y) -> Math.max(x,y) 来表示,或者更简单地表示为方法引用 Math::max),则缩减对应于查找最大值。

通过将累加描述为缩减,而不使用累加器反模式,可以采用更抽象、更紧凑、更并行化 的方式来描述计算 — 只要您的二元运算符满足一个简单条件:结合性。回想一下,如果 a、b 和 c 元素满足以下条件,二元运算符 * 就是结合性的:

((a * b) * c) = (a * (b * c))
结合性意味着分组无关紧要。如果二元运算符是结合性的,那么可以按照任何顺序安全地执行缩减。在顺序执行中,执行的自然顺序是从左向右;在并行执行中,数据划分为分段,分别缩减每个分段,然后组合结果。结合性可确保这两种方法得到相同的答案。如果将结合性的定义扩展到 4 项,可能更容易理解:

(((a * b) * c) * d) = ((a * b) * (c * d))

左侧对应于典型的顺序计算;右侧对应于表示典型的并行执行的分区执行,其中输入序列被分解为几部分,各部分并行缩减,并使用 * 将各部分的结果组合起来。(或许令人惊奇的是,* 不需要是可交换的,但许多运算符通常都可用于缩减,比如相加和求最大值等。具有结合性但没有可交换性的二元运算符的一个例子是字符串串联,如下所示。)

满足:((a * b) * c) = (a * (b * c))
但是不满足:((a * b) * c) =(c * (a * b) ) 

缩减方法

Optional<T> reduce(BinaryOperator<T> op)
T reduce(T identity, BinaryOperator<T> op)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

缩减不需要仅应用于整数和字符串,它可以应用于您想要将元素序列缩减为该类型的单个元素的任何情形。例如,您可以通过缩减来计算最高的人:

Comparator<Person> byHeight = Comparators.comparingInt(Person::getHeight);
BinaryOperator<Person> tallerOf = BinaryOperator.maxBy(byHeight);
Optional<Person> tallest = people.stream().reduce(tallerOf);

可变缩减
缩减获取一个值序列并将它缩减为单个值,比如数列的和或它的最大值。但是有时您不想要单个汇总值;您想将结果组织为类似 List 或 Map 的数据结构,或者将它缩减为多个汇总值。在这种情况下,您应该使用缩减 的可变类似方法,也称为收集。

累加器方式:

ArrayList<String> list = new ArrayList<>();
for (Person p : people)
    list.add(p.toString());

当累加器变量是一个简单值时,缩减是累加的更好替代方法,与此类似,在累加器结果是更复杂的数据结构时,也有一种更好的替代方法–可变缩减。

可变缩减类似的方法:
  一种生成空结果容器的途径
  一种将新元素合并到结果容器中的途径
  一种合并两个结果容器的途径

这些构建块可以轻松地表达为函数。这些函数中的第 3 个支持并行执行可变缩减:您可以对数据集进行分区,为每一部分生成一个中间累加结果,然后合并中间结果。Streams 库有一个 collect() 方法,它接受以下 3 个函数:

<R> collect(Supplier<R> resultSupplier,
            BiConsumer<R, T> accumulator, 
            BiConsumer<R, R> combiner)

使用缩减来计算字符串串联,因为 Java 中的字符串是不可变的,而且串联要求复制整个字符串,所以它还有 O(n2) 运行时(一些字符串将复制多次)。您可以通过将结果收集到 StringBuilder 中,更高效地表达字符串串联:


    StringBuilder concat = strings.stream()
                              .collect(() -> new StringBuilder(),
                                       (sb, s) -> sb.append(s),
                                       (sb, sb2) -> sb.append(sb2));

//两代码相同


    StringBuilder concat = strings.stream()
                              .collect(StringBuilder::new,
                                       StringBuilder::append,
                                       StringBuilder::append);

收集器

List<String> list = Stream.of("hello", "world", "hello", "java")
						  .collect(Collectors.toList()); //收集结果为list集合

常见收集器

收集器 行为
toList() 将元素收集到一个 List 中
toSet() 将元素收集到一个 Set 中
reducing() 向元素应用缩减(通常用作下游收集器,比如用于 groupingBy)(各种版本)
minBy(BinaryOperator) 计算元素的最小值(与 maxBy() 相同)
counting() 计算元素数量。(通常用作下游收集器)
groupingBy(Function<T,U>) 将元素分组到一个 Map 中,其中的键是所提供的应用于流元素的函数,值是共享该键的元素列表

收集器的应用
将数据收集用到Collector抽象中的语法上更简单,但有时你会有复杂的结果汇总,如groupingBy() 收集器,例如,要创建超过 1000 美元的交易的 Map,可以使用卖家作为键:

Map<Seller, List<Txn>> bigTxnsBySeller =
    txns.stream()
        .filter(t -> t.getAmount() > 1000)
        .collect(groupingBy(Txn::getSeller));

假设不想要每个卖家的交易 List,而想要来自每个卖家的最大交易,可以这样:

Map<Seller, Txn> biggestTxnBySeller =
    txns.stream()
        .collect(groupingBy(Txn::getSeller,
                            maxBy(comparing(Txn::getAmount))));

如果不想要该卖家的最大交易,而想要总和,可以使用 summingInt() 收集器:

Map<Seller, Integer> salesBySeller =
    txns.stream()
         .collect(groupingBy(Txn::getSeller,
                            summingInt(Txn::getAmount)));

要获得多级汇总结果,比如每个区域和卖家的销售,可以使用另一个 groupingBy 收集器作为下游收集器:

Map<Region, Map<Seller, Integer>> salesByRegionAndSeller =
    txns.stream()
        .collect(groupingBy(Txn::getRegion,
                            groupingBy(Txn::getSeller, 
                                       summingInt(Txn::getAmount))));

要计算一个文档中的词频直方图,可以使用 BufferedReader.lines() 将文档拆分为行,使用 Pattern.splitAsStream() 将它分解为一个单词流,然后使用 collect() 和 groupingBy() 创建一个 Map,后者的键是单词,值是这些单词的数量:

Pattern whitespace = Pattern.compile("\\s+");
Map<String, Integer> wordFrequencies =
    reader.lines()//获取stream
          .flatMap(s -> whitespace.splitAsStream())
          .collect(groupingBy(String::toLowerCase),
                              Collectors.counting());

Stream总结:
■ 不是数据结构
它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
■ 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
■ 所有 Stream 的操作必须以 lambda 表达式为参数
■ 不支持索引访问
你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。不过请参阅下一项。
■ 但是可以很容易生成数组或者 List
■ 惰性化
中间操作 Stream 是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始,就是当遇到末端操作时才会开始进行中间操作。Intermediate 操作永远是惰性化的。
■ 并行能力
当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
■ Stream可以是无限的
集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting(短路) 操作可以对无限的 Stream 进行运算并很快完成。

猜你喜欢

转载自blog.csdn.net/flydoging/article/details/84660133