Spark编程基础(Scala版)
上QQ阅读APP看书,第一时间看更新

2.2 Scala基础知识

本节介绍Scala的基础编程知识,包括基本数据类型和变量的定义、输入输出语句、流程控制结构和数据结构等。

2.2.1 基本数据类型和变量

1.基本数据类型

表2-1列出了Scala的9种基本数据类型及其取值范围。其中,类型Byte、Short、Int、Long和Char统称为整数类型,Float和Double被称为浮点数类型。可以看出,Scala与 Java有着相同的基本数据类型,只是类型修饰符的首字母大小写不同,Scala 中所有基本类型的首字母都采用大写,而Java中除了字符串类型用首字母大写的String,其他八种基本类型都采用首字母小写的修饰符。

Scala 是一门纯粹的面向对象的语言,每个值都是对象,也就是说 Scala 没有 Java 中的原生类型,表2-1中列出的数据类型都有相应的类与之对应。在Scala中,除了String类型是在java.lang包中被声明之外,其余类型都是包 scala 的成员。例如,Int 的全名是 scala.Int。由于包 scala 和java.lang 的所有成员都被每个 Scala 源文件自动引用,因此可以省略包名,而只用 Int、Boolean等简化名。

表2-1 Scala的基本数据类型

除了以上9种基本类型,Scala还提供了一个Unit类型,类似Java中的void类型,表示“什么都不是”,主要作为不返回任何结果的函数的返回类型。

2.字面量

字面量是直接在源代码里书写常量值的一种方式。不同类型的字面量书写语法如下:

整数字面量

整数字面量有两种格式:十进制和十六进制。十进制数开始于非零数字,十六进制数开始于0x或0X前缀。需要注意的是,不论用什么进制的字面量进行初始化,Scala的Shell始终打印输出十进制整数值。整型字面量被编译器解释为 Int 类型,如果需要表示 Long,需要在数字后面添加大写字母L或者小写字母l作为后缀。

浮点数字面量

浮点数字面量是由十进制数字、小数点和可选的E 或e 及指数部分组成。如果以F或f结尾,会被编译器解释为Float类型,否则就是Double类型。

布尔型字面量

布尔型只有true和false两种字面量。

字符及字符串字面量

字符字面量是在半角单引号之间的任何 Unicode 字符,还可以用反斜杠“\”表示转义字符。字符串字面量用半角双引号包括一系列字符表示,如果需要表示多行文本,则用三个双引号包括。举例如下:

scala> val c='A'

c: Char = A

scala> var c1='\u0045'

c1: Char = E

scala> c1='\''

c1: Char = '

scala> val s = "hello world"

s: String = hello world

scala> val ss = """ the first line

 | the second line

 | the third line"""

ss: String =

" the first line

the second line

the third line"

Unit字面量

Unit类型只有一个唯一的值,用空的原括号表示,即()。

3.操作符

Scala为它的基本类型提供了丰富的操作符集,包括:

算术运算符:加(+)、减(-)、乘(*)、除(/)、余数(%);

关系运算符:大于(>)、小于(<)、等于(=)、不等于(!=)、大于等于(>=)、小于等于(<=);

逻辑运算符:逻辑与(&&)、逻辑或(||)、逻辑非(!);

位运算符:按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移(<<)、右移(>>)、无符号右移(>>>)。

赋值运算符:“=”及其与其他运算符结合的扩展赋值运算符,例如+=、%=。

需要强调的是,尽管这些基本操作符在使用上与Java基本一致,但是,Scala的操作符实际上是方法,也就是说,在Scala中,每个操作都是方法调用,操作符不过是对象方法调用的一种简写形式。例如,5 + 3和5.+(3)是等价的,因为Scala作为一门纯粹的面向对象语言,它的每个值都是一个对象,即这里的数值5也是一个Int类型的对象,由于Int 类有一个名为“+”的方法,它接收一个Int型参数并返回一个Int型的结果,因此,5.+(3)就表示在5这个对象上调用名称为“+”的方法,把3作为参数传递给该方法,完成加法计算。实际上,Int类还包含了许多带不同参数类型的重载加法方法。例如,有一个名为“+”的、参数和返回类型都为Double的方法。所以,5+3.5会返回Double型的8.5,相当于调用了5.+(3.5)。另外,与Java不同的是,Scala中各种赋值表达式的值都是Unit类型,因此,尽管“a=b=5”是合法的语句,但不是表示将a和b的值都赋值为5;实际上,执行该语句时,首先执行赋值表达式b=5,使得b的值变为5,b=5这个赋值表达式的值是Unit类型,这样a就成为Unit类型。

Scala操作符的优先级和Java基本相同,从高到低基本遵循以下顺序:

算术运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符

