Scala 快速入门

1 初识 Scala

Scala 与 Java 的 Lambda 表达式、Stream 以及函数式编程相似

可以与 Java 进行无缝互操作,可以导入 Java 中的包

// 同一行中导入包
import java.util.{Date, Locale}
import java.text.DateFormat

// Scala 使用下划线(underscore, _)而不是星号( asterisk, *),这是因为,在 Scala
//中,星号是一个合法的标识符(比如:方法名)
import java.text.DateFormat._
object FrenchDate {
def main(args: Array[String]) {
val now = new Date
val df = getDateInstance(LONG, Locale.FRANCE)
	// 如果一个方法只接收一个参数,那么可以使用 infix 语法, 例如 df format now 等效于 df.format(now)
	println(df format now)
	}
}

Scala 中可以直接继承 Java 的类或者实现 Java 的接口

1.1 定义变量和函数

Scala 有两类变量:val 和 var。val 等同于 Java 中的 final 变量,一旦被初始化,就不能再被重新赋值了。相反地,var 是非 final 变量,可以重复被赋值。

变量后面可以跟“冒号 + 类型”,以显式标注变量的类型。如果不写“:Long”,也是可以的,因为 Scala 可以通过后面的值“1L”自动判断出 a 的类型。显式标注变量类型,可以让代码有更好的可读性和可维护性。

var a:Long = 1L

函数定义

def max(x: Int, y: Int): Int = {
  if (x > y) x
  else y
}

def 关键字表示这是一个函数。max 是函数名,括号中的 x 和 y 是函数输入参数,它们都是 Int 类型的值。结尾的“Int =”组合表示 max 函数返回一个整数。

Scala 中没有显式 return 关键字返回结果,在 Scala 中,函数体具体代码块最后一行的值将被作为函数结果返回。在这个例子中,if 分支代码块的最后一行是 x,因此,此路分支返回 x。同理,else 分支返回 y。

def deleteIndicesIfExist(
  // 这里参数suffix的默认值是"",即空字符串
  // 函数结尾处的Unit类似于Java中的void关键字,表示该函数不返回任何结果
  baseFile: File, suffix: String = ""): Unit = {
  info(s"Deleting index files with suffix $suffix for baseFile $baseFile")
  val offset = offsetFromFile(baseFile)
  Files.deleteIfExists(Log.offsetIndexFile(dir, offset, suffix).toPath)
  Files.deleteIfExists(Log.timeIndexFile(dir, offset, suffix).toPath)
  Files.deleteIfExists(Log.transactionIndexFile(dir, offset, suffix).toPath)
}

上面的例子中,有两个语法特性:

第一个特性是参数默认值,这是 Java 不支持的。这个函数的参数 suffix 默认值是空字符串,因此,以下两种调用方式都是合法的:

deleteIndicesIfExist(baseFile) // OK
deleteIndicesIfExist(baseFile, ".swap") // OK

第二个特性是该函数的返回值 Unit。Scala 的 Unit 类似于 Java 的 void,因此,deleteIndicesIfExist 函数的返回值是 Unit 类型,表明它仅仅是执行一段逻辑代码,不需要返回任何结果。

1.2 定义元祖

**元组是承载数据的容器,一旦被创建,就不能再被更改了。**元组中的数据可以是不同数据类型的。定义和访问元组的方法很简单


scala> val a = (1, 2.3, "hello", List(1,2,3)) // 定义一个由4个元素构成的元组,每个元素允许是不同的类型
a: (Int, Double, String, List[Int]) = (1,2.3,hello,List(1, 2, 3))

scala> a._1 // 访问元组的第一个元素
res0: Int = 1

scala> a._2 // 访问元组的第二个元素
res1: Double = 2.3

scala> a._3 // 访问元组的第三个元素
res2: String = hello

scala> a._4 // 访问元组的第四个元素
res3: List[Int] = List(1, 2, 3)

1.3 循环

常见的循环有两种写法:命令式编程方式和函数式编程方式。我们熟悉的是第一种,比如下面的 for 循环代码:

scala> val list = List(1, 2, 3, 4, 5)
list: List[Int] = List(1, 2, 3, 4, 5)

scala> for (element <- list) println(element)
1
2
3
4
5

Scala 支持的函数式编程风格的循环,类似于下面的这种代码:

scala> list.foreach(e => println(e))
// 省略输出......
scala> list.foreach(println)
// 省略输出......

2 一切皆对象

2.1 数字是对象

数字 1. 会被解释器当成 double 1.0 , 加括号的 (1). 当作整数

2.2 函数是对象

将函数当做值进行操作的能力,是函数式编程( functional programming)最重要的特性( cornerstone,基石)之一。

