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)

image

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]

相关文章
|
2月前
|
Java API Maven
如何使用Java开发抖音API接口?
在数字化时代,社交媒体平台如抖音成为生活的重要部分。本文详细介绍了如何用Java开发抖音API接口,从创建开发者账号、申请API权限、准备开发环境,到编写代码、测试运行及注意事项,全面覆盖了整个开发流程。
312 10
|
6天前
|
JSON Java 数据挖掘
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
|
23天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
23天前
|
存储 缓存 Oracle
Java线程池,白话文vs八股文,原来是这么回事!
本文介绍了Java线程池的原理、实现方式及相关参数。首先,通过类比公司员工的方式解释了线程池的核心概念,如核心线程、最大线程数、任务队列和拒绝策略。接着,详细描述了线程池的任务处理流程,并提供了使用`ThreadPoolExecutor`和`Executors`创建线程池的代码示例,强调了`ThreadPoolExecutor`的灵活性和`Executors`的局限性。最后,总结了线程池的相关参数及不同类型的线程池实现,并附带常见面试题及其解答,帮助读者全面理解线程池的应用场景和优化方法。
37 4
|
1月前
|
算法 Java API
如何使用Java开发获得淘宝商品描述API接口?
本文详细介绍如何使用Java开发调用淘宝商品描述API接口,涵盖从注册淘宝开放平台账号、阅读平台规则、创建应用并申请接口权限,到安装开发工具、配置开发环境、获取访问令牌,以及具体的Java代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
82 10
|
1月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
76 6
|
1月前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
2月前
|
安全 Java API
告别SimpleDateFormat:Java 8日期时间API的最佳实践
在Java开发中,处理日期和时间是一个基本而重要的任务。传统的`SimpleDateFormat`类因其简单易用而被广泛采用,但它存在一些潜在的问题,尤其是在多线程环境下。本文将探讨`SimpleDateFormat`的局限性,并介绍Java 8引入的新的日期时间API,以及如何使用这些新工具来避免潜在的风险。
44 5
|
2月前
|
开发框架 Java 关系型数据库
Java哪个框架适合开发API接口?
在快速发展的软件开发领域,API接口连接了不同的系统和服务。Java作为成熟的编程语言,其生态系统中出现了许多API开发框架。Magic-API因其独特优势和强大功能,成为Java开发者优选的API开发框架。本文将从核心优势、实际应用价值及未来展望等方面,深入探讨Magic-API为何值得选择。
83 2
|
2月前
|
安全 Java API
Java中的Lambda表达式与Stream API的高效结合####
探索Java编程中Lambda表达式与Stream API如何携手并进,提升数据处理效率,实现代码简洁性与功能性的双重飞跃。 ####
33 0