原文: Scala Collections Tips and Tricks ,
作者 Pavel Fatin 是JetBrains 的一名员工,为神器IntelliJ IDEA开发Scala插件。
受其工作 Scala Collections inspections )的启发,他整理了这个关于Java Collections API技巧的列表。
一些技巧只在一些微妙的实现细节中体现,但是大部分技巧都是一般的常识,但是在大部分情况下被忽视了。
性和谐提示和技巧很有价值,可以帮你深入理解Scala Collections,可以是你的代码更快更简洁。
图例 Legend
为了使下面的例子更容易理解,这里列出了一些约定:
-
seq
— 一个Seq
-based的集合, 比如Seq(1, 2, 3)
-
set
— 一个Set
实例, 比如Set(1, 2, 3)
-
array
— 一个数组, 比如Array(1, 2, 3)
-
option
— 一个Option
, 比如Some(1)
-
map
— 一个Map
, 比如Map(1 -> "foo", 2 -> "bar")
-
p
— 一个断言predicate
函数,类型T => Boolean
, 比如_ > 2
-
n
— 一个整数 -
i
— 一个索引 -
f, g
— 简单函数,A => B
-
x, y
— 一些字面值(arbitrary values) -
z
— 初始值或者缺省值
组合Composition
记住,尽管这些技巧都是独立和自包含的,我们还是可以将它们组合起来逐步迭代得到一个更高级的表达式,比如下面的例子,在一个Seq中检查一个元素是否存在:
我们可以依赖”substitution model of recipe application“ ( SICP) )来简化复杂的表达式。
副作用 Side effects
”Side effects“是一个基础概念,在函数式编程语言中会有这个概念。 Scala有一个PPT专门介绍: Side-effect checking for Scala
基本上,”Side effects“是这样一个动作, 除了返回一个值外,外部函数或者表达式还观察到此动作还有以下行为之一:
- 有输入输出操作 (比如文件,网络I/O)
- 对外部变量有修改
- 外部对象的状态有改变
- 抛出异常
当一个函数或者表达式有以上任何一种情况时,我们就说它有副作用(Side effects),否则我们就说它是"纯"的函数或者表达式 (pure)。
side effects有什么大不了的?当有副作用时,计算的顺序不能随便改变。 比如下面两个"纯" (pure)表达式:
因为它们没有副作用,两个表达式可以互换位置,先 x
后 y
和先 y
后 x
的效果一样。
如果有副作用 (有控制台输出):
两个表达式不能互换位置,因为一旦互换位置,输出结果的顺序变了。
所以副作用的一个影响就是会减少可能的转换的数量(reduces the number of possible transformations),包括可能的简化和优化。
同样的原因可适用用Collection相关的表达式上。看一个外部的 builder
变量(副作用方法 append
):
原则上 seq.filter(p).headOption
可以简化为 seq.find(p)
,但是副作用阻止我们这么做. 如果你尝试这么做:
结果和前一个表达式并不一样。 前一个表达式计算后所有大于3的元素都增加到 builder
中了, 后一个表达式在找到第一个大于3的元素后就不会再增加了。
两个表达式并不等价。
自动简化是否可能?这里有两条黄金法则,可以用在有副作用的代码上:
- 尽可能的避免副作用
- 否则将副作用嗲吗从纯代码中分离开
对于上面的例子,我们需要去掉 builder
或者将它从纯代码中隔离。 考虑到 builder
是第三方的对象,我们无法去除,那我们通过隔离的方式实现:
这样我们就可以用到本文中的技巧进行替换:
干的漂亮!自动简化也成为可能,一个额外好处是由于清晰的隔离,代码更容易理解。
一个不太明显的好处是,代码变得更健壮。如上面的例子,副作用针对不同的 Seq
实现,副作用的结果也不相同, 比如 Vector
和 Stream
, 副作用隔离可以让我们避免这种不确定的行为。
Sequence
本节中的技巧针对 Seq
以及它的子类, 而一些转换可以应用于其它集合类,如 Set
, Optio
n, Map
,甚至 Iterator
类,因为它们提供了相近的接口。
创建Creation
显示创建集合
有时候可以节省内存(重用empty对象)和CPU (length check浪费)。
也可应用于 Set
, Option
, Map
, Iterator
.
Length
对于数组,优先使用 length
而不是 size
。
length
和 size
基本是同义词。在Scala 2.11中, Array.size
是通过隐式转换实现的。因此每次调用时,一个中间包装类会被创建,除非你允许jvm的 escape analysis 。这会产生多余的GC对象,影响性能。
不要对检查empty的属性取反
同样适用于 Set
, Option
, Map
, Iterator
不要通过计算length来检查empty
一方面已经有检查empty的方法,另一方面,,比如 LinearSeq
和子类 List
,会花费 O(n)
的时间计算 length
( IndexedSeq
花费 O(1)
)。
同样适用于 Set
, Map
不要直接使用 length
来比较
同上一条,计算 length
有时候非常昂贵,有可能将花费从 O(length)
减少到 O(length min n)
。
对于无限的stream来说,上面的技巧是绝对必要的。
等价 Equality
不要使用 ==
比较数组:
因为 ==
只是比较实例对象,而不是里面的元素。
同样适用于 Iterator
不要检查不同分类(categories)的集合的相等性
不要使用 sameElements
来比较有序集合
不要手工检查相等性
使用内建的方法。
Indexing
不要使用index得到第一个元素
不要使用index得到最后一个元素
不要显式检查index的边界
不要仿造 headOption
不要仿造 lastOption
小心 indexOf
和 lastIndexOf
参数类型
不要构造index的Range
不要手工使用index来zip集合
检查元素的存在 Existence
不要用断言equality predicate 来检查存在
同样应用于 Set
, Option
, Iterator
小心 contains
参数类型
不要用断言inequality predicate 来检查不存在
同样应用于 Set
, Option
, Iterator
不要统计元素的数量来检查存在
同样应用于 Set
, Map
, Iterator
不要借助 filter
来检查存在
同样应用于 Set
, Option
, Map
, Iterator
Filtering
不要对断言取反
同样应用于 Set
, Option
, Map
, Iterator
不要借助 filter
统计元素数量
调用 filter
会产生一个临时集合,影响GC和性能。
同样应用于 Set
, Option
, Map
, Iterator
不要借助 filter
找到元素的第一个值
同样应用于 Set
, Option
, Map
, Iterator
Sorting
不要手工按一个属性排序
不要手工按照identity排序
一步完成排序反转
Reduction
不要手工计算sum
其它可能用的方法 reduceLeft
, reduceRight
, foldLeft
, foldRight
同样应用于 Set
, Iterator
不要手工计算product
同样应用于 Set
, Iterator
不要手工搜索最小值和最大值
同样应用于 Set
, Iterator
不要仿造 forall
同样应用于 Set
, Option
(for the second line), Iterator
不要仿造 exists
Rewriting
合并连续的 filter
调用
或 seq.view.filter(p1).filter(p2).force
同样应用于 Set
, Option
, Map
, Iterator
合并连续的 map
调用
或 seq.view.map(f).map(g).force
同样应用于 Set
, Option
, Map
, Iterator
filter
完后再排序
在调用 map
前不要显式调用反转 reverse
不要显示反转得到迭代器
不要通过将集合转成 Set
得到不重复集合
不要仿造 slice
同样应用于 Set
, Map
, Iterator
不要仿造 splitAt
不要仿造 span
不要仿造 partition
不要仿造 takeRight
不要仿造 flatten
同样应用于 Set
, Map
, Iterator
不要仿造 flatMap
同样应用于 Set
, Option
, Iterator
不需要结果时不要用 map
同样应用于 Set
, Option
, Map
, Iterator
不要产生临时集合
1.使用view
- 将view转换成一个同样类型的集合
如果中间的转换是filter,还可以
- 将view转换成另一种集合
还有一种 “transformation + conversion”
方法:
使用赋值操作符
Scala有一个语法糖,自动将 x <op>= y
转换成 x = x <op> y
. 如果 op
以 :
结尾,则被认为是右结合的操作符。
一些list和stream的语法:
同样应用于 Set
, Map
, Iterator
Set
大部分的 Seq
的技巧也可以应用于 Set
。另外还有一些只针对 Set
的技巧。
不要使用 sameElements
比较未排序的集合
同样应用于 Map
不要手工计算交集
不要手工计算diff
Option
Option
并不是集合类,但是它提供了类似的方法和行为。
大部分针对 Seq
的技巧也适用于 Option
。这里列出了一些特殊的只针对 Option
的技巧。
Value
不要使用 None
和 Option
比较
不要使用 Some
和 Option
比较
不要使用实例类型来检查值的存在性
不要使用模式匹配来检查值的存在
同样适用于 Seq
, Set
对于检查存在性的属性不要取反
不要检查值的存在性再处理值
Null
不要通过和 null
比较来构造 Option
不要显示提供 null
作为备选值
Rewriting
将 map
with getOrElse
转换成 fold
不要仿造 exists
不要手工将 option
转换成 sequence
Map
同上,这里只列出针对map的技巧
不要使用 lift
替换 get
因为没有特别的需要将map值转换成一个Option。
不要分别调用 get
和 getOrElse
不要手工抽取键集合
不要手工抽取值集合
小心使用 filterKeys
因为 filterKeys
包装了原始的集合,并没有复制元素,后续处理得小心。
小心使用 mapValues
同上。
不要手工filter 键
使用赋值操作符重新赋值
补充
除了以上的介绍,建议你看一下官方文档 Scala Collections documentation 。
还有
- Scala for Project Euler
- Ninety-nine
最后一段是作者的谦虚话,欢迎提供意见和建议。