// 定时器 oncePerSecond 参数为一个函数,通过使用该函数完成操作
object Timer {
// () => Unit,代表任何无参数、无返回值的函数
def oncePerSecond(callback: () => Unit) {
	while (true) { callback(); Thread sleep 1000 }
	}
def timeFlies() {
	System.out.println("time flies like an arrow...")
	}
def main(args: Array[String]) {
	oncePerSecond(timeFlies)
	}
}

2.2.1 匿名函数 Anonymous functions

函数在用到的时候即时构造,可以省去定义和命名的麻烦,在 Scala 中,这样的函数称为匿名函数( anonymous
functions),也就是没有名字的函数。

object TimerAnonymous {
def oncePerSecond(callback: () => Unit) {while (true) { callback(); Thread sleep 1000 }
	}
def main(args: Array[String]) {
	// 代码中的右箭头‘=>’表明程序中存在一个匿名函数,箭头左边是匿名函数的参数列表,
	// 右边是函数体。在本例中,参数列表为空(箭头左边是一对空括号)
	oncePerSecond(() =>
		println("time flies like an arrow..."))
	}
}

2.3 Option 对象

不论是 Scala 中的 Option,还是 Java 中的 Optional,都是用来帮助我们更好地规避 NullPointerException 异常的。

Option 表示一个容器对象,里面可能装了值,也可能没有装任何值。由于是容器,因此一般都是这样的写法:Option[Any]。中括号里面的 Any 就是上面说到的 Any 类型,它能是任何类型。如果值存在的话,就可以使用 Some(x) 来获取值或给值赋值,否则就使用 None 来表示。

scala> val keywords = Map("scala" -> "option", "java" -> "optional") // 创建一个Map对象
keywords: scala.collection.immutable.Map[String,String] = Map(scala -> option, java -> optional)

scala> keywords.get("java") // 获取key值为java的value值。由于值存在故返回Some(optional)
res24: Option[String] = Some(optional)

scala> keywords.get("C") // 获取key值为C的value值。由于不存在故返回None
res23: Option[String] = None

Option 对象还经常与模式匹配语法一起使用,以实现不同情况下的处理逻辑。比如,Option 对象有值和没有值时分别执行什么代码。


def display(game: Option[String]) = game match {
  case Some(s) => s
  case None => "unknown"
}

scala> display(Some("Heroes 3"))
res26: String = Heroes 3

scala> display(Some("StarCraft"))
res27: String = StarCraft

scala> display(None)
res28: String = unknown

3 类

Scala中的类定义可以带参数( parameters)

// 复数类,可以接受两个参数,分别代表复数的实部和虚部
class Complex(real: Double, imaginary: Double) {
	def re() = real
	def im() = imaginary
}

创建 Complex 类的实例,则必须提供这两个参数,比如: new Complex(1.5, 2.3)。该类有两个方法: re 和 im,分别用于访问复数的实部和虚部。

3.1 无参数方法

Complex 类中的 re 和 im 方法有个小问题,那就是调用这两个方法时,需要在方法名后面跟上一对空括号

object ComplexNumbers {
	def main(args: Array[String]) {
		val c= new Complex(1.2, 3.4)
		println("imaginary part: " + c.im())
		}
}

省掉这些方法后面的空括号,就像访问类属性( fields)一样访问类的方法,则程序会更加简洁。这在 Scala 中是可行的,只需将方法显式定义为没有参数( without arguments)即可。无参方法和零参方法( methods with zero arguments)的差异在于: 无参方法在声明和调用时,均无须在方法名后面加括号。所以,前面的 Complex 类可以重写如下:

class Complex(real: Double, imaginary: Double) {
	def re = real
	def im = imaginary
}

3.2 继承和方法重写 Inheritance and overriding

Scala 中的所有类都继承自某一个父类(或者说超类, super-class),若没有显式指定父类(比如前面的 Complex 类),则默认继承自 scala.AnyRef。

在 Scala 中可以重写( overriding)从父类继承的方法,但必须使用 override 修饰符来显式声明,这样可以避免无意间的方法覆盖( accidental overriding)。例如,前面定义的 Complex 类中,我们可以重写从 Object 类中继承的 toString 方法

class Complex(real: Double, imaginary: Double) {
	def re = real
	def im = imaginary
	override def toString() =
		"" +re + (if (im < 0) "" else "+") + im + "i"
}

4 条件类和模式匹配 Case classes and pattern matching

4.1 case 类

在 Scala 中,case 类与普通类是类似的,只是它具有一些非常重要的不同点。Case 类非常适合用来表示不可变数据。同时,它最有用的一个特点是,case 类自动地为所有类字段定义 Getter 方法,这样能省去很多样本代码。

