【编者按】在之前文章中,我们介绍了 Java 8和Scala的Lambda表达式对比。在本文,将进行 Hussachai Puripunpinyo Java 和 Scala 对比三部曲的第二部分,主要关注 Stream 和 Collection,本文由 OneAPM 工程师编译整理。

首先,为大家做一个简短的介绍,collection 是有限的数据集,而 stream 是数据的序列集,可以是有限的或无限的。

Streams API 是 Java 8 中新发布的 API,主要用于操作 collection 和 streaming 数据。Collections API 会改变数据集状态,而 Streams API 则不会。例如,当你调用Collections.sort(list)时,该方法会对传入的参数进行排序,而调用list.stream().sorted() 则会复制一份数据进行操作,保持原数据不变。你可以在这里获得更多关于 API 数据流的信息

以下是笔者从 Java 8 文档中摘出的 collections 和 streams 之间的比较。强烈建议大家阅读 完整版

Streams 和 collections 有以下几点区别:

  1. 无存储。steam 不是存储数据元素的数据结构。而是通过计算操作管道从源头传输数据元素。

2.本质是函数。对 Stream 对象操作能得到一个结果,但是不会修改原始数据。

  1. Laziness-seeking(延迟搜索):Stream 的很多操作如 filter、map、sort 和 duplicate removal(去重)可以延迟实现,意思是我们只要检查到满足要求的元素就可以返回。

  2. 可能是不受限制的:Streams 允许 Client 取足够多的元素直到满足某个条件为止。而 Collections 不能这么做。

  3. 消耗的。Steam 中的元素在 steam 生存期内只能被访问一次。

Java 和 Scala 都可以很简单地同时计算 collection 中的值。在 Java 中,你只需调用parallelStream()* 或者 stream().parallel(),而不是stream()。在 Scala 中,在调用其他方法之前,必须先调用 par()函数。而且可以通过添加 parallelism 来提高程序的性能。不幸的是,大多数时间它的执行速度都非常慢。事实上,parallelism 是一个很容易被误用的功能。 点这阅读这有趣的文章

