Scala 同时支持不可变集合和可变集合。不可变集合主要用于多线程的安全并发访问,因此读的效率更高。对于两种集合,Scala提供两个不同的包:
- 不可变集合:scala.collection.immutable
- 可变集合:scala.collection.mutable
无论是哪种集合,它们的顶层特质(或抽象类)都延续了这种结构:
出于效率考虑, Scala 默认采用不可变集合,不过这并不耽误你使用可变集合来完成一些必要的任务。
Scala 的集合有三大类:序列 Seq,集 Set,映射 Map。所有的集合都拓展自Iterable 特质,即它们都是可使用 for 循环遍历的。对于各种集合,Scala 都提供了非常,非常多的方法,本文仅介绍较基本的使用方式。
下方是 Scala 2.11.12 版本的官方API说明文档:scala.collection
1. 明确 “不可变” 的概念
Scala 中的不可变集合,代表这个集合本身的内存空间大小不允许进行动态变化,在这里可以类比 Java 的各类数组,它们总是在创建时就已经确定了尺寸。然而,集合内部的元素是可以改变的。
可变集合,指这个集合所占的内存空间大小可以动态发生变化。再比如: Java 中的 ArrayList 可以通过 add 或 remove 动态改变集合的长度。
ArrayList<String> strings = new ArrayList<>();
strings.add("Welcome back");
System.out.println("hashCode:"+strings.hashCode());
strings.add("Java");
System.out.println("hashCode:"+strings.hashCode());
从表层来看,ArrayList 是一个可变集合,然而,它的内部实现却是借助不可变的数组完成的。当不断地向 ArrayList 当中添加新元素而引起 strings 列表的长度发生变化时,程序实际上是将原先较短的数组内元素挪到了一个新的较长数组内,因为它不能直接在原数组中拓展空间。
综上,每次在对 ArrayList 调用 remove 方法时,都会导致其引用的 hashcode 发生变化。
2. Scala 集合的体系
下面是 不可变 (immutable) 集合的族谱:
Seq 可细分为索引序列和线性序列。一般来说索引序列的访问速度比线性序列要快一些,但是线性序列的应用场景比较多,比如队列和栈等都是很常用的数据结构。
Set 和 Map 都提供有序方式的集合 SortedXXX 。Java 中 List 归属于 Set 集合下,在 Scala 则属于 Seq 的线性序列部分。
String 被归纳到了索引序列下:因为它本质上可被认作是 Char[] 类型集合,并且还可以通过索引的方式来输出对应位置的字符。
下面是 可变 (mutable) 集合 的族谱:
可变集合的内容明显要比不可变集合要更复杂一些,主要是多了 Buffer (缓冲)的内容。在实际开发中我们常用的有 ArrayBuffer 和 ListBuffer。
如果涉及到线程安全的内容,则使用以 Synchronized 作为前缀的集合。
3. Scala 数组
如果从可变、不可变的角度对 Scala 数组进行划分,则大致上可分为 Array (不可变的) 和 ArrayBuffer (可变的)数组类型。
3.1 创建 Array 定长数组
Scala 用泛型的形式来指明数组内的元素类型,使用圆括号 () 来指定索引位置(Scala 的方括号 [] 表示泛型),下标从 0 开始。在初始化时,空位使用 0,或者 null 进行填充,取决于数组内的元素是何种类型。
//中括号表示泛型
//圆括号表示数组长度
val ints: Array[Int] = new Array[Int](10)
//取0号位置的元素
val i: Int = ints(0)
Scala 的数组元素的类型可以声明为 Any 类型,表示该数组内可以存放任何类型的元素。
//Scala的数据元素未必都是同一种类型。
val anys: Array[Any] = new Array[Any](4)
anys(0) = "Hello World"
anys(1) = 24.00d
anys(2) = new Student
//打印每一个元素的类型,设置守卫避开空指针异常。
for(i <- anys if i!=null) println(i.getClass)
也可以选择在创建数组的时候直接放入元素。下面的代码块调用了 Array 的 apply 方法,因此 new 关键字被省略掉了。编译器在编译期间将根据放入元素的类型来自动推断数组类型:
val array = Array(1,2,3,"Hello")
//输出类型:class [Ljava.lang.Object; 相当于 Java 中的 Object[]。
println(array.getClass)
val array2 = Array(1,2,3,4)
//输出类型:class [I; 相当于 Java 中的 Int[]。
println(array2.getClass)
如果放入的都是一类元素,则该数组的类型和该类元素相同。如果放入的不是同一类元素,则数组类型为 Any 类型。不过,一旦数组确定了更具体的元素类型,那么再传入其它类型的元素就会出现错误。
val ints = Array(1, 2, 4, 5, 6)
println(ints.getClass)
//ints 在编译时会被确定是 Array[Int]。
ints(0) = 1 //ok
ints(1) = "Hello" //Oops!
同类型的 Array 之间可以使用 ++ 符号合并。
//ints = Array(1,2,3,4,5)
val ints: Array[Int] = Array(1,2,3) ++ Array(4,5)
若使用传统的数组下标方式来遍历数组,则可通过 arrays.indices 来获取从 0 到 arrays.length-1 的下标值。
val ints = Array(1,2,3,4,5)
//IDE推荐使用seq.indices进行遍历。
//等价于:
//for(index <- 0 until ints.size )
//for(index <- 0 to ints.size -1 )
//indices是一个函数,返回 Range 0 until length.
for(index <- ints.indices)
{
println(index)
println(s"position$index:${ints(index)}")
}
indices 实际上是一个封装起来的函数:
/** Produces the range of all indices of this sequence.
*
* @return a `Range` value from `0` to one less than the length of this $coll.
*/
def indices: Range = 0 until length
3.2 创建 ArrayBuffer 变长数组
ArrayBuffer 位于scala.collection.mutable包下。和之前的定长数组 Array 相比,ArrayBuffer 还支持自身进行 拓展(append) 或者 删除(remove)。创建方法如下,圆括号内可以不指定初始元素,这样默认长度就为 0。
import scala.collection.mutable.ArrayBuffer
val arrs = new ArrayBuffer[Int]()
//ArrayBuffer提供apply方法,因此也可以省略new关键字。
val arrs1 = ArrayBuffer[Int]()
我们尝试着对 ArrayBuffer 进行长度上改动,然后观察其 hashcode 是否会发生变化:
val arrs = new ArrayBuffer[Int]()
arrs.append(5,6,7)
println(arrs.hashCode())
arrs.append(2,3,4)
println(arrs.hashCode())
控制台中打印的两行 hashcode 是不一样的。这说明 ArrayBuffer 在底层的动态拓展原理类似于前文提到的 ArrayList 。
3.2.1 val 修饰的 ArrayBuffer 为什么可以动态拓展?
上述代码中,被 val 修饰的 arrs 为什么会允许被修改引用呢?笔者翻阅了 ArrayBuffer 的源代码:
class ArrayBuffer[A](override protected val initialSize: Int)
extends AbstractBuffer[A]
with Buffer[A]
with GenericTraversableTemplate[A, ArrayBuffer]
with BufferLike[A, ArrayBuffer[A]]
with IndexedSeqOptimized[A, ArrayBuffer[A]]
with Builder[A, ArrayBuffer[A]]
with ResizableArray[A]
with CustomParallelizable[A, ParArray[A]]
with Serializable
ArrayBuffer 继承了相当多的特质。有这样一个特质需要注意:ResizableArray[A],从命名看它表示可拓展的数组特质。该特质里面有一个被var所修饰的重要成员:
protected var array: Array[AnyRef] = new Array[AnyRef](math.max(initialSize, 1))
可以发现,ArrayBuffer 实际上进行了一层包装:动态扩容/减少操作所替换的都是 array 成员的引用。val修饰符只限制 ArrayBuffer 引用不允许改变,当对它进行动态扩容时,实际上改变的是其内部被 var 修饰的 array 成员。然而,若尝试着对 ArrayBuffer 的引用做出修改,则程序一定会报错。
val arrs = new ArrayBuffer[Int]()
//这个 val 不允许修改 arrs 本身。但是允许修改内部的 array ,因为它是 var 修饰的成员。
arrs = new ArrayBuffer[Int](4)
3.3 ArrayBuffer 的符号运算
笔者在隐式转换章节提到过:Scala 热衷于使用简单的助记符号来让我们写出更加优雅的代码。而你也一定很清楚,这些符号并不是 Scala 神秘的语法特性,我们不过是在调用一些以助记符号做标识符的函数而已。
ArrayBuffer 适用于以下运算符对元素进行操作。
| 符号 | 作用 |
|---|---|
++ | 用于合并 ArrayBuffer 或者 Array 。 |
+= | 将符合类型的元素添加到此 ArrayBuffer 中。 |
++= | 将另一个 Array 或者 ArrayBuffer 内的元素添加到原 ArrayBuffer 内。 |
-= | 删除一个指定的元素。如果该元素并不存在,则该操作不会报错,也不会引起变化。 |
--= | 删掉和另一个 Array 或者 ArrayBuffer 内重复的元素。 |
3.4 变长数组与定长数组的转换
定长数组 Array 提供 toBuffer 方法转换为变长数组 Buffer (它是特质)。
变长数组 ArrayBuffer 和 toArray 方法转换为定长数组 Array。
注意,Array 和 ArrayBuffer 并不是完全对等的相互转换,而是下图中的 "三角关系":
Array 提供的 toBuffer 方法实际上源自于 IndexedSeqLike 特质。该方法声明的返回类型是 Buffer,而通过源码可知,真正的返回值是一个实现 Buffer 特质的 ArrayBuffer 上转型对象,我们可以通过 getClass 方法检查出来。
如果要将 Array 转化得到 ArrayBuffer ,可以在 toBuffer 的基础上再通过 asInstanceOf[ArrayBuffer[T]] 进行强制转换。它涉及了高阶类型,简单点说就是泛型内嵌套泛型的情况,这得益于 Scala 更强大的类型系统。下面给出代码示例:
val array : Array[Int] = Array[Int](1,2,3,4)
val arrayBuffer : ArrayBuffer[Int] = ArrayBuffer[Int](5,6,7,8)
//toBuffer方法返回的是一个 scala.collection.mutable.Buffer 类型。
val toBuffer: Buffer[Int] = array.toBuffer[Int]
// 可以通过强制转换得到更具体的 ArrayBuffer,而不是 Buffer 。
// val toBuffer: ArrayBuffer[Int] = array.toBuffer[Int].asInstanceOf[ArrayBuffer[Int]]
//检查这个 toBuffer 也可以发现它实际是 ArrayBuffer 类型的上转型对象。
println(toBuffer.getClass)
//array 本身没有发生变化。输出会得到[I,因为它本质上是Java的Int[]数组。
println(array.getClass)
//ArrayBuffer[T] 可以通过 toArray 方法转换为 Array[T]。
val toArray: Array[Int] = arrayBuffer.toArray
//arrayBuffer 本身也没有变化。输出会得到 scala.collection.mutable.ArrayBuffer。
println(arrayBuffer.getClass)
3.5 在 Scala 中创建多维数组
Scala 通过下面的方式来指定不可变的多维数组的 维度(dimension) 和元素类型,其泛型是一个高阶类型。
val array: Array[Array[Int]] = Array.ofDim[Int](2,3)
这行代码生成的是一个 2 行 3 列的数组。即:生成了 2 个子数组,每个子数组内包含 3 个元素。在赋值和访问 n 维数组时,需要加 n 个括号来 "定位" :
// array(1) 得到的是第一行数组。
array(1)(1) =2
多维数组遍历起来也要稍微麻烦一些,下面给出了 for-each 遍历风格和下标风格的遍历实现:
//for-each 风格遍历方式
for{
line<-array2d
e<-line
}{
print(e)
}
//indices 索引下标方式
for(x <- array2d.indices){
for (y<- array2d(x).indices){
println(s"position:{$x,$y} : value =${array2d(x)(y)}")
}
}
3.6 Scala 和 Java 之间的集合转换
在编写 Java 和 Scala 混合开发的项目时,为了保证数据类型之间的兼容性,Scala 提供了工具来实现 Scala 和 Java 之间的集合转换。实现方式正是上一章提及的隐式转换,因此我们可以不去追究底层细节。
3.6.1 ArrayBuffer (Scala) -> List (Java)
Scala 提供了一个可以将 ArrayBuffer(该类属于 Scala)转换为 List (该类属于 Java) 的隐式转换函数。
//JavaConversions还提供许多其它的隐式转换函数。
import scala.collection.JavaConversions.bufferAsJavaList
之后可以使用java.util.List[T]类型的变量来接受一个scala.collection.mutable.ArrayBuffer对象的赋值了。下面给出完整的代码示例:
val scalaObjectsBuffer: ArrayBuffer[scalaObject] =
ArrayBuffer[scalaObject](new scalaObject(1), new scalaObject(2))
//JavaConversions还提供许多其它的隐式转换函数。
import scala.collection.JavaConversions.bufferAsJavaList
import java.util
//利用导入的隐式转换函数进行Scala ArrayBuffer -> Java List的转换。
val javaObjectsList: util.List[scalaObject] = scalaObjectsBuffer
//Java风格的迭代器Iterator遍历。
val iterator: util.Iterator[scalaObject] = javaObjectsList.iterator()
while (iterator.hasNext) {
println(s"scalaObject id:${iterator.next().oid}")
}
//--------scalaObject是一个简单类-------------//
class scalaObject(val oid: Int) {}
3.6.2 List (Java) -> Scala (ArrayBuffer)
逆过程同理。这一次在 Java 代码中来实现它,并保证此 Java 代码可以使用 SDK。
//导入 scala.collection.JavaConversions 伴生对象。
import scala.collection.JavaConversions;
import scala.collection.mutable.Buffer;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class Java2Scala {
public static void main(String[] args) {
//在 Java 内创建一个 List 。
List<javaObject> javaObjects = new LinkedList<>();
Collections.addAll(javaObjects, new javaObject(1), new javaObject(2));
//Java 没有隐式函数的概念,在这里要显式地调用函数。
Buffer<javaObject> scalaObjectBuffer = JavaConversions.asScalaBuffer(javaObjects);
System.out.println(scalaObjectBuffer);
}
}
@SuppressWarnings("all")
class javaObject {
public int jid;
public javaObject(int jid) {
this.jid = jid;
}
@Override
public String toString() {
return "javaObject:[id = " + jid + "]";
}
}
4. Scala 元组
4.1 在什么情况下需要它
Scala 元组 (Tuple) 的存在让涉及到大量中间过程的流式处理业务变得非常灵活,便捷。是一个保存复合数据类型的简单容器。那么,既然有了 OOP 的概念为什么还要引入这样的元组呢?
以 Java Web 开发为例,对于每个零散查询结果,我们都应该再编写一个 Bean 类型承载它,比如:
public class Result {
private Integer count;
private String studentName;
private Integer disqualification;
//超长的Get Set方法
//超长的构造器方法
}
显然,这个 Result 除了充当某条查询结果的数据容器就没有其它意义了。随着业务的中间过程越来越多,这种简单的 Result 类会越来越多,那么此应用可能会处于 "类爆炸" 的窘境。如果不想针对每一个中间结果都设计一个类来保存,那用 Map 来代替 Bean 并存储这些数据是否可行呢?
//由于value的类型包含Integer,String,因此value的类型只能指定为Object.
LinkedHashMap<String,Object> hashMap = new LinkedHashMap<>();
hashMap.put("count",1);
hashMap.put("String","李先生");
hashMap.put("disqualification","1");
//这导致所有的类型全变成了Object.
Object string = hashMap.get("String");
理论上当然可行,但是处理方式并不优雅。由于查询结果往往混杂着多种数据类型,这就意味着只能使用最顶级的 Object 来保存其 value 值,并通过强制转换来得到预期的数据类型。
而 Tuple 的存在一定程度上简化了这个问题,对于这些少量的,零散的数据,我们不再需要建立一个庞大的 Bean ,也不需要使用 Map :
//使用 Tuple 直接将这些松散的数据装入一个容器中。
//Tuple3 表示这是一个装载了 3 个数据的容器。
new Tuple3<>(21,"李先生",34);
不过,Tuple 也同样可能带来一些麻烦。由于 Tuple 是一个松散的数据组织结构,若想要查找 Tuple 内的某一个数据,只能通过索引号来找到它。当一个结构体组合了过多的数据时,我们很容易会忘记(或记错)每个位置上的元素所对应具体的含义。因此在这种情况下,我们还是需要建立 Bean 来保管这些数据。
4.2 创建 Scala 元组
Scala 的 Tuple 最可以存放 22 个元素,这已经足够使用了。
为了高效的操作元组,Scala 根据 Tuple 内放置的元素数量进行了归类:比如只装载 1 个元素的 Tuple 是 Tuple1 类型,装载了 2 个元素的 Tuple 是 Tuple2 类型 ...... 以此类推。
对于 TupleX(X指从1-22,下文同)内的每一个对应位置的元素类型,是通过泛型来指定的。比如一个 Tuple3 的类型声明:
val tuple3 : Tuple3[String,Int,Double] = Tuple3("scala",1,1.00d)
它代表创建一个这样的 Tuple 容器:第一个位置装载 String 类型元素,第二个位置装载 Int 类型的元素 ...... 注意,装入元素的时候,要注意位置的对应关系。
按照上述的写法,实际上编译器会提示:syntactic sugar could be used。这里有一个语法糖:直接用小括号形式来表示元组,并由编译器推断出实际的 Tuple 类型。
val tuple : (String, Int, Double) = Tuple3("scala",1,1.00d)
//更精简的声明方式:
val tuple_ = ("scala", "java", 200)
4.3 番外:IntelliJ 提供的懒人功能
不过,当面对包装多个元素的 Tuple 时,手动补充一个完整类型的变量赋值语句仍是很麻烦的事情。
// 试着在 IntelliJ 中使用 tab 键补充一个完整的赋值
Tuple3("scala",1,1.00d).var
对于使用 IntelliJ 环境开发的同学,这里有个小诀窍:在一条有返回值的语句后面打上 .var 之后,再敲击一下 Tab 键,IntelliJ 可以自动将其转化为一行赋值语句,并可以根据返回值提供一些可选的变量名。另外,你还可以选择:
- 将该本地变量声明为
var类型。 - 指定该变量的完整类型。
IntelliJ 还提供了非常多的快捷键。了解更多的细节,可以去阅览笔者在掘金博客里发表的另外一篇文章:Windows 10 && IntelliJ 开发环境的常用快捷键
4.4 访问 TupleX 内的元素
TupleX 通过形如 ._index 的形式来访问对应索引位置的元素,index 取值为1 ~ X。额外提醒这里不同于数组的 0 号下标,第一个位置的 index 为 1。比如说访问某个 Tuple 中的第 2 个元素:
val tuple = Tuple3("scala", 1, 1.00d)
// _x 表示访问第 x 个位置上的元素,因此会打印 1 。
// 或者写成后缀表达式的形式: tuple _2
println(tuple._2)
如果习惯从 0 下标位置开始,则可以使用 productElement(index) 进行访问。
println(tuple.productElement(0))
笔者认为用 _index 的方式更加直观,第二种方法了解即可。
4.5 遍历 TupleX 内的元素
遍历Tuple的内部元素需要依赖一个迭代器 productIterator 。
//不要直接写成下面的形式:
//for(e <- tuple)
for (e<-tuple.productIterator){
println(e)
}
5. Scala List
Java 中的 List 是一个接口,真正的实现是 ArrayList,LinkedList 等。而 Scala 提供的 List 是可以直接存放数据的列表。
注意,Scala 中的 List 是 不可变(immutable) 的,并且它属于序列 Seq 。如果要使用可变的列表类型,需要引入另一个数据类型:ListBuffer。
List 定义在 scala.Predef 包,因此不需要显示地引入scala.collection.immutable.List。下面演示了如何创建一个简单的集合 List,包括创建一个不含任何元素的 List。
//简单创建一个集合
val list : List[Int] = List(1,2,3,4,5)
//打印list会显示List(1,2,3,4,5)
print(list)
//创建一个空集合,Nil是一个独立的类,它继承于List[Nothing].
val nil: Nil.type = Nil
//打印Nil会打印List()
println(nil)
List 内同样也可以放入不限于一种数据类型,但是这需要声明该 List 的泛型为Any。访问 List 元素的方式也和 Array 相仿,使用圆括号 (index)。
//访问 list 的第 3 个元素,下标从 0 开始。
print(list(2))
5.1 List 元素的追加
5.1.1 首插,尾部追加单个元素
由于 List 本身是不可改变的,因此追加操作不会改变原来的 List ,而是返回一个新的 List。Scala 的 List 提供操作符号来表示追加,比如头插入 +: 和尾部追加 :+ :
//在列表后面追加元素,使用函数 :+ ,且 list 写在前面。
val addTohead: List[Int] = list :+ 0
//在列表前面追加元素,使用函数 +: ,且 list 写在后面。
val addToTail: List[Int] = 6 +: list
5.1.2 拼接元素
除了头部插入,尾部插入之外,使用 :: 符号可以实现将多个零散的元素拼接到 List 当中。
val tail = List(3,4,5)
// ints = List(1,2,3,4,5)
val ints: List[Int] = 1 :: 2 :: tail
:: 符号的运算顺序是由右到左,因此使用该符号进行拼接时,一定要保证表达式最右边是 List 类型。如果没有可用做容器的 List ,则可以使用 Nil 代替,表示将拼接的元素装入到一个新的空 List 当中:
// ints = List(1,2,3)
val ints: List[Int] = 1 :: 2 :: 3 :: Nil
注意,使用 :: 会将元素当作一个整体装入 List 中。如果我们意图用 :: 符号将多个子 List 拼接成一个长 List ,可能会有意料之外的结果:
val list_1: List[Int] = List(1, 2, 3)
val list_2: List[Double] = List(1.0, 2, 0, 3.0)
val list_3: List[Any] = List(obj(1), obj(2), obj(3))
val multi_list: List[Any] = list_1 :: list_2 :: list_3
print(s"list_3's size = ${multi_list.size}")
基于运算的顺序是从右到左,程序会将最右边的 list_3 当作容器,然后依次将 list_2 和 list_1 整体装入到 list_3 中。
换句话说:现在的 multi_list 当中有 5 个元素( 2 个 List 和 3 个 Any 类型的元素),而非 6 个。
5.1.3 拼接多个子 List
这可能不符合我们的预期:因为这里希望实现的是类似于 Java Collections 的 addAll 方法。这时候我们需要使用::: 操作符而非 :: ,来表示装入的是某个List内的所有元素,而不是这个List本身。
::: 相当于做了一步 flat 操作。
val multi_list: List[Any] = list_1 ::: list_2 ::: list_3
print(s"list_3's size = ${multi_list.size}")
当然,::和:::是可以混用的,运算顺序总是从右到左。
val multi_list: List[Any] = list_1 ::: list_2 :: list_3 :: Nil
print(s"list_3's size = ${multi_list.size}")
for (index <- multi_list.indices) {println(multi_list(index))}
注意,无论是使用::还是:::来拼接 List ,一定要保证最右边的元素是一个数组(可以为Nil),否则会提示语法错误。
5.2 使用可变长度的 ListBuffer
ListBuffer 是可变长度的列表,属于 Seq 序列。它与不可变的 List 不同的是,所有的添加、删除操作会在原列表里进行,而不是返回一个新的列表。
//使用 new 调用构造器构建一个空对象。
val ints = new ListBuffer[Int]()
ints.append(1)
//使用静态的apply方法构建。
val ints1: ListBuffer[Int] = ListBuffer[Int](1, 2, 3, 4)
ints1.append(2)
ListBuffer 提供了追加元素的方法:append,或者更精简的符号 +=。
//如果只添加一个元素,一般用+=。
ints1 += 2
//append 方法支持可变参数 Any*,因此用于一次性追加多个元素。
ints1.append(3,4,5,6)
使用 ++= 来将其它 **ListBuffer 内的所有元素 **全部追加进来。
//使用 new 调用构造器构建,这相当于 List<Integer> list = new LinkedList<>();
val ints = new ListBuffer[Int]()
ints.append(1, 2, 3, 4)
//使用静态的 apply 方法构建。
val ints1: ListBuffer[Int] = ListBuffer[Int](5, 6, 7, 8)
ints1 ++= ints
//控制台显示ints1有8个元素,而不是5个。
println(ints1.size)
我们也可以单独使用++=符号拆成++和=来使用,用于给另外一个新的 ListBuffer 赋值:
//将追加之后的数组赋值到新的ListBuffer当中。
val lnts2: ListBuffer[Int] = ints1 ++ ints
ListBuffer 通过下标来寻找并删除对应位置的元素,使用remove方法。
5.3 List 和 ListBuffer 的常用符号操作
| 符号 | 适用List | 适用ListBuffer | 作用 |
|---|---|---|---|
:+ | yes | yes | 在列表后添加新的元素。不改变原来的列表,而是返回新列表。 |
+: | yes | yes | 在列表首部添加新元素。不改变原来的列表,而是返回新列表。 |
:: | yes | no | 将前面的 List 整体装入到一个最右边的 List 当中。 |
::: | yes | no | 将前面的 List 内的每一个元素装入到一个最右边的 List 当中。 |
+= | no | yes | 在此列表内添加一个新的元素。 |
++= | no | yes | 将另一个 ListBuffer 的所有元素依次添加到此列表当中。 |
++ | no | yes | 与++=的区别是它不改变原来的 ListBuffer ,而是会返回一个新的 ListBuffer。 |
6. Scala Queue
Scala 同 Java 一样,已经直接给出了 Queue 的实现。它本质上是一个有序列表,在底层可以使用数组或者链表去实现。
Queue 显然遵循先入先出的原则。Scala 同时给出了对于队列的 mutable 实现和 immutable 实现。对于不可变队列,进行操作后会返回新的队列;对于可变队列,进队出队操作是在原先的队列中进行的。
我们在这里使用的是可变集合中的队列,因此这里主要介绍 scala.collection.mutable.Queue 的功能。为了防止混淆,可以加上前缀来明确声明使用的是可变队列还是不可变队列。
//apply方法实现
mutable.Queue[Any](1,2,3,4,5,6,7)
//new调用构造器来实现
new mutable.Queue[Any]()
6.1 尝试在 Queue 中添加新元素
可变的 Queue 支持使用 += 和 ++= 操作实现类似在 ListBuffer 中介绍过的功能,这里直接以代码形式给出,并在注释上附带上一些要点。
//+= 可以用于添加单个元素。
queue += 8
//+= 会将一个 List 整体当作一个元素加到队列。不过你的 queue 未必支持将 List 整体放入其中,因为这可能其实并不是你的本意。
//如果你希望的是添加 ListBuffer 内的每个元素,那么应该使用 ++= 方法。
queue += ListBuffer[String]("a","b","c")
//++= 和 += 的区别是,它会将队列里的元素按次序全部追加到队列中,而不是添加列表本身。
queue ++= ListBuffer[Int](8,9,10)
6.2 Queue 队列的进队和出队
Queue 还提供enqueue方法进队,以及dequeue出队方法,可变队列会在原队列中修改,不可变队列会返回新的队列。
//apply方法实现
val queue: mutable.Queue[Any] = mutable.Queue[Any](1,2,3,4,5,6,7)
//运算符方式追加元素
queue += 8
//查看队列头
println(s"queue head : ${queue.head}")
//弹出首个元素
val first: Any = queue.dequeue()
print(s"popped element: $first")
//再次查看队列头
println(s"queue head : ${queue.head}")
另外,可以通过queue.head来查看队列头的元素内容,或者通过queue.last来查看队列尾部元素内容(但都不弹出)。
Queue 还提供一个独特的方法:tail。故名思意,就是返回尾部。在队列中,除了第一个 head 元素以外都可称作尾部(而不是指最后一个元素)。这个方法弹出首元素,然后将剩下的元素排列成一个新队列返回。更通俗点说,enqueue是取 “头” ,则 tail 是去掉 "头" 之后剩下的 "躯干" 部分。
//查看原队列头部的元素。
print(ints.head)
//tail方法会将原先的队列去head元素之后返回新队列。
val newInts : mutable.Queue[Int] = ints.tail
//打印新生成的队列。
println(newInts)
//原来的队列没有变化。
println(ints)
tail方法的返回值也是一个队列,因此可以级联调用该方法,让它一次性弹出多个元素。
val ints: mutable.Queue[Int] = mutable.Queue[Int](1,2,3,4,5)
//tail方法可以级联调用,因为该方法的返回值也是一个队列。
val newInts : mutable.Queue[Int] = ints.tail.tail.tail.tail
//去掉4个头元素之后,这个队列只剩下5。
print(newInts)
使用下标访问的方式越过 FIFO 的原则直接访问元素其实也完全没有问题,因为Scala 将 Queue 划分到了线性序列中。不过既然使用了队列,就应该尽量按照队列先进先出的原则去使用它。
//get the 2th element of the queue?
println(ints(1)) //OK. queue is indeed a sequence.
7. Scala Map
在 Java 中,HashMap 是一个散列表,内部构造是无序的,并且 key 不能重复,否则进行覆盖处理。
Scala 的 Map 分为 可变(mutable) 和 不可变(immutable) 的。其中可变的 Map 是无序的,而不可变的 Map 则是有序的。默认情况下使用的是不可变的 Map ,如果要使用可变的 Map 则需要加上前缀加以区分。下文介绍的 Map 及其相关方法不区分是可变还是不可变的。
Map 内部使用类型 Tuple2 来表示键值对,并且 Scala 提供隐式转换将其包装成了 "更好看" 的形式:
//由于本质上 k-v 对是一个元组,因此也可以写成括号的形式。
//注意箭头的形式为 "->", 而非 "=>"。
val mapSample = mutable.Map("Kotlin" -> "2020", ("Go", "2019"), "Python" -> "2018", "Java" -> "2017")
我们试着直接输出该 mapSample 变量并反复运行这行代码,可以观察到键值对的打印顺序和放入顺序未必一样:
print(mapSample)
//----------Console-----------------//
//Map(Kotlin -> 2020, Go -> 2019, Java -> 2017, Python -> 2018)
//Process finished with exit code 0
这也说明了 mutable.Map 是一个无序集合,不会像 Seq 一样严格的按照索引顺序进行排列。
7.1 从 Map 中取出 k-v 对
7.1.1 根据 key 取出 Map 内的 value
Map 的取值直接使用小括号即可。括号内的参数是所查找的 value 所对应的 key 值。
val map = mutable.Map("scala"->"2019","go"->"2018")
//不需要像java一样调用get方法,也可以直接取出value值。
//当然Scala也提供了传统的get方法,且和java有一些区别,我们稍后再探讨它。
print(map("scala"))
//当程序找不到这个key时,还会像java一样返回null吗?
print(map("scala~"))
这里有一个重点要注意:在 Java 中,如果能找到存在的 key,则会返回对应的 value,反之返回一个 null。而在 Scala 中,处理方式有所不同:如果程序找不到key 值,则会抛出一个java.NoSuchElementException。
/**
* 这个方法实现这样的功能:检查传入的map有没有key为"scala"的键值对。
* @param StringMap 需要传入一个Map[String,String]的Map.
* @throws java.util.NoSuchElementException scala的机制和java有所不同,若找不到key,则会抛出异常。
* @return 若没有发生异常,则会返回true。
*/
@throws(classOf[NoSuchElementException])
def getScala(StringMap : mutable.Map[String,String]) : Boolean ={
StringMap("scala")
true
}
7.1.2 使用 contains 方法检查 key 是否存在
显然不加任何判断机制而直接抛出异常,这风险太大了。为了避免这个问题,我们可以首先使用 contains 方法,即在取出这个 key 之前先检查它是否存在。下面给出改进的getScala内部逻辑:
//它判断的是key是否存在。
if (StringMap.contains("scala")) {
//若存在,则取出。
StringMap("scala")
true
} else false
7.1.3 使用 get 方法避免异常
到刚才为止我们都是直接以map(key)的形式去取 value 的。其实Scala Map 同样提供了get(key)一样的方法,但是它和 Java Map 的 get 方法(至少是Java 8版本及之前)有所区分。
Scala Map 的 get 方法返回一个更安全的 Option[T] 类型。Option[T] 本身衍生了两个子类:Some[T] 和 None。当 get 方法找到这个 key 时,则会将对应的 value 封装到Some[T]类中返回,这个 T 正是 value 的返回类型;否则,返回一个None[Nothing]类型,表示没有找到匹配的 key 值。
我们利用这个特性,再对之前的getScala方法进行一个优化:
def getScala(StringMap : mutable.Map[String,String]) : Boolean = StringMap.get("String").isDefined
7.1.4 简洁的 getOrElse 方法
回忆一下 Java 8 中曾引入的一个 Optional[T] 类:它的 orElse 方法有效的避免了曾困扰 Java 程序员许久的空指针异常。而Scala Map 所提供的getOrElse()方法思路上和它大同小异:如果找到了匹配的 key,则返回 value ,否则返回一个默认的 value。
def getScala(StringMap: mutable.Map[String, String]): String = {
StringMap.getOrElse("map","None.")
}
7.1.5 如何选择这几种取值方式
- 如果能确定这个 key 一定存在于这个 map 中,则应当使用最简洁的
map(key)形式。 - 如果要根据这个 key 是否存在于 map 而采取不同的业务逻辑,则应当使用
map.contains(key)方法判断。 - 如果仅想知道某个键值对是否存在,则使用
map.get(<key>).isDefined来判断。 - 如果在找不到 key 时希望能提供一个默认值,则使用
map.getOrElse方法。
7.2 Map 的更改,添加与删除
本小节介绍的添加,删除操作,对于可变和不可变 Map 而言会有所区别。
7.2.1 添加某个 Key
+= 符号一般用于可变 Map 中,用于在原 Map 当中添加一个 k-v 键值对。那如果这个 key 已经存在,则这条语句就变成了修改操作:它将新的 value 值赋值给这个 key,而不会报出错误。
注意,+= 符号实际上预期的是一个二元组的序列,如果是按照元组的写法添加键值对,则需要在最外层再嵌套一个小括号。如果仅添加单个元素,这里推荐 -> 的写法。
val map = mutable.Map("scala" -> "2019", "go" -> "2018")
//通过+=符号向Map当中追加键值对。
map += "Kotlin" -> "2017"
//或者使用元组的方式追加键值对。注意,要嵌套两层括号,表示放入的是一个包含二元组的集合。
// map += (("Kotlin"->"2017"))
println(map.getOrElse("Kotlin", "()"))
//如果添加了一个重复的key,程序会报错嘛?
map += "scala" -> "2020"
println(map.getOrElse("scala", "()"))
不可变 Map 一般仅用 + 符号添加新的键值对,并返回一个新的不可变 Map 。不可变 Map 可以使用 += 符号操作,不过这要求此 Map 变量是 var 修饰的,因为这相当于将操作结果的引用覆盖了原先不可变 Map 的引用。
var map = Map("scala"->"2.11", "java"->"8")
map += "python" -> "3.8"
7.2.2 删除某个 key
-= 符号表示从某个 Map 当中删除掉指定 key 的键值对。如果这个 key 本身就不存在,那就什么都不会发生,而不会报出错误。
val map = mutable.Map("scala" -> "2019", "go" -> "2018")
//通过-= "key" 删掉map中存储的key-value对。
map -= "go"
println(map.getOrElse("go", "deleted"))
//如果正企图删除一个不存在的key,程序会报错嘛?
map -= "Java"
对于不可变 Map,同样可以使用 - 或者 -= 达到删除键值对的功能,注意事项和添加 key 同理。
7.2.3 修改某个 Key
用一个简单的赋值语句即可实现修改 key 的对应 value。如果这个 key 之前就不存在,那么这条语句就变成了一条添加操作:它会在这个 Map 当中放入Tuple2("scala","2020"),而不会报出错误。
注意,只有可变的 Map 才支持用这种方式对 k-v 对进行修改。
val map = mutable.Map("scala" -> "2019", "go" -> "2018")
//将原先key = scala 的value从2019 修改为2020.
map("scala") = "2020"
//尝试打印出scala对应的value值。
println(map.getOrElse("scala", "()"))
//如果为一个不存在的key赋值呢?
map("Java") = "2014"
//会提示错误嘛?
println(map.getOrElse("Java", "()"))
7.3 Map 的遍历
Java 中,对 Map 的遍历是比较麻烦的:需要获得 Map 的 KeySet,然后再使用迭代器去遍历。Scala 提供了一个更简洁高效的遍历方式:只需要一个 for 循环即可遍历 Map 元组。
根据不同的需求,for 循环的写法可能也有所差别。额外提示:keys和values可用于获取这个 Map 的所有 key 值,或者是所有的 value 值。
//如果要在遍历过程中同时使用key和value:
for ((k, v) <- map) {
println(s"key:$k,value=$v")
}
println("---------------------------")
//如果要在遍历过程中只使用key:
for (k <- map.keys) {
println(s"key:$k")
}
println("---------------------------")
//如果要在遍历过程中只使用value:
var count = 1
for (v <- map.values) {
println(s"$count element value=$v")
count += 1
}
println("---------------------------")
//如果要在遍历过程中将这个键值对视作是Tuple2:
for ( tuple <- map) {
println(s"key:${tuple._1},value=${tuple._2}")
}
8. Scala Set
集(Set)是不重复元素的集合,它不保证元素的顺序。Scala 中的 Set 同样分为可变(mutable)的和不可变(immutable)的,默认情况下是不可变的。
//创建一个不可变的Set集合。
val immutableInts = Set(1,2,3,4)
//创建一个可变的Set集合。
val mutableInts = mutable.Set(1,2,3,4,5)
8.1 向 Set 中添加元素
对于不可变 Set,支持 + 符号返回一个执行添加操作后的新的集,原 Set 并不改变。
val ints: immutable.Set[Int] = immutableInts + 5
对于可变 Set,还可使用 += 或者 add 表示在原集内添加元素。注意,Set 中不会添加重复的元素。
//向集合中添加一个元素5。
mutableInts += 5
//使用add方法也可以向集合中添加一个元素。
//也可以这样声明:mutableInts add 6
mutableInts.add(6)
如果对不可变的 Set 使用 += 方法,则要求它必须是被 var 修饰的变量,因为该操作会使用结果的引用覆盖掉原先的引用,这和 Map 章节介绍的添加元素,删除元素道理类似。
var ints3: immutable.Set[Int] = Set(1,2,3)
ints3 += 4
8.2 移除 Set 内的元素
对于可变 Set,可使用 -= 或者 remove 表示删除集合中的某个元素。当 Set 中没有要删除的元素时,什么都不会发生,不会报出错误。
//删除集合中的元素,如果没有改元素,则什么都不会发生。
mutableInts -= 5
//使用remove方法也可以删除集合中的元素。
mutableInts remove 5
不可变的 Set 同样可以使用 - 或者 -= 符号达到删除元素的目的。
8.3 Set 集合的常用方法
Set 更趋向于数学意义的集合。常用的方法就是:求两个集合的交集 intersect 或者是求两个集合的并集 ++。
不可变 Set 和可变 Set 都支持该类方法,甚至两者之间也可以进行集合运算,并总是返回新的 Set 结果。然而,该 Set 是可变还是不可变的,这取决于调用此操作的 Set 是可变还是不可变的。
val setA = mutable.Set(1, 2, 3, 4)
val setB = mutable.Set(1, 2, 5, 7)
//返回一个新的可变 Set 集合:它是A和B的交集。
val intersection: mutable.Set[Int] = setA.intersect(setB)
//返回一个新的可变 Set 集合,它是A和B的并集。
val union: mutable.Set[Int] = setA ++ setB
println("a ∩ b = "+intersection.mkString(","))
println("a ∪ b = "+union.mkString(","))
Set 提供 max 方法提取最大值,min 方法提取最小值。在后续了解了scala.math.Ordering[T]特质 (它的作用类似于 Java 的 Comparable<T> 接口) 之后,可以自定义一个类的 “最大” 和 "最小" 的概念。对于基本数据类型来说,我们可以直接提取出最大值和最小值。
//在了解了scala.math.Ordering[T]特质之后,可以实现自定义类的最大值最小值返回。
//返回Set集合当中的最大值
println(setA.min)
//返回Set集合当中的最小值
println(setA.max)
9. 更便捷的 mkString 方法
Scala 集合提供一个 mkString 方法,能够让我们快速获取到任意类型集合内的元素信息组成的字符串。下面给出一个使用案例:
//mkString需要传入一个字符(串)做分隔符。
//屏幕会打印:1,2,3,4,5,6,7
println(ints.mkString(","))
//屏幕会打印:1-2-3-4-5-6-7
println(ints.mkString("-"))
其基本的逻辑是调用内部元素的 toString 方法,并使用分隔符进行拼接。