编写一个类表示平面上的一个点,Java 代码大概如下:


public final class Point {
  private int x;
  private int y;
  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }
  // setter methods......
  // getter methods......
}

如果用 Scala 的 case 类,只需要写一行代码就可以了:

case class Point(x:Int, y: Int) // 默认写法。不能修改x和y
case class Point(var x: Int, var y: Int) // 支持修改x和y

4.2 模式匹配

和 Java 中 switch 仅仅只能比较数值和字符串相比,Scala 中的 match 要强大得多。

// x 是 Any 类型,这相当于 Java 中的 Object 类型,即所有类的父类。
def describe(x: Any) = x match {
  case 1 => "one"
  case false => "False"
  case "hi" => "hello, world!"
  case Nil => "the empty list"
  case e: IOException => "this is an IOException"
  case s: String if s.length > 10 => "a long string"
  case _ => "something else"
}

4.2 使用条件类和模式匹配例子

树是软件开发中使用频率很高的一种数据结构,例如:解释器和编译器内部使用树来表示代码结构; XML 文档是树形结构;还有一些容器(集合, containers)也是基于树的,比如:红黑树( red-black tree,一种自平衡二叉查找树)。

示例展示如何实现一个变量和常量的加法器,如 1 + 2、 (x + x) + (7 + y)

表示这样的表达式。最自然的选择是树形结构,用非叶子节点表示操作符(具体到这个例子,只有加法操作),用叶子节点表示操作数(具体到这个例子是常量和变量)。

如果是在 Java 中,建立树形结构最常见的做法是:创建一个表示树的抽象类,然后每种类型的节点用一个继承自抽象类的子类来表示。而在函数式编程语言中,则可以使用代数数据类型( algebraic data-type)来达到同样的目的。 Scala 则提供了一种介于两者之间(类继承和代数数据类型),被称为条件类( case classes)的概念。

abstract class Tree
case class Sum(l: Tree, r: Tree) extends Tree
case class Var(n: String) extends Tree
case class Const(v: Int) extends Tree

上例中的 Sum, Var和 Const就是条件类,它们与普通类的差异主要体现在如下几个方面:

  • 新建条件类的实例,无须使用 new 关键字(比如,可以直接用 Const(5)代替 new Const(5)来创建实例)。
  • 自动为构造函数所带的参数创建对应的 getter 方法(也就是说,如果 c 是 Const 的实例,通过 c.v 即可访问构造函数中的同名参数 v 的值)
  • 条件类都默认实现 equals 和 hashCode 两个方法,不过这两个方法都是基于实例的结构本身(structure of instance), 而不是基于实例中可用于区分的值(identity),这一点和 java 中 Object提供的同名方法的默认实现是基本一致的。
  • 条件类还提供了一个默认的 toString 方法,能够以源码形式(source form)打印实例的值(比如,表达式 x+1 会被打印成Sum(Var(x),Const(1)),这个打印结果,和源代码中创建表达式结构树的那段代码完全一致)。
  • 条件类的实例可以通过模式匹配( pattern matching)进行分解(decompose)

定义一个在特定环境( environment,上下文)中对表达式进行求值的函数,其中环境的作用是为了确定表达式中的变量的取值。例如:有一个环境,对变量 x 的赋值为 5,我们记为: {x → 5},那么,在这个环境上求 x + 1的值,得到的结果为 6。

实际上,环境就是一个给变量赋予特定值的函数。上面提到的环境: {x → 5},在 Scala 中可以写成:{ case "x" => 5 }

对环境的类型( type of the environments)进行命名。虽然在程序中全都使用 String => Int 这种写法也可以的,但给环境起名后,可以简化代码,并使得将来的修改更加方便(这里说的环境命名,简单的理解就是宏,或者说是自定义类型)。

type Environment = String => Int

此后,类型名 Environment 可以作为“从 String 转成 Int” 这一类函数的别名。

现在,我们来写求值函数。求值函数的实现思路很直观:两个表达式之和( sum),等于分别对两个表达式求值然后求和;变量的值直接从环境中获取;常量的值等于常量本身。在 Scala 中描述这个概念并不困难:

def eval(t: Tree, env: Environment): Int = t match {
	case Sum(l, r) => eval(l, env) + eval(r, env)
	case Var(n) => env(n)
	case Const(v) => v

求值函数的工作原理是对树t上的结点进行模式匹配,下面是对匹配过程的详细描述(实际上是递归):

  1. 求值函数首先检查树 t 是不是一个求和( Sum),如果是,则把 t 的左子树和右子树分别绑定到两个新的变量 l 和 r 上,然后对箭头右边的表达式进行运算(实际上就是分别求左右子树的值然后相加,这是一个递归)。箭头右边的表达式可以使用箭头左边绑定的变量,也就是 l 和 r。
  2. 如果第一个检查不满足,也就是说,树 t 不是 Sum,接下来就要检查 t 是不是一个变量 Var;如果是,则 Var 中包含的名字被绑定到变量 n 上,然后继续执行箭头右边的逻辑。
  3. 如果第二个检查也不满足,那意味着树 t 既不是 Sum,也不是 Var,那就进一步检查 t 是不是常量 Const。如果是, 则将常量所包含的值赋给变量 v,然后继续执行箭头右边的逻辑。
  4. 最后,如果以上所有的检查都不满足,程序会抛出异常,表明对表达式做模式匹配时产生了错误。这种情况,在本例中,只有声明了更多 Tree 的子类,却没有增加对应的模式匹配条件时,才会出现。

由以上可以总结出:模式匹配的过程,实际上就是把一个值( value)和一系列的模式进行比对,如果能够匹配上,则从值( value)中取出有用的部件( parts)进行命名,然后用这些命名的部件(作为参数)来驱动另一段代码的执行。

为了更深入的探索模式匹配,我们要在算术表达式上定义一个新的操作:对符号求导( symbolic derivation,导数)。该操作的规则如下:

  1. 对和求导,等于分别求导的和。 the derivative of a sum is the sum of the derivatives
  2. 对变量 v 求导,有两种情况:如果变量 v 正好是用于求导的符号,则返回 1,否则返回 0。
  3. 常量求导恒为 0。
def derive(t: Tree, v: String): Tree = t match {
	case Sum(l, r) => Sum(derive(l, v), derive(r, v))
	case Var(n) if (v == n) => Const(1)
	case _ => Const(0)
}

通过求导函数的定义,又引出了两个跟模式匹配相关的知识点。

第一、 case 语句可以带一个 guard,它由关键字 if 和紧随其后的表达式组成。 guard 的作用是对 case 匹配的模式进行二次限定,只有 if 后面的表达式为 true 时,才允许匹配成功。

第二、模式匹配中可以使用通配符(记为_, 下划线)来匹配任意值(相当于 java 中 switch 语句的 default 分支)。

定义的函数如何使用,先创建一个表达式: (x + x) + (7 + y),然后在环境{x → 5, y → 7}上求表达式的值,最后分别求表达式相对于 x 和 y 的导数。

def main(args: Array[String]) {
	val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
	val env: Environment = { case "x" => 5 case "y" => 7 }
	println("Expression: " + exp)
	println("Evaluation with x=5, y=7: " + eval(exp, env))
	println("Derivative relative to x:\n " + derive(exp, "x"))
	println("Derivative relative to y:\n " + derive(exp, "y"))
}

输出如下:

Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y)))
Evaluation with x=5, y=7: 24
Derivative relative to x:
Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0)))
Derivative relative to y:
Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))

5 Traits(特征、特性)

Scala 中的类不但可以从父类继承代码( code),还可以从一个或者多个 traits 引入代码。

对于 Java 程序员来说,理解 traits 最简单的方法,是把它当作可以包含代码逻辑的接口( interface)。在 Scala 中,如果一个类继承自某个 trait,则该类实现了 trait 的接口,并继承了 trait 的所有代码( code)。

6 泛型 Genericity

所谓泛型,就是代码中可以使用参数化类型的能力。例如,有一个负责类库开发的程序员,他想提供一个链表( linked list)结构,他必须决定,在链表中可以存放什么类型的元素。由于链表可以被广泛使用,所以预先限定链表中元素的类型是不现实的,即便勉强作出武断的决定,势必会给类库的应用带来极大的局限性。

定义一个引用类,引用( reference)是最简单的容器,可以指向某种类型的对象,或者为空(什么都不指向)。

class Reference[T] { 
	 // _表示各种类型的默认值,其中,数字类型的默认值是 0, Boolean 型的默认
	// 值是 false, Unit 类型是(),而所有的对象类型(object type)的默认值为 null。
	private var contents: T = _
	def set(value: T) { contents = value }
	def get: T = contents 
	}

Reference 是参数化类, T 是类型参数。类型参数在类中用于定义变量 contents 的类型、用作 set 方法的参数以及 get 方法的返回值。

要使用 Reference 类,需要指定类型参数 T 的具体类型,也就是被引用的对象的类型。

object IntegerReference {
	def main(args: Array[String]) {
		val cell = new Reference[Int]
		cell.set(13)
		println("Reference contains the half of " + (cell.get*2))
		}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值