唯一的例外是,逻辑非(!)有比算术运算符更高的优先级。但是,在实际应用中,没有必要记住所有操作符之间的优先级顺序,推荐的做法是,除了不言自明的优先级以外(例如,乘除法优先级比加减法高),尽量使用括号去厘清表达式中操作符的优先级。

对于基本数据类型,除了以上提到的各种操作符外,Scala还提供了许多常用运算的方法,只是这些方法不是在基本类里面定义,而是被封装到一个对应的富包装类中。表2-1中每个基本类型都有一个对应的富包装类。例如,Int有一个对应的RichInt类、String有一个对应的RichString类,这些富包装类位于包scala.runtime中。当对一个基本数据类型的对象调用其富包装类提供的方法时,Scala会自动通过隐式转换,将该对象转换为对应的富包装类型,然后再调用相应的方法。例如,执行语句3 max 5时,Scala检测到基本类型Int没有提供max方法,但是Int的富包装类RichInt具有max方法,这时,Scala会自动将3这个对象转换为RichInt类型,然后调用RichInt的max方法,并将5作为参数传给该方法,最后返回的结果是Int型的5。

4.变量

尽管 Scala 有多种基本数据类型,但是从声明变量的角度看,Scala 只有两种类型的变量,分别使用关键字val和var进行声明。对于用val声明的变量,在声明时就必须被初始化,而且初始化以后就不能再赋新的值;对于用var声明的变量,是可变的,可以被多次赋值。声明一个变量的基本语法为:

val 变量名:数据类型 = 初始值

var 变量名:数据类型 = 初始值

Scala的这种语法结构与Java中“变量类型 变量名=值”的语法结构有所区别。同时,Scala提供了一种类型推断机制(Type Inference),它会根据初始值自动推断变量的类型,这使得定义变量时可以省略具体的数据类型及其前面的冒号。例如,语句var str= "Hello world"与var str : String ="Hello world"的作用是一样的,因为使用了一个字符串文本初始化变量str,Scala可以自动推断出str的类型是String。同理,var i=1和var i:Int=1也是等价的。但是,如果需要将i定义为浮点型,则必须显式指定类型:var i:Double=1,或者用浮点型的值初始化:var i=1.0。

需要注意的是,在 REPL 环境下,可以重复使用同一个变量名来定义变量,而且变量前的修饰符和其类型都可以不一致,REPL会以最新的一个定义为准,例如:

scala> val a = "Xiamen University"

a: String = Xiamen University

scala> var a = 50

a: Int = 50

2.2.2 输入/输出

1.控制台输入输出语句

为了从控制台读写数据,可以使用以read为前缀的方法,包括:readInt、readDouble、readByte、readShort、readFloat、readLong、readChar、readBoolean及readLine,分别对应9种基本数据类型,其中,前8种方法没有参数,readLine可以不提供参数,也可以带一个字符串参数的提示。所有这些函数都属于对象scala.io.StdIn的方法,使用前必须导入,或者直接用全称进行调用。使用示例如下:scala> import io.StdIn._

import io.StdIn._

scala> var i=readInt()

54

i: Int = 54

scala> var f=readFloat

1.618

f: Float = 1.618

scala> var b=readBoolean

true

b: Boolean = true

scala> var str=readLine("please input your name:")

please input your name:Li Lei

str: String = Li Lei

需要注意的是,在Scala的REPL中,从键盘读取数据时,看不到用户的输入,需要按回车以后才能看到效果。

为了向控制台输出信息,常用的两个函数是print()和println(),可以直接输出字符串或者其他数据类型,两个函数唯一的区别是,后者输出结束时,会默认加一个换行符,而前者没有。例如:

scala> val i=345

i: Int = 345

scala> print("i=");print(i) //两条语句位于同一行,不能省略中间的分号

i=345

scala> println("hello ");println("world")

hello

world

此外,Scala还带有C语言风格的格式化字符串的printf函数,例如:

scala> val i = 34

i: Int = 34

scala> val f=56.5

f: Double = 56.5

scala> printf("I am %d years old and weight %.1f Kg.",i,f)

I am 34 years old and weight 56.5 kg.

上述提到的三个输出方法(print、println和printf)都是在对象Predef中定义的,该对象在默认情况下会自动被所有Scala程序引用,因此,可以直接使用Predef对象提供的print、println和printf等方法,而无需使用scala.Predef.println("Hello World")这种形式。另外,Scala提供了字符串插值机制,以方便在字符串字面量中直接嵌入变量的值。为了构造一个插值字符串,只需要在字符串字面量前加一个“s”字符或“f”字符,然后,在字符串中即可以用“$”插入变量的值,s插值字符串不支持格式化,f插值字符串支持在$变量后再跟格式化参数,例如:

scala> val i=10

i: Int = 10

scala> val f=3.5452

f: Double = 3.5452

scala> val s="hello"

s: String = hello

scala> println(s"$s:i=$i,f=$f") //s插值字符串

hello:i=10,f=3.5452

scala> println(f"$s:i=$i%-4d,f=$f%.1f") //f插值字符串

hello:i=10 ,f=3.5

2.读写文件

Scala使用类java.io.PrintWriter实现文本文件的创建与写入。该类由Java库提供,这正好体现了Scala与Java的互操作性。PrintWriter类提供了print和println两种写方法,其用法与向控制台输出数据所采用的print和println完全一样。例如:

scala> import java.io.PrintWriter

scala> val outputFile = new PrintWriter("test.txt")

scala> outputFile.println("Hello World")

scala> outputFile.print("Spark is good")

scala> outputFile.close()

上面语句中,new PrintWriter("test.txt")中使用了相对路径地址,这意味着,文件test.txt就会被保存到启动Scala REPL时的当前目录下。比如,如果在“/usr/local/scala”目录下使用scala命令启动进入了Scala REPL,则test.txt会被保存到“/usr/local/scala”目录下。如果要把文件保存到一个指定的目录下,就需要在new PrintWriter()的圆括号中给出文件路径全称,比如,new PrintWriter("/usr/local/scala/mycode/output.txt")。

尽管PrintWriter类也提供了printf函数,但是,它不能实现数值类型的格式化写入。为了实现数值类型的格式化写入,可以使用String类的format方法,或者用f插值字符串,例如:

scala> import java.io.PrintWriter

scala> val outputFile = new PrintWriter("test.txt")

scala> val i = 9

scala> outputFile.print("%3d --> %d\n".format(i,i*i))

scala> outputFile.println(f"$i%3d --> ${i*i}%d") //与上句等效

scala> outputFile.close()

Scala使用类scala.io.Source实现对文件的读取,最常用的方法是getLines方法,它会返回一个包含所有行的迭代器(迭代器是一种数据结构,将在“2.2.4 数据结构”中介绍)。下面是从一个文件读出所有行并输出的实例代码:

scala> import scala.io.Source

scala> val inputFile = Source.fromFile("test.txt")

scala> for (line <- inputFile.getLines()) println(line)

scala> inputFile.close()

2.2.3 控制结构

同各种高级语言一样,Scala也包括了内建的选择控制结构和循环控制结构。其中,选择结构包括if语句,循环结构包括for语句和while语句。另外,Scala也有内建的异常处理结构try-catch。

1.if条件表达式

if语句用来实现两个分支的选择结构,基本语法结构为:

if (表达式) {

  语句块1

}

else {

  语句块2

}

执行if语句时,会首先检查if条件表达式是否为真,如果为真,就执行语句块1,如果为假,就执行语句块2。例如:

scala> val x = 6

x: Int = 6

scala> if (x>0) {println("This is a positive number")

 | } else {

 | println("This is not a positive number")

 | }

This is a positive number

Scala与Java类似,if结构中else子句是可选的,而且if子句和else子句中都支持多层嵌套if结构。例如:

scala> val x = 3

x: Int = 3

scala> if (x>0) {

 | println("This is a positive number")

 | } else if (x==0) {

 | println("This is a zero")

 | } else {

 | println("This is a negative number")

 | }

This is a positive number

与Java不同的是,Scala中的if表达式会返回一个值,因此,可以将if表达式赋值给一个变量,这与Java中的三元操作符“?:”有些类似,例如:

scala> val a = if (6>0) 1 else -1

a: Int = 1

2.while循环

Scala的while循环结构和Java的完全一样,包括以下两种基本结构,只要表达式为真,循环体就会被重复执行,其中,do-while循环至少被执行一次。

while (表达式){

    循环体

}

或者

do{

    循环体

}while (表达式)

3.for循环表达式

与Java的for循环相比,Scala的for循环在语法表示上有较大的区别,同时,for也不是while循环的一个替代者,而是提供了各种容器遍历的强大功能,用法也更灵活(容器的概念,将在“2.2.4 数据结构”中介绍)。for 循环最简单的用法就是对一个容器的所有元素进行枚举,基本语法结构为:

for (变量 <- 表达式) {语句块}

其中,“变量<-表达式”被称为“生成器(Generator)”,该处的变量不需要关键字var或val进行声明,其类型为后面的表达式对应的容器中的元素类型,每一次枚举,变量就被容器中的一个新元素所初始化。例如:

scala> for (i <- 1 to 3) println(i)1

2

3

其中,1 to 3为一个整数的Range型容器(将在“2.2.4 数据结构”中介绍Range),包含1、2和3。i依次从1枚举到3。for循环可以对任何类型的容器类进行枚举,例如:

