Java 8 vs Scala — Part II Streams API

Stream 与 Collection 的比较

这是我按自己的意思给的一个十分简要的说明:collection 是一个有限的数据集,而 stream 是数据的一个序列,可以是有限的也可以是无限的。区别就这么简单。

Streams API 是 Java 8 的一个新的 API,用于操作 collection 和 stream 数据。Collections API 会改变数据集的状态,而 Streams API 不会。例如,调用 Collections.sort(list) 会把传入的参数排好序,而 list.stream().sorted() 则不同,它会把数据复制一份,保持原数据不变。Streams API 可以参考这里。

下面是我从 Java 8 的文档中摘出来的关于 collections 和 streams 的比较。强烈建议你看一下完整的版本。

Streams 和 collections 几个不同之处:

  1. 无存储。一个 steam 不是一个存储数据元素的数结构。而是通过计算操作管道从源头传输数据元素。
  2. 本质是函数。在 steam 上的一个操作就产生一个新的结果,而不对数据源做任何的改动。
  3. 懒执行的。许多 steam 的操作,如 filtering,mapping 或者 duplicate removal 都是懒执行的,使其能进行更好的优化。
  4. 可能不受限制的。Collection 的大小是有限制的,streams 则没有。
  5. 消耗的。Steam 中的元素在 steam 的生存时间内只能被访问一次。

Java 和 Scala 都有一个十分简单的方式去同时计算 collection 中的值。在 Java 中,你只需要使用parallelStream()* 或者 stream().parallel(),而不是简单的使用 stream()。在 Scala 中,在使用其他方法之前必须先调用 par()。而且可以通过添加并行度来提高程序的性能。不幸的是,大多数时间它的执行速度都是非常慢的。事实上,parallelism 是一个很容易被错误的使用组件。查看这个文章Java Parallel Streams Are Bad for Your Health!

  • 在 JavaDoc 中,关于parallelStream() 方法是这样说明的,这个方法可能会返回一个并行stream,那么意味着它也可能返回的是一个串行 stream。看到这里你一定会觉得很奇怪,有人已经就这个问题进行了研究,详细情况请阅读下面链接中的文章。 (someone did some research on why this API exists)

Java 的 Stream API 是延后执行的。这就意味着,如果你在调用 stream API 的时候,没有指定一个终结操作(比如 collect() 方法调用),那么所有的中间调用(比如 filter 调用)是不会被执行的。这样做的主要目的是为了优化 stream API 的执行,并提高 stream API 的执行效率。比如,我们要对一个数据流进行过滤,映射,求和运算,通过使用延后执行机制,那么对所有这些操作只要遍历一遍数据流就可以了。同时延后执行能力,也实现了每个操作只处理感兴趣的的数据(数据经过前一个操作之后才传入下一个操作)。 而对于 Scala,默认的集合是非延后处理的,这意味着每个操作都会完全遍历 Collection 中的每个元素。这样是否意味着,在我们的测试中,Java Stream API 应该优于 Scala 的呢?如果我们只是在 Java Stream API 和 Scala Collection API 之间做比较,那么答案是正确的,Java Stream API 要优于 Scala Collection API。但是在 Scala 中,你可以通过一个简单的 toStream() 调用,将一个 Collection 转换成一个 Stream。就算你不把 Collection 转成Stream,在 Scala 中你还可以使用 view (一种提供延后处理能力的 Collection)来处理你的数据集合。

让我们快速的看一下 Scala 的 Stream 和 View 特性。

Scala 的 Stream

Scala 的 Stream 和 Java 的 Stream 有点不同。在 Scala 的 Stream 中,你无须去调用终端操作去取的 Stream 的结果,因为它本身就是一个结果。Stream 是继承 AbstractSeq, LinearSeq, 和 GenericTraversableTemplate 的一个抽象类。所以你可以把 Stream 看做一个Seq 。如果你不怎么熟悉 Scala,可以把 Seq 看做 Java 中的 List。(Scala 中的 List 不是一个接口,当然这个另作讨论:)).

我们必须要知道 Streams 中的元素都是懒计算的,也正因如此 Stream 可以计算无限的数据。如果要计算几个集合里的所有元素,Stream 和 List 有着相同的计算效率。一旦被使用,它的值就被 cache了。Stream 有一个叫 force 的方法,它强制评估整个 stream 并返回结果。在计算无限数据的时候千万不要使用这个方法。还有 size(),toList(),foreach() 这些强制计算这个 Stream 的方法。这些操作在 Scala 的 Stream 中都是隐式的。

