Java8新特性Stream API与Lambda表达式详解(1)

本文详细介绍了Java8中的Stream API和Lambda表达式,阐述了为何需要这两个新特性,以及它们如何提高编程效率和程序可读性。Stream API通过惰性求值和并行处理提供了高效的数据操作,而Lambda表达式则简化了函数式编程,使得代码更加紧凑和易于理解。文章深入探讨了Stream的构成、操作类型、数据源、惰性与并行性,以及Lambda表达式的背景、目标类型和上下文。
摘要由CSDN通过智能技术生成

1 为什么需要Stream与Lambda表达式?

1.1  为什么需要Stream

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

 

什么是聚合操作?

在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的聚合操作来完成诸如:

>客户每月平均消费金额

>最昂贵的在售商品

>本周完成的有效订单(排除了无效的)

>取十个数据样本作为首页推荐

这类的操作。

但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。而 Java 的集合 API 中,仅仅有极少量的辅助型方法,更多的时候是程序员需要用 Iterator 来遍历集合,完成相关的聚合应用逻辑。这是一种远不够高效、笨拙的方法。在 Java 7 中,如果要发现 type 为 grocery 的所有交易,然后返回以交易值降序排序好的交易 ID 集合,我们需要这样写:

//代码1 Java 7 的排序、取值实现
List<Transaction>groceryTransactions = new Arraylist<>();
for(Transactiont: transactions){
 if(t.getType() == Transaction.GROCERY){
 groceryTransactions.add(t);
 }
}
Collections.sort(groceryTransactions,new Comparator(){
 public int compare(Transaction t1, Transactiont2){
 return t2.getValue().compareTo(t1.getValue());
 }
});
List<Integer>transactionIds = new ArrayList<>();
for(Transaction t:groceryTransactions){
 transactionsIds.add(t.getId());
}

而在 Java 8 使用 Stream,代码更加简洁易读;而且使用并发模式,程序执行速度更快。

//代码2. Java 8 的排序、取值实现
List<Integer>transactionsIds = transactions.parallelStream().
 filter(t -> t.getType() ==Transaction.GROCERY).
 sorted(comparing(Transaction::getValue).reversed()).
 map(Transaction::getId).
 collect(toList());

1.2  为什么需要Lambda表达式

我们为什么需要Lambda表达式

   主要有三个原因:

  > 更加紧凑的代码

     比如Java中现有的匿名内部类以及监听器(listeners)和事件处理器(handlers)都显得很冗长

  > 修改方法的能力(我个人理解为代码注入,或者有点类似JavaScript中传一个回调函数给另外一个函数)

     比如Collection接口的contains方法,当且仅当传入的元素真正包含在集合中,才返回true。而假如我们想对一个字符串集合,传入一个字符串,只要这个字符串出现在集合中(忽略大小写)就返回true。

     简单地说,我们想要的是传入“一些我们自己的代码”到已有的方法中,已有的方法将会执行我们传入的代码。Lambda表达式能很好地支持这点

  > 更好地支持多核处理

     例如,通过Java 8新增的Lambda表达式,我们可以很方便地并行操作大集合,充分发挥多核CPU的潜能。

     并行处理函数如filter、map和reduce。

2 Stream与Lambda表达式总览

2.1 Stream总览

2.1.1 什么是流

Stream不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

Stream就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。Java 的并行 API 演变历程基本如下:

1.0-1.4中的 java.lang.Thread

5.0中的 java.util.concurrent

6.0中的 Phasers 等

7.0中的 Fork/Join 框架

8.0中的 Lambda

Stream的另外一大特点是,数据源本身可以是无限的。

2.1.2 流的构成

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

有多种方式生成 Stream Source:

从 Collection 和数组

  Collection.stream()

  Collection.parallelStream()

Arrays.stream(T array) or Stream.of()

从 BufferedReader

 java.io.BufferedReader.lines()

静态工厂

  java.util.stream.IntStream.range()

  java.nio.file.Files.walk()

自己构建

  java.util.Spliterator

其它

  Random.ints()

  BitSet.stream()

  Pattern.splitAsStream(java.lang.CharSequence)

  JarFile.stream()

流的操作类型分为两种:

Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

还有一种操作被称为 short-circuiting。用以指:

对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。

对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

//代码3. 一个流操作的示例
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
 .mapToInt(w -> w.getWeight())
 .sum();

stream()获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,最后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。

2.1.3 流和集合

集合和流尽管在表面上看起来很相似,但它们的设计目标是不同的:集合主要用来对其元素进行有效(effective)的管理和访问(access),而流并不支持对其元素进行直接操作或直接访问,而只支持通过声明式操作在其上进行运算然后得到结果。除此之外,流和集合还有一些其它不同:

无存储:流并不存储值;流的元素源自数据源(可能是某个数据结构、生成函数或I/O通道等等),通过一系列计算步骤得到;