scala> for (i <- Array(3,5,6)) println(i)

3

5

6

其中,Array(3,5,6)创建了一个数组(将在“2.2.4 数据结构”中进一步介绍),for循环依次对数组的3个元素进行了枚举。可以发现,通过这种方式遍历一个数组,比Java语言的表达方法更加简洁高效,而且不需要考虑索引是从0还是1开始,也不会发生数组越界问题。

for循环不仅仅可以对一个集合进行完全枚举,还可以通过添加过滤条件对某一个子集进行枚举,这些过滤条件被称为“守卫式(Guard)”,基本语法结构为:

for (变量 <- 表达式 if 条件表达式) 语句块

此时,只有当变量取值满足if后面的条件表达式时,语句块才被执行。例如:

scala> for (i <- 1 to 5 if i%2==0) println(i)

2

4

上面语句执行时,只输出1到5之间能被2整除的数。如果需要添加多个过滤条件,可以增加多个if语句,并用分号隔开。从功能上讲,上述语句等同于:

for (i <- 1 to 5)

  if (i%2==0) println(i)

可以通过添加多个生成器实现嵌套的for循环,其中,每个生成器之间用分号隔开,例如:

scala> for (i <- 1 to 5; j <- 1 to 3) println(i*j)

1

2

3

2

...

其中,外循环为1到5,内循环为1到3。与单个生成器类似,在多个生成器中,每个生成器都可以通过if子句添加守卫式进行条件过滤。

以上所有的for循环都只是对枚举值进行某些操作即结束,实际上,Scala的for结构更灵活之处体现在,可以在每次执行的时候创造一个值,然后将包含了所有产生值的容器对象作为for循环表达式的结果返回。为了做到这一点,只需要在循环体前加上yield关键字,即for结构为:

for (变量 <- 表达式) yield {语句块}

其中yield后的语句块中最后一个表达式的值作为每次循环的返回值,例如:

scala> val r=for (i <- Array(1,2,3,4,5) if i%2==0) yield { println(i); i}

2

4

r: Array[Int] = Array(2,4)

执行结束后,r为包含元素2和4的新数组。这种带有yield关键字的for循环,被称为“for推导式”。也就是说,通过for循环遍历一个或多个集合,对集合中的元素进行“推导”,从而计算得到新的集合,用于后续的其他处理。

4.异常处理结构

Scala不支持Java中的检查型异常(Checked Exception),将所有异常都当作非检查型,因此,在方法声明中不需要像Java中那样使用throw子句。和Java一样,Scala也使用try-catch结构来捕获异常,例如:

import java.io.FileReader

import java.io.FileNotFoundException

import java.io.IOException

try {

  val f = new FileReader("input.txt")

  // 文件操作

} catch {

  case ex: FileNotFoundException =>... // 文件不存在时的操作

  case ex: IOException =>... // 发生I/O错误时的操作

} finally {

  file.close() // 确保关闭文件

}

如果try程序体正常被执行,则没有异常抛出;反之,如果执行出错,则抛出异常。该异常被catch子句捕获,捕获的异常与每个case子句中的异常类别进行比较(这里使用了模式匹配,将在“2.3.6 模式匹配”中介绍)。如果异常是FileNotFoundException,第一个case子句将被执行;如果是IOException类型,第二个 case 子句将被执行;如果都不是,那么该异常将向上层程序体抛出。其中,finally 子句不管是否发生异常,都会被执行。finally子句是可选的。与Java类似,Scala也支持使用throw关键字手动抛出异常。

5.对循环的控制

为了提前终止整个循环或者跳到下一个循环,Java提供了break和continue两个关键字,但是, Scala 没有提供这两个关键字,而是通过一个名称为 Breaks 的类来实现类似的功能,该类位于包scala.util.control下。Breaks类有两个方法用于对循环结构进行控制,即breakable和break,通常都是放在一起配对使用,其基本使用方法如下:

breakable{

...

if(...) break

...

}

即将需要控制的语句块作为参数放在breakable后面,然后,其内部在某个条件满足时调用break方法,程序将跳出breakable方法。通过这种通用的方式,就可以实现Java循环中的break和continue功能。下面通过一个例子来说明,请在Linux系统中新建一个代码文件TestBreak.scala,内容如下:

//代码文件为/usr/local/scala/mycode/TestBreak.scala

import util.control.Breaks._ //导入Breaks类的所有方法

val array = Array(1,3,10,5,4)

breakable{

for(i<- array){

   if(i>5) break //跳出breakable,终止for循环,相当于Java中的break

println(i)

  }

}

// 上面的for语句将输出1,3