在 Scala 的 Stream 中实现斐波那契数列。

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 //不要这么使用,因为它会无限的执行下去,然后报内存溢出错误。
//fib1.size //不要这么使用和上面同样的原因。
fib1.take(10) //将返回前10个值
fib1.take(20).foreach(println(_)) //打印前20个值

:: 是集合中常用的连接数据的方法。而 #:: 方法则是连接数据但是是懒执行的(Scala中的方法名是比较随意的)。

Scala 的 View

再次重申,Scala 中的 collection 是一个样的 collection 而 View 则是一个非严格的 collection。View 是基于一个基础 collection 的 collection,其中所有的转换都是懒执行的。通过调用 view 方法可以把一个严格的 collection 转换成 view,也可以通过调用 force 方法把它转换回来。View 并不 cache 结果,每次你调用它的时候它都会执行一次。就像数据库的 View,但它是虚拟的集合。

创建一个要使用的数据集。

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);
    }
}

这种方式是我们在命令式语言中常见的。你必须创建一个临时的集合,之后遍历每个元素并存储每一个满足谓词的元素放入这个临时的集合。有点啰嗦,但其所做的工作和其性能一样,是惊人的。在这里,我会破坏这种更快的传统流式 API 方式。不要担心性能,因为那会让代码更优雅,这超过了轻微的性能增益。

Java Approach 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 去过滤集合中的元素。我故意调用两次 filter 是想展示Streams 的 API 设计的就像是一个 Builder pattern。在 Builder pattern 中,在构造结果集前你可以把一系列方法串联起来使用。在 Streams API 中,构造方法被叫做装卸操作。中间操作不是一个装卸操作。装卸操作可能和构造方法有些不同,因为它在 Streams API 中只能被调用一次。有很多你可以使用的装卸操作 --collect,count,min,max,iterator,toArray。这些操作产生的结果和一些装卸操作一样会消耗其中的值,例如,foreach。你认为传统和 Streams API 哪一个可读性更强?

Java Approach 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’s law,那样的话代码看起来就像是第二个声明。

Scala 入门:集合,视图,与流

//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 中流 API。看看那每一个,你不得不调用视图函数把严格的集合转向非严格的集合,并且调用 tostream 函数,把严格的集合转向一个流。

我认为,我已经有了这个想法,因此,我将向你显示该代码,并且保持沉默。

分组

元素属性中的一个元素中的组元素。该结果将是地图>,和一个泛型类型。

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

//Java approach
Map<Pet.Type, List<Pet>> result = pets.stream().collect(groupingBy(Pet::getType));
//Scala approach
val result = pets.groupBy(_.getType)

排序

集合中的任何属性元素中的各种元素。结果将是任何类型的集合,依靠配置,来维持元素的秩序。

要求:我们要按类型、名称和色序来给宠物分类。

//Java approach
pets.stream().sorted(comparing(Pet::getType)
    .thenComparing(Pet::getName)
    .thenComparing(Pet::getColor))
    .collect(toList());
//Scala approach
pets.sortBy{ p => (p.getType, p.getName, p.getColor) }

Mapping

在集合中每个元素上应用给定的方法。根据你给定义的方法不同返回的结果类型也不同。

需求: 我们想把宠物类转换成“%s — name: %s, color: %s”格式。

//Java 方法
pets.stream().map( p->
        String.format(“%s — name: %s, color: %s”,
            p.getType(), p.getName(), p.getColor())
    ).collect(toList());
//Scala 方法
pets.map{ p => s"${p.getType} - name: ${p.getName}, color: ${p.getColor}"}

Finding First

返回第一个和给定值匹配的值.

需求:我们想找一个名叫 “Handsome”的宠物。 不管有多少个“Handsome",只取第一个。

//Java 方法

pets.stream()

    .filter( p-> p.getName().equals(“Handsome”))

    .findFirst();
//Scala 方法
pets.find{ p=> p.getName == “Handsome” }

这个有点狡猾。你有注意到在 Scala 中我使用的是 find 而不是 filter 方法吗?如果用 filter 代替 find,它就会读取所有的元素,因为 scala 的集合严格的。然而,在 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个宠物的名字。这就是集合懒执行的好处,连计算都是懒的。

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

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

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

在文章的下一个部分,我们将比较哪一个比较快。它是可以准确比较的。请保持关注。

