目录
一、基础数据结构
1. 变量定义
Scala有两种变量,var和val,val类似于Java里的final变量,一旦初始化了,val就不能被再赋值。相反,var如同java里的非final变量。
2.函数定义
一个简单的函数定义如下:
def max(x: Int, y: Int): Int = {
if (x > y) x
else y
}
函数的定义以def开始。然后是函数名,跟着括号里带有冒号分割的参数列表。函数的每个参数都必须带有前缀冒号的类型标注,以为Scala编译器无法推断函数的参数类型。有时候,Scala编译器需要函数结果类型的定义。比方说,如果函数是递归的,那么函数结果类型就必须被明确地说明。然而在max的例子里,即使不写结果类型函数编译器也能够推断它。同样,如果函数仅包含一个语句,那么连花括号都可以选择不写。这样,max函数就可以写成这样:
def max2(x: Int, y: Int) = if x > y x else y
3. scala数组
val greetStrings = new Array[String](3)
greetStrings(0) = "Hello"
greetStrings(1) = ","
greetStrings(2) = "world!\n"
与scala其他类一样,数组也只是类的实例。用括号传递给变量一个或多个值参数时,Scala会把它转换成对apply方法的调用。于是greetStrings(0)转换成greetStrings.apply(0)。数组的更简介写法:
val nums = Array("one", "two", "three")
Scala Array是定长的,如果要使用变长数组,可以使用ArrayBuffer:
val b = ArrayBuffer[Int]()
b += (1, 2, 3, 5)
b.insert(2, 6)
4. Scala List(列表)
尽管数组在实例化后长度固定,但它的元素值确实可变的,所以数组是可变的。而Scala List一旦创建了,就不可改变。
val list = List(1, 2, 3)
列表类中定义了":::"方法实现列表叠加功能:
val oneTwo = List(1, 2)
val threeFour = List(3, 4)
val oneTwoAndThreeFour = oneTwo:::threeFour
println("oneTwo: " + oneTwo)
println("threeFour: " + threeFour)
println("oneTwoAndThreeFour: " + oneTwoAndThreeFour)
列表中最常用的操作或许是"::",发音为"cons"。它可以把新元素组合到现有列表的最前端,然后返回作为执行结果的新列表:
val twoThree = List(2, 3)
val onwTwoThree = 1 :: twoThree
println(onwTwoThree)
表达式"1::twoThree"中,::是操作数twoThree的方法。如果方法使用操作符来标注,如a * b,那么左操作数是方法的调用者,可以改写成a.*(b),除非方法名以冒号结尾。在这种情况下,方法被右操作数调用,因此"1::twoThree"可以改写成: twoThree.::(1)。
Nil是空列表的简写,所以可以使用cons操作符把所有元素都串起来,并以Nil作结尾来定义新列表,例如可以用以下的方法产生与上文同样的输出:
val oneTwoThree = 1 :: 2 :: 3 :: Nil
println(oneTwoThree)
5. 元组(Tuple)
元祖也是很有用的容器对象。与列表一样,元祖也是不可变的;但与列表不同,元祖可以包含不同类型的元素。例如列表只能写成List[Int]或List[String],但元祖可以同时拥有List和String。
var pair = (10, "hello")
println(pair._1)
println(pair._2)
为什么不能用访问列表的方法来访问元组,如pair(0)。那是因为列表的apply方法始终返回同样的类型,但元组里的类型不尽相同。
6. 集(set)和映射(map)
Scala的api里包含了set的基本特质(trait),特质这个概念接近于java的接口。Scala还提供两个子特质,分别为可变set和不可变set,结构如下图:
var numSet = Set("one", "two")
numSet += "three"
println(numSet.contains("three"))
要加入新变量,可以对numSet调用+,并传入新元素。可变的和不可变的set都提供了+方法,但结果不同。可变set把元素加入自身,而不可变set则创建并返回包含了添加元素的新set。map的类继承机制看上去和set的很像:
7. 单例对象(singleton)
Scala比Java更为面向对象的特点之一是Scala不能定义静态成员,而是代之以定义单例对象。用object关键字替换class关键字。
class ChecksumAccumulator {
private var sum = 0
def add(b: Byte) { sum += b }
def checksum(): Int = ~(sum & 0xFF) + 1
}
// In file ChecksumAccumulator.scala
import scala.collection.mutable.Map
object ChecksumAccumulator {
private val cache = Map[String, Int]()
def calculate(s: String): Int =
if (cache.contains(s))
cache(s)
else {
val acc = new ChecksumAccumulator
for (c <- s)
acc.add(c.toByte)
val cs = acc.checksum()
cache += (s -> cs)
cs
}
}
上图中的单例对象叫ChecksumAccumulator,与它上面的类同名。当单例对象与与某个类共享一个名称时,它就被成为这个类的伴生对象。类和它的伴生对象必须定义在一个源文件里。类被成为是这个对象的伴生类。类和它的伴生对象可以互相访问其私有成员。不与伴生类共享名称的单例对象被称为独立对象。任何拥有合适签名的main方法的单例对象都可以用来作为程序的入口点。
二、程序逻辑控制
1. 内建控制结构
Scala 的if语句是能返回值的表达式:
val filename = if (!args.isempty(0)) args(0)
else "default.txt"
while和do-while结构之所以被称为“循环”,而不是表达式,是因为它们不能产生有意义的结果。结果的类型是Unit。
var line = ""
while (line = readLine()) != "") //不起作用
println("Read: " + line)
虽然,在Java里,赋值语句可以返回被赋予的那个值,在同样的情况下Scala的赋值语句只能得到Unit值()。
for表达式遍历枚举集合:
var filesHere = (new java.io.File(".")).listFiles
for (file <- filesHere)
println(file)
for表达式遍历Range类型:
for (i <- 1 to 4)
println("Iteration: " + i)
如果不想包含被枚举的Range的上边界,还可以用until代替to:
for (i <- 1 to 4)
println("Iteration: " + i)
for表达式的括号中添加过滤器(filter),即if字句,实现过滤某个子集:
var filesHere = (new java.io.File(".")).listFiles
for (file <- filesHere if file.getName.endsWith(".scala"))
println(file)
2. Scala函数和闭包
2.1 定义函数最通用的方法是作为某个对象的成员。这种函数被称为方法(method)
object LongLines {
def processFile(filename: String, width: Int) {
val source = Source.fromFile(filename)
for (line <- source.getLines)
processLine(filename, width, line)
}
private def processLine(filename: String,
width: Int, line: String) {
if (line.length > width)
println(filename +": "+ line.trim)
}
}
Scala可以把函数定义在别的函数之内。就像本地变量那样,这种称为本地函数:
def processFile(filename: String, width: Int) {
def processLine(filename: String,
width: Int, line: String) {
if (line.length > width)
print(filename +": "+ line)
}
val source = Source.fromFile(filename)
for (line <- source.getLines) {
processLine(filename, width, line)
}
}
Scala的函数是头等函数。你不仅可以定义和调用函数,还可以把他们写成匿名的字面量,并把它们作为值传递。函数字面量被编译进类,并在运行期实例化为函数值。因此函数字面量和值的区别在于函数字面量存在于源代码,而函数值作为对象存在于运行期。
2.2 占位符语法
如果想让函数字面量尽量保持简洁,可以把下划线当作一个或多个参数的占位符,只要每个参数在函数字面量内仅出现一次。
someNumbers.filter(_ > 0)
部分应用函数是一种表达式,你不需要提供函数需要的所有参数。代之仅仅以提供部分,或不提供所需参数:
def sum(a: Int, b: Int, c: Int) = a + b + c
val a = sum _
2.3 闭包
依照函数字面量在运行时创建的函数值(对象)被称为闭包:closure。名称源自于通过”捕获“自由变量的绑定,从而对函数字面量执行的”关闭“行动。不带自由变量的函数字面量,如(x: Int) => x + 1,被称为封闭项,这里项指的是一小部分源代码。因此依照这个函数字面量在运行时创建的函数值严格意义上来讲就不是闭包。因为(x: Int) => x + 1 在编写的时候就已经封闭了。
scala> var more = 1
val addMore = (x: Int) => x + more
3. 函数柯里化
柯里化的函数被应用于多个参数列表,而不是仅仅一个。
def curriedSum(x: Int)(y: Int) = x + y
curriedSum: (x: Int)(y: Int)Int
scala> curriedSum(1)(2)
res0: Int = 3
当你调用curriedSum函数时,实际上接连调用了两个传统函数。第一个函数调用带单个的名为x的Int参数,并返回第二个函数的函数值。第二个函数带Int参数y。
4. 偏函数(PartialFunction)
scala 偏函数的作用是,对于传入的参数,可以决定是否接受,官方定义如下:
- 对给定的输入参数类型,函数可接受该类型的任何值。换句话说,一个(Int) => String 的函数可以接收任意Int值,并返回一个字符串。
- 对给定的输入参数类型,偏函数只能接受该类型的某些特定的值。一个定义为(Int) => String 的偏函数可能不能接受所有Int值为输入。
在Scala中,所有偏函数的类型皆被定义为PartialFunction[-A, +B]类型。由于它仅仅处理输入参数的部分分支,因而它通过isDefineAt()来判断输入值是否应该由当前偏函数进行处理。PartialFunction的定义如下所示:
trait PartialFunction[-A, +B] extends (A => B) { self =>
import PartialFunction._
def isDefinedAt(x: A): Boolean
def applyOrElse[A1 <: A, B1 >: B](x: A1, default: A1 => B1): B1 =
if (isDefinedAt(x)) apply(x) else default(x)
}
其isDefinedAt()方法返回true或者false,该方法决定了是否接受传入的参数,其apply方法决定了具体怎么处理这个值,在实际场景中通常使用case语句来达到这个效果,因为在需要传入偏函数的地方,使用case语句,它会被自动编译成偏函数。
三、组合与继承
1. 抽象类定义
Scala定义抽象类是在类定义前面加关键字abstract:
// A first (faulty) design of the Currency class
abstract class Currency {
val amount: Long
def designation: String
override def toString = amount +" "+ designation
}
new Currency {
val amount = 79L
def designation = "USD"
}
一个方法只要没有实现(即没有等号或方法体),它就是抽象的。不像Java,方法的声明中不需要(也不允许)有抽象修饰符。拥有实现的方法被称为具体的方法,但是在trait(特质)定义类里可以有抽象修饰符。
Scala鼓励使用将不带参数且没有副作用的方法定义为无参数方法的的风格,即省略空括号。但是永远不要定义没有括号的带副作用的方法。
2. Scala 类层级
Scala层级的顶端是Any类,定义了下列的方法:
因为每个类都继承自Any,所以Scala程序里的每个对象都能用==、!=或equals()比较。根类Any有两个子类:AnyVal和AnyRef。AnyVal是Scala里每个内建值类的父亲。包括8个基本类型和Unit。AnyRef是Scala里所有引用类的基类。
四、Scala 高级特性
1. 下划线与星号(_*)的作用
1.1 变长参数
例如定义一个变长参数的方法sum,然后计算1-5的和,可以写为
scala> def sum(args: Int*) = {
| var result = 0
| for (arg <- args) result += arg
| result
| }
sum: (args: Int*)Int
scala> val s = sum(1,2,3,4,5)
s: Int = 15
但是如果使用这种方式就会报错
scala> val s = sum(1 to 5)
<console>:12: error: type mismatch;
found : scala.collection.immutable.Range.Inclusive
required: Int val s = sum(1 to 5)
这种情况必须在后面写上: _*将1 to 5转化为参数序列:
scala> val s = sum(1 to 5: _*)
s: Int = 15
1.2 变量声明中的模式
例如,下面代码分别将arr中的第一个和第二个值赋给first和second
scala> val arr = Array(1,2,3,4,5)
arr: Array[Int] = Array(1, 2, 3, 4, 5)
scala> val Array(1, 2, _*) = arr
scala> val Array(first, second, _*) = arr
first: Int = 1
second: Int = 2
2. 样本类
带有case修饰符的类被称为样本类,这种修饰符可以让Scala编译器自动为你的类添加一些句法上的便捷设定。首先,它会添加与类名一致的工厂方法,即新建对象时可以取消new关键字。第二个便捷设定是样本类参数列表中的所有参数隐式获得了val前缀。
case class Video (val videoId: String, val uploader: String, age: Int) {
}
3. Scala参数化字段
3.1 scala在定义类的构造方法时,可以指定任意个数的参数,并且类没有主体时,就不需要指定一对空的花括号。一个简单的实例如下:
class Rational(n: Int, d: Int) {
override def toString = n +"/"+ d
}
object Main {
def main(args: Array[String]) {
val oneHalf = new Rational(1, 2)
println("oneHalf [" + oneHalf + "]")
}
Rational类重写了toString方法,可是我们在类里并没有申明n和d字段,那它是怎么工作的呢?其实是scala编译器帮我们做了这一切,反编译Rational.class文件的类结构如下:
public class Rational{
private final int n;
private final int d;
public String toString()
{
return new StringBuilder()
.append(this.n).append("/")
.append(BoxesRunTime.boxToInteger(this.d)).toString();
}
}
可以看到类里多了两个final成员字段:n和d。再向类里添加一个merge方法:
class Rational(n: Int, d: Int) {
override def toString = n +"/"+ d
def merge(that: Rational): Rational = {
new Rational(n + that.n, d + that.d)
}
}
如果直接运行merge方法会报错,因为类成员n和d是private类型。
3.2 参数化字段:scala的参数化字段是在类的构造参数字段前面加val前缀,示例如下:
class Rational(val n: Int, val d: Int) {
override def toString = n +"/"+ d
def merge(that: Rational): Rational = {
new Rational(n + that.n, d + that.d)
}
}
此时的merge方法方法调用便不再会报错,查看反编译的Rational.class文件结构如下:
public class Rational
{
private final int n;
private final int d;
public int n() { return this.n; }
public int d() { return this.d; }
public String toString()
{
return new StringBuilder().append(n())
.append("/").append(BoxesRunTime.boxToInteger(d())).toString();
}
public Rational merge(Rational that)
{
return new Rational(n() + that.n(), d() + that.d());
}
}
从上图中可以看到,在定义了参数化字段后,scala在编译生成class文件时,不仅增加了成员字段,同时还增加了其获取值的方法,源文件的字段访问在编译成class文件时都变成了方法调用。此时merge方法可以正常运行。
总结:scala的参数化字段就是在定义类的构造参数时,在其前面添加val前缀。这样scala会自动为这些参数生成类的成员字段,同时生成对应的取其值的方法。我们在scala源码类文件里,不用再定义额外的字段来保存类的构造参数,直接使用即可。
4. Scala 隐式转换
通过隐式转换,程序员可以在编写Scala程序时故意漏掉一些信息,让编译器去尝试在编译期间自动推导出这些信息来,这种特性可以极大的减少代码量,忽略那些冗长,过于细节的代码。
4.1 使用方式:
- 1). 将方法或变量标记为implicit。
- 2). 将方法的参数列表标记为implicit。
- 3). 将类标记为implicit
4.2 Scala支持两种形式的隐式转换:
- 隐式值:用于给方法提供参数
- 隐式视图:用于类型间转换或使针对某类型的方法能调用成功
4.3 用implicit关键字定义一个隐式变量后,编译器会在方法省略隐式参数的情况下去搜索作用域内的隐式值作为缺少参数:
object ImplictConvert {
def main(args: Array[String]): Unit = {
implicit val p = "tom"
person
}
def person(implicit name: String) = name
}
4.4 隐式视图: 隐式转换为目标类型:把一种类型自动转换到另一种类型
object ImplictConvert {
implicit def intToString(x : Int) = x.toString
def main(args: Array[String]): Unit = {
foo(10)
}
}
4.5 隐式转换调用类中本不存在的方法:
class SwingType{
def wantLearned(sw : String) = println("兔子已经学会了"+sw)
}
object swimming{
implicit def learningType(s : AnimalType) = new SwingType
}
class AnimalType
object AnimalType extends App{
import swimming._
val rabbit = new AnimalType
rabbit.wantLearned("breaststroke") //蛙泳
}
4.6 隐式类
在scala2.10后提供了隐式类,可以使用implicit声明类,但是需要注意以下几点:
- 1). 其所带的构造参数有且只能有一个
- 2). 隐式类必须被定义在类,伴生对象和包对象里
- 3). 隐式类不能是case class(case class在定义会自动生成伴生对象与2矛盾)
- 4). 作用域内不能有与之相同名称的标示符
object Stringutils {
implicit class StringImprovement(val s : String){ //隐式类
def increment = s.map(x => (x +1).toChar)
}
}
object Main extends App{
import Stringutils._
println("mobin".increment)
}
4.7 事实上 Scala 标准库中提供了一个方法让编译器可以自己寻找到一个类的隐式变量:
def implicitly[T](implicit t: T) = t
例如一个类型 Foo,调用implicitly[Foo]会产生什么结果呢?首先编译器会寻找 Foo 的隐式对象,找到以后调用这个对象的 implicitly 方法,然后返回该对象,所以通过implicitly[Foo]可以返回 Foo 的隐式对象
5. Scala中==,eq与equals的区别
根据官方API的定义:
- final def ==(arg0: Any): Boolean The expression x == that is equivalent to if (x eq null) that eq null else x.equals(that).
- final def eq(arg0: AnyRef): Boolean Tests whether the argument (that) is a reference to the receiver object (this).
- def equals(arg0: Any): Boolean The equality method for reference types.
简言之,equals方法是检查值是否相等,而eq方法检查的是引用是否相等。所以如果比较的对象是null那么==调用的是eq,不是null的情况调用的是equals。 看一个简单的例子: 在java中如果要对两个对象进行值比较,那么必须要实现equals 和hashCode方法。而在scala中为开发者提供了case class,默认实现了equals 和hashCode方法。
而对于Array或者Map对象不能简单点使用equals进行值比较,要通过sameElements
方法。