for(i<- array){

  breakable{

    if(i>5) break //跳出breakable,终止当次循环,相当于Java的continue

println(i)

  }

}

// 上面的for语句将输出1,3,5,4

可以在Scala REPL中使用“:load /usr/local/scala/mycode/TestBreak.scala”执行该代码文件并查看程序执行效果。

2.2.4 数据结构

在Scala编程中经常需要用到各种数据结构,比如数组(Array)、元组(Tuple)、列表(List)、映射(Map)、集合(Set)等。

1.数组

数组(Array)是一种可变的、可索引的、元素具有相同类型的数据集合,它是各种高级语言中最常用的数据结构。Scala 提供了参数化类型的通用数组类 Array[T],其中,T 可以是任意的 Scala类型。Scala数组与Java数组是一一对应的。即Scala的Array[Int]可看作Java的Int[],Array[Double]可看作Java的Double[],Array[String]可看作Java的String[]。可以通过显式指定类型或者通过隐式推断来实例化一个数组,例如:

scala> val intValueArr = new Array[Int](3)

scala> val myStrArr = Array("BigData","Hadoop","Spark")

第一行通过显式给出类型参数Int定义一个长度为3的整型数组,数组的每个元素默认初始化为0。第二行省略了数组的类型,而通过具体的3个字符串来初始化,Scala自动推断出为字符串数组,因为Scala会选择初始化元素的最近公共类型作为Array的参数类型。需要注意的是,第二行中没有像Java那样使用new关键字来生成一个对象,实际是因为使用了Scala中的伴生对象的apply方法,具体将在“2.3.2 对象”中介绍。

另外,不同于Java的方括号,Scala使用圆括号来访问数组元素,索引也是从零开始。例如,对于上述定义的两个数组,可以通过 intValueArr(0)=5改变数组元素的值,myStrArr(1)返回字符串"Hadoop"。Scala 使用圆括号而不是方括号来访问数组元素,这里涉及到 Scala 的伴生对象的 update方法,具体将在“2.3.2 对象”中介绍。

需要注意的是,尽管两个数组变量都用val关键字进行定义,但是,这只是表明这两个变量不能再指向其他的对象,而对象本身是可以改变的,因此可以对数组内容进行改变。

既然Array[T]类是一个通用的参数化类型,那么就可以很自然地通过给定T也为Array类型来定义多维数组。Array提供了函数ofDim来定义二维和三维数组,用法如下:

val myMatrix = Array.ofDim[Int](3,4)

val myCube = Array.ofDim[String](3,2,4)

其中,第一行定义了一个3行4列的二维整型数组,如果在REPL模式下,可以看到其类型实际就是Array[Array[Int]],即它就是一个普通的数组对象,只不过该数组的元素也是数组类型。同理,第二行定义了一个三维长度分别为3、2、4的三维字符串数组,其类型实际是Array[Array[Array[String]]]。同样可以使用多级圆括号来访问多维数组的元素,例如myMatrix(0)(1)返回第1行第2列的元素。

2.元组

Scala的元组是对多个不同类型对象的一种简单封装。Scala提供了TupleN类(N的范围为1~22),用于创建一个包含N个元素的元组。构造一个元组的语法很简单的,只需把多个元素用逗号分开并用圆括号包围起来就可以了。例如:

scala> val tuple = ("BigData",2015,45.0)

这里定义了包含三个元素的元组,三个元素的类型分别为String、Int和Double,因此实际上该元组的类型为Tuple3[String,Int,Double]。可以使用下划线“_”加上从1开始的索引值,来访问元组的元素。例如,对于刚定义的元组tuple,tuple._1的值是字符串"BigData",tuple._3的值是浮点数45.0。还可以一次性提取出元组中的元素并赋值给变量,例如,下例展示了直接提取tuple的3个元素的值,并分别赋值给三个变量(实际上这里涉及到 Scala 的模式匹配机制,将在“2.3.6 模式匹配”中进一步介绍)。

scala> val (t1,t2,t3) = tuple

t1: String = BigData

t2: Int = 2015

t3: Double = 45.0

如果需要在方法里返回多个不同类型的对象,Scala可以简单地返回一个元组,为了实现相同的功能,Java通常需要创建一个类去封装多个返回值。

3.容器

Scala 提供了一套丰富的容器(Collection)库,定义了列表(List)、映射(Map)、集合(Set)等常用数据结构。根据容器中元素的组织方式和操作方式,可以区分为有序和无序、可变和不可变等不同的容器类别。Scala用了三个包来组织容器类,分别是scala.collection、scala.collection.mutable和scala.collection.immutable。从名字即可看出scala.collection.immutable包是指元素不可变的容器;scala.collection.mutable包指的是元素可变的容器;而scala.collection封装了一些可变容器和不可变容器的超类或特质(将在“2.3.5 特质”中介绍,这里可以将其理解为Java中的接口),定义了可变容器和不可变容器的一些通用操作。scala.collection 包中的容器通常都具备对应的不可变实现和可变实现。