文章转载自 开源中国社区[https://www.oschina.net]

时间: 2024-07-31 11:48:05

Java 8 vs Scala — Part II Streams API的相关文章

Java 8 vs. Scala:Part I

比较 Java 8 和 Scala 在使用 Stream API 时的表达方式和性能的差异. 经过漫长的等待,终于等到了有着高阶函数的 Java 8.我迷恋 Java,但是我必须承认和现在一些其它的语言相比 Java 的语法确实是十分的冗余.现在使用 lambda 表达式,我就可以编写实用且可读性的代码(有时,比传统的方式可读性更强). 虽然 Java 8 在 2014 年 3 月就发布了,但是我最近才有机会使用它.自从我知道 Scala 后,就一直想比较一下 Java 8 和 Scala 的表

向Java开发者介绍Scala

Scala结合了面向对象编程与函数编程思想,使用一种能够完全兼容Java.可以运行在Java虚拟机上的.简洁的语法.对于函数编程风格的支持,尤其是对于Lambda表达式的支持,能够有助于减少必须要编写的逻辑无关固定代码,也许让它可以更简单的关注要面对的任务本身,而相对的Java中对Lamdba表达式的支持要到预定于2012年发布的JavaSE8才会实现.本文就是对于Scala介绍. 相关厂商内容 Flash Builder 4.5高级版试用版免费高速下载 QClub(北京站)--<云计算与虚拟化

给Java开发者的Scala教程

author:Michel Schinz,Philipp Haller 1. 简介 本文将该要的介绍Scala语言和其编译.这里假设读者已经有一定的java开发经验,需要概要的了解他们可以用Scala 做些什么. 2. 第一个例子 我们用全世界最著名的代码来作为开始.虽然没什么用,但是可以很好地直观的了解Scala: object HelloWorld { def main(args: Array[String]): Unit = { println("Hello, world!") 

学习方法-java新手,请问各位大神api怎么用啊?

问题描述 java新手,请问各位大神api怎么用啊? 老师叫我们写一个简易计算器,我只能做出界面.却不知道怎么写监听器.api也不会用求前辈们指导一下学习方法 解决方案 查一下java.awt中有个Button 解决方案二: 用awt或者swt,这方面,你可以找个现成的完整的计算器来看,google里搜索下面的关键字(注意我的搜索技巧) site:download.csdn.net java 计算器

如何使用Java测试IBM Systems Director的REST API

本教程介绍了有关使用 Java 代码自动化 IBM Systems Director 的 REST API 测试的基本步骤.技巧和窍门. 在开始之前 了解可从本教程中获得的预期内容,以及如何充分利用本教程. 关于本教程 IBM Systems Director 支持三种类型的接口:http://www.aliyun.com/zixun/aggregation/18378.html">图形用户界面 (GUI).命令行界面 (CLI) 和使用 RESTful webservices 的应用程序

Java 8 中的 Streams API 详解

为什么需要 Stream Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念.它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream.Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利.高效的聚合操作(aggregate operation),或者大批量数据操作 (

JAVA/JSP学习系列之十(JavaMail API发邮件[servlet])

js|servlet|发邮件 我这里用的是1.2版本,将相关包(jar文件)加到CLASSPATH中 二:该程序非常简单,不需要我们考虑很多地层的东西,因为API都帮我们做好了这些事情,下面是一个简单的发邮件的Servlet:(对于熟悉的人来说,恐怕是再简单不过了的一个servlet) import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import sun.net.smtp.*; public class

产品经理教你写代码—用JAVA写一个阿里云VPC Open API调用程序

引言 VPC提供了丰富的API接口,让网络工程是可以通过API调用的方式管理网络资源.用程序和软件管理自动化管理网络资源是一件显著提升运维效率和网络生产力的事情.产品经理教你写代码系列文章的目标是不懂代码的网络工程师能一步一步的学会用API管理网络.另外通过文章标题大家也可以看出来,产品经理教你写代码肯定是一个业余班,里面的代码很多写的都不规范,可能也有很多Bug.专业选手可以参考的有限,请适度喷,手下留情.其实如果只是想用单个API调用,可以用下面这个线上工具: https://api.ali

Java反射机制剖析:定义和API

1.什么是Java反射机制 Java的反射机制是在程序运行时,能够完全知道任何一个类,及其它的属性和方法,并且能够任意调用一个对象的属性和方法.这种运行时的动态获取就是Java的反射机制.其实这也是Java是动态语言的一个象征. 用一句话来概括反射就是加载一个运行时才知道的类以及它的完整内部结构. 2.为什么要有Java反射机制 我们为什么要用Java的反射机制呢? 我认为有两种: 第一种:反射的目的就是为了扩展未知的应用.比如你写了一个程序,这个程序定义了一些接口,只要实现了这些接口的dll都