天然的函数式风格(Functional in nature):对流的操作会产生一个结果,但流的数据源不会被修改;

惰性求值:多数流操作(包括过滤、映射、排序以及去重)都可以以惰性方式实现。这使得我们可以用一遍遍历完成整个流水线操作,并可以用短路操作提供更高效的实现;

无需上界(Bounds optional):不少问题都可以被表达为无限流(infinite stream):用户不停地读取流直到满意的结果出现为止(比如说,枚举完美数这个操作可以被表达为在所有整数上进行过滤)。集合是有限的,但流不是(操作无限流时我们必需使用短路操作,以确保操作可以在有限时间内完成);

从API的角度来看,流和集合完全互相独立,不过我们可以既把集合作为流的数据源(Collection拥有stream()和parallelStream()方法),也可以通过流产生一个集合(使用前例的collect()方法)。Collection以外的类型也可以作为stream的数据源,比如JDK中的BufferedReader、Random和BitSet已经被改造可以用做流的数据源,Arrays.stream()则产生给定数组的流视图。事实上,任何可以用Iterator描述的对象都可以成为流的数据源,如果有额外的信息(比如大小、是否有序等特性),库还可以进行进一步的优化。


集合上的流操作一般会生成一个新的值或集合。不过有时我们希望就地修改集合,所以我们为集合(例如Collection,List和Map)提供了一些新的方法,比如Iterable.forEach(Consumer),Collection.removeAll(Predicate),List.replaceAll(UnaryOperator),List.sort(Comparator)和Map.computeIfAbsent()。除此之外,ConcurrentMap中的一些非原子方法(例如replace和putIfAbsent)被提升到Map之中。

2.1.4 惰性(Laziness)

过滤和映射这样的操作既可以被急性求值(以filter为例,急性求值需要在方法返回前完成对所有元素的过滤),也可以被惰性求值(用Stream代表过滤结果,当且仅当需要时才进行过滤操作)在实际中进行惰性运算可以带来很多好处。比如说,如果我们进行惰性过滤,我们就可以把过滤和流水线里的其它操作混合在一起,从而不需要对数据进行多遍遍历。相类似的,如果我们在一个大型集合里搜索第一个满足某个条件的元素,我们可以在找到后直接停止,而不是继续处理整个集合。(这一点对无限数据源是很重要,惰性求值对于有限数据源起到的是优化作用,但对无限数据源起到的是决定作用,没有惰性求值,对无限数据源的操作将无法终止)

 

对于过滤和映射这样的操作,我们很自然的会把它当成是惰性求值操作,不过它们是否真的是惰性取决于它们的具体实现。另外,像sum()这样生成值的操作和forEach()这样产生副作用的操作都是“天然急性求值”,因为它们必须要产生具体的结果。

 

以下面的流水线为例:

int sum = shapes.stream()
                .filter(s -> s.getColor() ==BLUE)
                .mapToInt(s ->s.getWeight())
                .sum();

这里的过滤操作和映射操作是惰性的,这意味着在调用sum()之前,我们不会从数据源提取任何元素。在sum操作开始之后,我们把过滤、映射以及求和混合在对数据源的一遍遍历之中。这样可以大大减少维持中间结果所带来的开销。

 

大多数循环都可以用数据源(数组、集合、生成函数以及I/O管道)上的聚合操作来表示:进行一系列惰性操作(过滤和映射等操作),然后用一个急性求值操作(forEach,toArray和collect等操作)得到最终结果——例如过滤—映射—累积,过滤—映射—排序—遍历等组合操作。惰性操作一般被用来计算中间结果,这在Streams API设计中得到了很好的体现——与其让filter和map返回一个集合,我们选择让它们返回一个新的流。在Streams API中,返回流对象的操作都是惰性操作,而返回非流对象的操作(或者无返回值的操作,例如forEach())都是急性操作。绝大多数情况下,潜在的惰性操作会被用于聚合,这正是我们想要的——流水线中的每一轮操作都会接收输入流中的元素,进行转换,然后把转换结果传给下一轮操作。

 

在使用这种数据源—惰性操作—惰性操作—急性操作流水线时,流水线中的惰性几乎是不可见的,因为计算过程被夹在数据源和最终结果(或副作用操作)之间。这使得API的可用性和性能得到了改善。

 

对于anyMatch(Predicate)和findFirst()这些急性求值操作,我们可以使用短路(short-circuiting)来终止不必要的运算。以下面的流水线为例:

Optional<Shape> firstBlue =shapes.stream()
                                  .filter(s-> s.getColor() == BLUE)
                                  .findFirst();

由于过滤这一步是惰性的,findFirst在从其上游得到一个元素之后就会终止,这意味着我们只会处理这个元素及其之前的元素

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值