Scala为容器的操作精心设计了很多细粒度的特质,但对于用户来说,无需掌握每一个特质的使用,因为Scala已经通过混入这些特质生成了各种高级的容器。图2-4所示为scala.collection包中容器的宏观层级关系(省略了很多细粒度的特质)。所有容器的根为Traverable特质,表示可遍历的,它为所有的容器类定义了抽象的foreach方法,该方法用于对容器元素进行遍历操作。混入Traverable特质的容器类必须给出foreach方法的具体实现。Traverable的下一级为Iterable特质,表示元素可一个个地依次迭代,该特质定义了一个抽象的iterator方法,混入该特质的容器必须实现iterator方法,返回一个迭代器(Iterator),另外,Iterable特质还给出了其从Traverable继承的foreach方法的一个默认实现,即通过迭代器进行遍历。

图2-4 scala.collection包中容器的宏观层次结构

在 Iterable下的继承层次包括3个特质,分别是序列(Seq)、映射(Map)和集合(Set),这3种容器最大的区别是其元素的索引方式,序列是按照从0开始的整数进行索引的,映射是按照键值进行索引的,而集合是没有索引的。

4.序列

序列(Sequence)是指元素可以按照特定的顺序访问的容器。在Scala的容器层级中,序列容器的根是collection.Seq特质,是对所有可变和不可变序列的抽象。序列中每个元素均带有一个从0开始计数的固定索引位置。特质Seq具有两个子特质LinearSeq和IndexedSeq,这两个子特质没有添加任何新的方法,只是针对特殊情况对部分方法进行重载,以提供更高效的实现。LinearSeq序列具有高效的head和tail操作,而IndexedSeq序列具有高效的随机存储操作。实现了特质LinearSeq的常用序列有列表(List)和队列(Queue)。实现了特质IndexedSeq的常用序列有可变数组(ArrayBuffer)和向量(Vector)。

这里介绍两种常用的序列,即列表(List)和Range。

列表

列表(List)是一种共享相同类型的不可变的对象序列,是函数式编程中最常见的数据结构。Scala的List被定义在scala.collection.immutable包中。不同于Java的java.util.List,Scala的List一旦被定义,其值就不能改变,因此,声明List时必须初始化,例如:

scala> var strList=List("BigData","Hadoop","Spark")

上面语句定义了一个包含3个字符串的列表 strList。这里直接使用了 List,而无需加包前缀scala.collection.immutable,这是因为Scala默认导入了Predef对象,而该对象为很多常用的数据类型提供了别名定义,包括列表scala.collection.immutable.List、不可变集scala.collection.immutable.Set和不可变映射scala.collection.immutable.Map等。由于List是一个特质,因此不能直接用new关键字来创建一个列表,这里使用了List的apply工厂方法创建一个列表strList(关于apply方法将在“2.3.2对象”中介绍)。创建List时也可以显示指定元素类型,例如:

scala> val l = List[Double](1,3.4)

l: List[Double] = List(1.0, 3.4)

值得注意的是,对于包括List在内的所有容器类型,如果没有显式指定元素类型,Scala会自动选择所有初始值的最近公共类型来作为元素的类型。因为Scala的所有对象都来自共同的根Any,因此,原则上容器内可以容纳任意不同类型的成员(尽管实际上很少这样做),例如:

scala> val x=List(1,3.4,"Spark")

x: List[Any] = List(1, 3.4, Spark) //1,3.4,“Spark”最近公共类型为Any

列表有头部和尾部的概念,可以分别使用head和tail方法来获取,例如,strList.head将返回字符串"BigData",strList.tail返回List ("Hadoop","Spark"),即head返回的是列表第一个元素的值,而tail返回的是除第一个元素外的其他值构成的新列表,这体现出列表具有递归的链表结构。正是基于这一点,常用的构造列表的方法是通过在已有列表前端增加元素,使用的操作符为“::”,例如:

scala> val otherList="Apache"::strList

其中,strList是前面已经定义过的列表,执行该语句后,strList保持不变,而otherList将成为一个新的列表 List("Apache","BigData","Hadoop","Spark")。注意,这里的“::”只是 List 类型的一个方法,而且Scala规定,当方法名以冒号结尾时,其作为操作符使用时,将执行“右结合”规则,因此,"Apache"::strList等效于strList.::("Apache")。Scala还定义了一个空列表对象Nil,借助Nil可以将多个元素用操作符::串起来初始化一个列表,例如:

scala> val intList = 1::2::3::Nil