在 JavaDoc 中, parallelStream()方法的介绍是:可能返回一个并行的 stream(collection作为数据源),所以它也可能返回一个串行 stream。(有人做过关于该API的研究

图像标题

Java 的 Stream API 是延后执行的。这意味着,没有指定一个终结操作(比如 collect() 方法调用),那么所有的中间调用(比如 filter 调用)是不会被执行的。延迟的流处理主要是为了优化 stream API 的执行效率。比如对一个数据流进行过滤、映射以及求和运算,通过使用延后机制,那么所有操作只要遍历一次,从而减少中间调用。同时,延后执行允许每个操作只处理必要的数据。相反,Scala 的 collections 是即时处理的。这样是否意味着,在测试中,Java Stream API始终优于 Scala ?如果只比较 Java 的 Stream API 和 Scala的 Collection API,那么Java Stream API 的确优于 Scala Collection API。但在 Scala 中有更多的选择。通过简单地调用toStream(),就可以将一个 Collection 转换成一个 Stream,或者可以使用 view (一种提供延后处理能力的 Collection)来处理数据集合。

下面粗略介绍下 Scala 的 Stream 和 View 特性

Scala 的 Stream

Scala 的 Stream 和 Java 的有所不同。在 Scala Stream 中,无需调用终结操作去取得Stream 的结果。Stream 是一个继承 AbstractseqLinearseqGenericTraversableTemplate trait的抽象类。所以,你可以把Stream当作 SEQ

如果你不熟悉 Scala,可以将 Seq 当作 Java 里的 List。(Scala 中的 List 不是一个接口)。

这里需知道 Streams 中的元素都是延迟计算的,正因为此,Stream能够计算无限数据流。如果要计算集合中的所有元素,Stream 和 List 有相同的性能。一旦计算出结果,数值将被缓存。 Stream 有一个 force 函数,能够强制评估 stream 再返回结果。注意,不要在无限流中调用该函数,也不要强制该 API 处理整个 stream 的操作,比如 size()、tolist()、foreach() 等,这些操作在 Scala 的 Stream 中都是隐式的。

在 Scala Stream 中实现 Fibonacci 数列。

def fibFrom(a: Int, b: Int): Stream[Int] = a #:: fibFrom(b, a + b)
val fib1 = fibFrom(0, 1) //0 1 1 2 3 5 8 …
val fib5 = fibFrom(0, 5) //0 5 5 10 15 …
//fib1.force //Don’t do this cause it will call the function infinitely and soon you will get the OutOfMemoryError
//fib1.size //Don’t do this too with the same reason as above.
fib1.take(10) //Do this. It will take the first 10 from the inifite Stream.
fib1.take(20).foreach(println(_)) //Prints 20 first numbers

:: 是 collection 中常用的连接数据的方法。而 #:: 表示是连接数据但是是延迟执行的(Scala中的方法名都很随意)。

Scala 的 View

再次重申,Scala 的 collection 是一个严格 collection,而 view 是非严格的。View 是基于一个基础 collection 的 collection,其中所有的转换都会延迟执行。通过调用 view 函数可以将严格 collection 转换成 view,也可以通过调用 force 方法转换回来。View 并不缓存结果,每次调用时才会执行转换。就像数据库的 View,但它是虚拟 collection。

创建一个数据集。

public class Pet {
public static enum Type {
CAT, DOG
}
public static enum Color {
BLACK, WHITE, BROWN, GREEN
}
private String name;
private Type type;
private LocalDate birthdate;
private Color color;
private int weight;
...
}

假设有一个宠物集,接下来会利用该集合详细说明。

过滤器

要求:从集合过滤一只胖乎乎的宠物,胖乎乎的定义是体重超过 50 磅,还想得到一个在 2013年1月1日出生的宠物名单。下面的代码片段显示了如何以不同的方式实现该滤波器的工作。

Java 方法1:传统方式

//Before Java 8
List<Pet> tmpList = new ArrayList<>();
for(Pet pet: pets){
if(pet.getBirthdate().isBefore(LocalDate.of(2013, Month.JANUARY, 1))
&& pet.getWeight() > 50){
tmpList.add(pet);
}
}

这种方式在命令式语言中十分常见。首先,必须创建一个临时集合,然后遍历所有元素,存储满足条件的元素到临时集中。的确有点绕口,但其结果和效率都非常不错。但本人不得不扫兴地说,传统方法比 Streams API 更快。不过,完全不用担心性能问题,因为代码的简洁比轻微的性能增益更重要。

Java 方法2:Streams API

//Java 8 - Stream
pets.stream()
.filter(pet -> pet.getBirthdate().isBefore(LocalDate.of(2013, Month.JANUARY, 1)))
.filter(pet -> pet.getWeight() > 50)
.collect(toList())

以上代码表示,使用 Streams API 过滤集合中的元素。之所以故意两次调用过滤函数,是想表明 Streams 的 API 设计就像一个 Builder pattern。在 Builder pattern 调用构建方法之前,可以将各种方法串联起来。在 Streams API 中,构建方法被称为终结操作,非终结操作的叫做中间操作。终结操作可能不同于构造函数,因为它在 Streams API 中只能被调用一次。但还有很多可使用的终结操作,比如 collect、count、min、max、iterator、toArray。这些操作会产生结果,而终端操作会消耗值,例如 forEach。那么,你认为传统方法和 Streams API 哪一个的可读性更强?

Java 方法3:Collections API

//Java 8 - Collection
pets.removeIf(pet -> !(pet.getBirthdate().isBefore(LocalDate.of(2013,Month.JANUARY, 1))
&& pet.getWeight() > 50));
//Applying De-Morgan's law.
pets.removeIf(pet -> pets.get(0).getBirthdate().toEpochDay() >= LocalDate.of(2013, Month.JANUARY, 1).toEpochDay()
|| pet.getWeight() <= 50);

这种方法是最简短的。但是,它修改了原始集合,而前面的方法不会。removeif 函数将Predicate(函数接口)作为参数。Predicate 是一个行为参数,它只有一个名为 test 抽象方法,只需要一个对象并返回布尔值。注意,这里必须使用“!”取反,或者可以应用 De Morgan 定理,使得代码看起来像二次声明。

Scala 方法:Collection、View和Stream

//Scala - strict collection
pets.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //List[Pet]
//Scala - non-strict collection
pets.views.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //SeqView[Pet]
//Scala - stream
pets.toStream.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //Stream[Pet]

Scala 的解决方案类似于 Java 的 Streams API。但首先,必须调用 view 函数把严格集转向非严格集,然后再用 tostream 函数把严格集转成一个 stream。

接下来直接上代码。

分组

通过元素的一个属性对起所在集合做 group。结果是 Map<T, List>,其中T是一个泛型类型。

要求:通过类型对宠物分组,诸如狗,猫等等。

注意:groupingBy 是 java.util.stream.Collectors 的静态的 helper method。

排序

根据属性对集合中的元素排序。结果会是任何类型的集合,根据配置来维持元素顺序。

要求:需按照类型、名字和颜色排序。

映射

将给定函数应用在集合元素中。根据定义的函数不同,其返回的结果类型也不同。

要求:需将宠物转化成字符串,以%s — name: %s, color: %s的格式。

寻找第一个

返回第一个能与指定 predicate 匹配的值。

要求:找一个名为Handsome的宠物。无论有多少个Handsome,只取第一个。

这个问题有点棘手。不知道你是否注意,在 Scala 中笔者所使用的是 find 函数而不是 filter ?如果用 filter 代替 find,它就会计算集合中所有元素,因为 scala collection 是严格的。但是,在 Java 的 Streams API 中你可以放心使用 filter,因为它会计算需要的第一个值,并不会计算所有元素。这就是延迟执行的好处!

接下来,向大家介绍 scala 中更多集合延迟执行的实例。我们假定 filter 总是返回 true,然后再取第二个值。将会是什么结果呢?

pets.filter { x => println(x.getName); true }.get(1) --- (1)

pets.toStream.filter { x => println(x.getName); true }.get(1) -- (2)

如上所示,(1)式将会打印出集合中所有宠物的名字,而(2)式则只输出前2个宠物的名字。这就是 lazy collection 的好处,总是延迟计算。

pets.view.filter { x => println(x.getName); true }.get(1) --- (3)

(3)式和(2)式会有一样的结果吗?错!它的结果和(1)是一样的,你知道为什么吗?

通过比较 Java 和 Scala 中的一些共同的操作方法 ——filter、group、map 和 find;很明显 Scala 的方法比 Java 更简洁。你更喜欢哪一个呢?哪一个的可读性更强?

在文章的下一个部分,我们将比较哪种方式更快。敬请期待!

原文链接: https://dzone.com/articles/java-8-vs-scalapart-ii-streams-api

OneAPM for Java 能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问 OneAPM 官方博客

Java 8 vs. Scala(二):Stream vs. Collection的更多相关文章

  1. idea中使用scala运行spark出现Exception in thread "main" java.lang.NoClassDefFoundError: scala/collection/GenTraversableOnce$class

    idea中使用scala运行spark出现: Exception in thread "main" java.lang.NoClassDefFoundError: scala/co ...

  2. Exception in thread "main" java.lang.NoSuchMethodError: scala.Predef$.refArrayOps([Ljava/lang/Object;)Lscala/collection/mutable/ArrayOps;

    Exception in thread "main" java.lang.NoSuchMethodError: scala.Predef$.refArrayOps([Ljava/l ...

  3. spark提示Caused by: java.lang.ClassCastException: scala.collection.mutable.WrappedArray$ofRef cannot be cast to [Lscala.collection.immutable.Map;

    spark提示Caused by: java.lang.ClassCastException: scala.collection.mutable.WrappedArray$ofRef cannot b ...

  4. Apache Spark Exception in thread “main” java.lang.NoClassDefFoundError: scala/collection/GenTraversableOnce$class

    问题: 今天用Maven搭建了一个Spark的Scala项目,运行后遇到下面异常: Apache Spark Exception in thread “main” java.lang.NoClassD ...

  5. Java 8 vs. Scala(一): Lambda表达式

    [编者按]虽然 Java 深得大量开发者喜爱,但是对比其他现代编程语言,其语法确实略显冗长.但是通过 Java8,直接利用 lambda 表达式就能编写出既可读又简洁的代码.作者 Hussachai ...

  6. Java 8新特性之Stream(八恶人-3)

    “You John Ruth The Hangman” 绞刑者鲁斯·约翰 “When the Hangman catches you, you hang.”当被绞刑者抓住了,你肯定会被绞死 一.基本介 ...

  7. scala(二)

    一.映射 1.Scala映射就是键值对的集合Map.默认情况下,Scala中使用不可变的映射. 如果想使用可变集合Map,必须导入scala.collection.mutable.Map    (导包 ...

  8. Java提高篇(三二)-----List总结

    前面LZ已经充分介绍了有关于List接口的大部分知识,如ArrayList.LinkedList.Vector.Stack,通过这几个知识点可以对List接口有了比较深的了解了.只有通过归纳总结的知识 ...

  9. java提高篇(二九)-----Vector

    在java提高篇(二一)-–ArrayList.java提高篇(二二)-LinkedList,详细讲解了ArrayList.linkedList的原理和实现过程,对于List接口这里还介绍一个它的实现 ...

随机推荐

  1. 【转】做产品VS做项目

    相关定义 根据GB/T19000—2008<质量管理体系基础和术语>,有以下定义 过程process 一组将输入转化为输出的相互关联或相互作用的活动 注:一个过程的输入通常是其他过程的输出 ...

  2. HTML5拖拽功能drag

    1.创建拖拽对象 给需要拖拽的元素设置draggable属性,它有三个值: true:元素可以被拖拽:false:元素不能被拖拽:auto: 浏览器自己判断元素是否能被拖拽. 2.处理拖拽事件当我们拖 ...

  3. Android寒假实训云笔记总结——欢迎页

    欢迎页使用的是viewpager,需要适配器. 注意点: 1.判断是否是第一次进入这个app. 2.欢迎页小圆点的逻辑. 实现原理: 首先在activity_welcome放入viewpager和固定 ...

  4. SQL Server 可疑的解决办法

    SQL SERVER 数据库状态为“可疑”的解决方法 --MyDB为修复的数据名 USE MASTER GO SP_CONFIGURE RECONFIGURE WITH OVERRIDE GO ALT ...

  5. 使用html5结构化元素

    显示的效果如上边这样,但是有没有发现内容的外侧没有边线(这个难道是因为浏览器不同吗?有待测试) html5中新增加的元素<header>可以明确的告诉浏览器此处是页头,<nav> ...

  6. 一个layer可以跟着画完的线移动ios程序 好玩啊。

    用法:采用的是关键帧实现的. 实验目的:让上层的layer子层能够跟着在另一个子层上花的线进行移动 .即当线画完之后,图形开始移动,并且能够停在最后的那个位置 效果图: 采用是直接在layer图层上进 ...

  7. 14_Response对象

    [简述] Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象和代表响应的response对象. request和response对象既然代表请求和响应 ...

  8. 在Windows下用MingW 4.5.2编译FFmpeg

    1.下载FFmpeg(http://ffmpeg.org/download.html),解压. 2.进入MingW Shell,cd F:/Qt/ffmpeg(假定解压到F:/Qt/ffmpeg) 3 ...

  9. HTML5之字体

    - 使用CSS样式来定义 context.font = [CSS font property] context.font = [font-style font-variant font-weight ...

  10. CSS3 animation-fill-mode 属性

    现在专注于移动端开发项目,对于动画这个点是非常重要的,每当我遇到一个新的知识点,我就会和大家一起分享 animation-fill-mode :把物体动画地从一个地方移动到另一个地方,并让它停留在那里 ...