该语句与val intList = List(1,2,3)等效,注意,最后的Nil是不能省略的,因为“::”是右结合的, 3是Int型,它并没有名为::的方法。

列表作为一种特殊的序列,可以支持索引访问,例如,上例的 strList(1)返回字符串"Hadoop"。但是需要注意的是,由于列表采用链表结构,因此,除了head、tail以及其他创建新链表的操作是常数时间O(1),其他诸如按索引访问的操作都需要从头开始遍历,因此是线性时间复杂度O(N)。为了实现所有操作都是常数时间,可以使用向量(Vector),例如:

scala> val vec1=Vector(1,2)

vec1: scala.collection.immutable.Vector[Int] = Vector(1, 2, 3)

scala> val vec2 = 3 +: 4 +: vec1

vec2: scala.collection.immutable.Vector[Int] = Vector(3, 4, 1, 2)

scala> val vec3 = vec2 :+ 5

vec3: scala.collection.immutable.Vector[Int] = Vector(3, 4, 1, 2, 5)

scala> vec3(3)

res6: Int = 2

上面语句中的“+:”和“:+”都是继承自特质Seq中的方法,用于向序列的前端和尾端添加新元素,注意以“:”结尾的方法是右结合的。

List和Vector都是不可变的,其包含的对象一旦确定就不能增加和删除。List和Vector对应的可变版本是ListBuffer和ArrayBuffer,这两个序列都位于scala.collection.mutable中。下面以ListBuffer为例子进行说明,ArrayBuffer的使用完全类似,只是其随机存储效率更高。

scala> import scala.collection.mutable.ListBuffer

scala> val mutableL1 = ListBuffer(10,20,30) //初始长度为3的变长列表

mutableL1: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20, 30)

scala> mutableL1 += 40 //在列表尾部增加一个元素40

res22: mutableL1.type = ListBuffer(10, 20, 30, 40)

scala> val mutableL2 = mutableL1:+50 //在列表尾部增加一个元素50,并返回这个新列表,原列表保持不变

mutableL2: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20, 30, 40, 50)

scala> mutableL1.insert(2, 60,40) //从第2个索引位置开始,插入60和40

scala> mutableL1

res24: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20, 60, 40, 30, 40)

scala> mutableL1 -= 40 //在数组中删除值为40的第一个元素

res25: mutableL1.type = ListBuffer(10, 20, 60, 30, 40)

scala> var temp=mutableL1.remove(2)//移除索引为2的元素,并将其返回

temp: Int = 60

scala> mutableL1

res26: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20, 30, 40)

上述代码中需要注意的是,“+:”方法会修改列表本身,而“+:”方法只是利用当前列表创建一个新的列表,并在其前端增加元素,当前列表本身并未改变。为了防止混淆,可以记住一个简单的规则:对于可变序列,包含等号“=”的方法都会直接修改序列本身,否则,就是创建新序列。例如,“++=”将另一个容器中的元素添加到列表后端,而“++”执行类似操作时,则只是返回新列表,并不会修改原列表。上述规则同样适用于下面将要介绍的可变集合和可变映射。

Range

Range类是一种特殊的、带索引的不可变数字等差序列,其包含的值为从给定起点按一定步长增长(减小)到指定终点的所有数值。可以使用两种方法创建一个 Range 对象,第一种方法是直接使用Range类的构造函数,例如:

scala> val r=new Range(1,5,1)

其中,第一个参数为起点,第二个参数为终点(终点本身不会被包含在创建得到的 Range 对象内),最后一个参数为步长。因此,上述语句创建的Range对象包括1、2、3和4共4个整数元素,可以使用从0开始的索引访问其元素,例如,r(2)的值是3,还可以分别使用start和end成员变量访问其起点和终点。

另一种构造Range的常用方法是使用数值类型的to方法,这种方法经常使用在for循环结构中。例如,“1 to 5”这个语句将生成一个从整数1到5的Range;如果不想包括区间终点,可以使用until方法,例如,“1 until 5”这个语句会生成1到4的Range;还可以设置非1的步长,例如,“1 to 5 by 2”这个语句将生成包含1、3和5的Range。

类似于整数的Range,还可以生成浮点值或字符型的等差序列。例如,“0.1f to 3f by 0.5f”将生成包含0.1、0.6、1.1、1.6的Range对象;“'a' to 'e' by 2”将生成包含’a'、'c'、'e’的Range对象。实际上,支持Range的类型包括Int、Long、Float、Double、Char、BigInt和BigDecimal等。

5.集合

Scala的集合(Set)是不重复元素的容器。相对于列表中的元素是按照索引顺序来组织的,集合中的元素并不会记录元素的插入顺序,而是以“哈希”方法对元素的值进行组织(不可变集在元素很少时会采用其他方式实现),所以,它可以支持快速找到某个元素。集合包括可变集和不可变集,分别位于scala.collection.mutable包和scala.collection.immutable包,缺省情况下创建的是不可变集。例如:

scala> var mySet = Set("Hadoop","Spark")

scala> mySet += "Scala"

其中,第一行创建集合的方法与创建数组和列表类似,通过调用Set的apply工厂方法来创建一个集合。第二行实际是一条赋值语句的简写形式,等效于 mySet=mySet+ "Scala",即调用了 mySet的名为+的方法,该方法返回一个新的 Set,将这个新的 Set 赋值给可变变量 mySet,因此,如果用val修饰这里的mySet,执行时将会报错。

如果要声明一个可变集,则需要提前引入scala.collection.mutable.Set。举例如下:

scala> import scala.collection.mutable.Set

scala> val myMutableSet = Set("Database","BigData")

scala> myMutableSet += "Cloud Computing"

可以看出,创建可变集的方法与创建不可变集是完全一样的。不过需要注意的是,这里创建可变集代码的第三行与上面创建不可变集代码的第二行,虽然看起来形式完全一样,但是,二者有着根本的不同。回忆上节介绍的等号规则,“+=”方法会直接在原集合上添加一个元素。这里变量myMutableSet引用本身并没有改变,因为其被val修饰,但其指向的集对象已经改变了。

6.映射

映射(Map)是一系列键值对的容器。在一个映射中,键是唯一的,但值不一定是唯一的。可以根据键来对值进行快速的检索。Scala提供了可变映射和不可变映射,分别定义在包scala.collection.mutable和scala.collection.immutable 里。默认情况下,Scala使用的是不可变映射。例如:

scala> val university = Map("XMU" ->"Xiamen University", "THU" ->"Tsinghua University","PKU"->"Peking University")

这里定义了一个从字符串到字符串的不可变映射,在 REPL 模式下,可以看到其类型为scala.collection.immutable.Map[String,String]。其中,操作符“->”是定义二元组的简写方式,它会返回一个包含调用者和传入参数的二元组,在该例中,即为(String,String)类型的二元组。

如果要获取映射中的值,可以通过键来获取。对于上述实例,university("XMU")将返回字符串"Xiamen University",对于这种访问方式,如果给定的键不存在,则会抛出异常,为此,访问前可以先调用 contains 方法来确定键是否存在,例如,在本例中,university.contains("XMU")将返回 true,但university.contains("Fudan")将返回false。更推荐的用法是使用get方法,它会返回Option[T]类型(见2.3.3节中关于Option的示例)。

对于不可变映射,不能添加新的键值对,也不能修改或者删除已有的键值对。对于可变映射,可以直接修改其元素。如果想使用可变映射,必须明确地导入scala.collection.mutable.Map。例如:

scala> import scala.collection.mutable.Map

scala> val university2 = Map("XMU" -> "Xiamen University", "THU" -> "Tsinghua University","PKU"->"Peking University")

scala> university2("XMU") = "Ximan University"

scala> university2("FZU") = "Fuzhou University"

scala> university2 += ("TJU"->"Tianjin University")

其中,第3条语句修改了键为“XMU”的已有元素,第4条语句通过修改不存在的键“FZU”,实现了添加新元素的目的,最后一行直接调用名为“+=”的方法增加新元素。映射的两个常用到的方法是keys和values,分别返回由键和值构成的容器对象。

7.迭代器

迭代器(Iterator)是一种提供了按顺序访问容器元素的数据结构。尽管构造一个迭代器与构造一个容器很类似,但迭代器并不是一个容器类,因为不能随机访问迭代器的元素,而只能按从前往后的顺序依次访问其元素。因此,迭代器常用于需要对容器进行一次遍历的场景。迭代器提供了两个基本操作:next和hasNext,可以很方便地实现对容器进行遍历。next返回迭代器的下一个元素,并从迭代器中将该元素抛弃,hasNext用于检测是否还有下一个元素。例如:

val iter = Iterator("Hadoop","Spark","Scala")

while (iter.hasNext) {

  println(iter.next())

}

该操作执行结束后,迭代器会移动到末尾,就不能再使用了,如果继续执行一次println(iter.next),就会报错,从这一点可以看出迭代器并不是一个容器,而有些类似C++中指向一个容器元素的指针,但该指针不能前后随意移动,只能逐次向后一个元素一个元素地移动。实际上,迭代器的大部分方法都会改变迭代器的状态,例如,调用 length 方法会返回迭代器元素的个数,但是,调用结束后,迭代器已经没有元素了,再次进行相关操作会报错。因此建议,除next和hasnext方法外,在对一个迭代器调用了某个方法后,不要再次使用该迭代器。