0%

七周七语言之-Scala

简介

Scala 是一门多范式(multi-paradigm)的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。Scala 运行在Java虚拟机上,并兼容现有的Java程序。Scala 源代码被编译成Java字节码,所以它可以运行于JVM之上,并可以调用现有的Java类库。

Scala编译器的主要作者Martin Odersky也是Java编译器的开发者,他认为Scala是在面向对象之间搭起一座桥梁。所以不要把Scala当作一个完全陌生的语言与看待!他有着和Java类似强类型约束,有着Js类似的匿名箭头函数,类似C系语言的语法风格,类似Ruby的绝对面向对象,还有着和python等脚本语言一样灵活的高级数据结构,如果你有过这些语言的基础,学习Scala会是一个得心应手的过程。

开始

Archlinux下sudo pacman -S scala sbt安装sbtscala包,需要已有Java运行环境。

Scala和源代码文件名为*.scala,使用scalac编译后产生同Java一样的*.class字节码文件,scala还提供了交互式编程环境,可以直接输入scala或者sbt shell启动scala shell,类似如下的界面:

╭─xuranus@Thanos ~/githubProjects
╰─$ sbt console
[warn] No sbt.version set in project/build.properties, base directory: /home/xuranus/githubProjects
[info] Loading global plugins from /home/xuranus/.sbt/1.0/plugins
[info] Set current project to githubprojects (in build file:/home/xuranus/githubProjects/)
[info] Starting scala interpreter...
Welcome to Scala 2.12.10 (OpenJDK 64-Bit Server VM, Java 13.0.2).
Type in expressions for evaluation. Or try :help.

scala>

接下里你可以直接在控制台里尝试本文的案例,你的第一个scala程序:HelloWorld

scala> println("hello world")
hello world
scala> System.out.println("hello world")
hello world

我们用两种不同的方式输出了“hello world”,第一种是scala推荐的方式,第二中是调用了java的原生的方法。这说明scala可以调用java的api。

基础语法

基本数据类型

scala> val a = 10
a: Int = 10

scala> val b = 10.5
b: Double = 10.5

scala> val c = "Hello"
c: String = Hello

scala> val d:Int = 20
d: Int = 20

scala> var e:Int = 30
e: Int = 30

scala> c + d
res14: String = Hello20

scala> 3 * "Hello"
<console>:12: error: overloaded method value * with alternatives:
(x: Double)Double <and>
(x: Float)Float <and>
(x: Long)Long <and>
(x: Int)Int <and>
(x: Char)Int <and>
(x: Short)Int <and>
(x: Byte)Int
cannot be applied to (String)
3 * "Hello"
^

Scala使用varval来申明一个量,和Java一样支持所有的基本数据类型,Scala同Java一样是强类型语言,运算的类型之间必须匹配才能运算,否则会错误。Scala支持声明变量的时候无须声明类型,因为Scala支持静态类型推导,不同于解释性语言,Scala的类型检查在编译期完成。(Java8之后也加入了var关键字,同样支持了静态类型推导,C++11的auto关键字也是如此)

scala> d = 21
<console>:12: error: reassignment to val
d = 21
^

scala> e = 31
e: Int = 31

var声明的是变量,可以修改,val声明的不可以修改(对应Java中的final)。但是Scala编程应该尽可能使用不变量,结合Scala的函数编程特性,可以有效提高并发性能。

基本运算符

scala> 1+1
res5: Int = 2

scala> 1.+(1)
res6: Int = 2

scala> 3*(5+6)
res7: Int = 33 ^

scala> 3.*(5.+(6))
res8: Int = 33

我们可以看到,scala的数值运算和其他高级语言无异同,但其原理却大有文章!scala并没有像Java一样特殊处理了基础类型(int,float之类)和运算符,Java在基本数据类型运算中没能做到完全的面向对象,所以Java不是一门完全面向对象的语言,如果完全面向对象,a + b应该被写作a.add(b)

但是这样显然很不直观,scala为了在完全面向对象的同时,继续保持运算符的优点,scala的运算符实际上是一种语法糖1 + 1实际上的含义是1.+(1).+()类似.add()实际上是一个方法,由于scala单个参数可以省略括号和点,于是最后的形式还是上述那样。

Java在原生类型和对象直接要做封包和解包,例如Integer和int,而scala真正做到了万物皆对象。

表达式与条件

scala> 1 < 3
res16: Boolean = true

scala> val a = (2 > 1)
a: Boolean = true

scala> a
res17: Boolean = true

scala> if(a) {
| print("this is true")
| } else {
| print("this is false")
| }
this is true

Scala的逻辑语句,类似C语言风格,清晰易懂,无需赘述。需要注意的是他对空值和0的逻辑判断:

scala> Nil
res19: scala.collection.immutable.Nil.type = List()

scala> NULL
<console>:12: error: not found: value NULL
NULL
^

scala> if(0) {print("True")}
<console>:12: error: type mismatch;
found : Int(0)
required: Boolean
if(0) {print("True")}
^

scala> if(Nil) {print("True")}
<console>:12: error: type mismatch;
found : scala.collection.immutable.Nil.type
required: Boolean
if(Nil) {print("True")}
^

Scala中的空值不是NULL,而是Nil,一个空列表。且不同于其他语言,Scala严谨的强类型特性不允许在逻辑判断中使用0/1/Nil代替布尔值。

循环结构‘

  1. while循环:

    var i = 1
    while(i < 3) {
    println(i)
    i += 1
    }

    输出:

    1
    2
  2. for循环:

    for(j <- 0 until 5) {
    println(j)
    }

    输出

    0
    1
    2
    3
    4
    5

范围与元组

scala支持类似ruby的特性:范围range

scala> val range = 0 until 10
range: scala.collection.immutable.Range = Range 0 until 10

scala> range.start
res3: Int = 0

scala> range.end
res4: Int = 10

scala> range.step
res5: Int = 1

scala> (0 to 10)
res6: scala.collection.immutable.Range.Inclusive = Range 0 to 10

scala> (0 to 10) by 5
res7: scala.collection.immutable.Range = Range 0 to 10 by 5

scala> ((0 to 10) by 5).start
res8: Int = 0

scala> ((0 to 10) by 5).end
res9: Int = 10

scala> ((0 to 10) by 5).step
res10: Int = 5

scala> (0 until 10) by 5
res11: scala.collection.immutable.Range = Range 0 until 10 by 5

scala> ((0 until 10)).end
res12: Int = 10

scala> 'a' to 'e'
res14: scala.collection.immutable.NumericRange.Inclusive[Char] = NumericRange a to e

range可以改变范围变化的方向:
scala> for(i <- (10 until 0) by -1) {
| print(i)
| }
10987654321

类似Prolog,Scala支持元组(tuple)—一个固定长度的对象集合,在函数式编程中表示对象的属性,例如:
scala> val person = ("Elvis","Presley")
person: (String, String) = (Elvis,Presley)

scala> person._1
res15: String = Elvis

scala> person._2
res16: String = Presley

Scala中还用元组进行多值赋值:
scala> val (x, y) = (1, 2)
x: Int = 1
y: Int = 2

scala> x
res18: Int = 1

scala> y
res19: Int = 2

函数/方法

Scala的函数定义可以使用def关键字,参数列表需要声明参数类型,定义一个返回加一的方法

scala> def addOne(x:Int):Int = {
| return x + 1
| }
addOne: (x: Int)Int

成功定义了函数,因为返回值在最后一行,不需要声明return关键字,去掉后再次定义:
scala> def addOne(x:Int):Int = {
| x + 1
| }
addOne: (x: Int)Int

scala> addOne(1)
res4: Int = 2

Scala还可以用匿名函数的定义方式,将函数直接申明为一个匿名函数,使用=>表明映射关系,将函数定义成一个变量:
scala> val addTwo = (x:Int) => x + 2
addTwo: Int => Int = $$Lambda$4051/0x00000001013f0040@2d93a219

scala> addTwo(2)
res29: Int = 4

函数在Scala中和Integer,Float等基础类型一样,是Scala的“一等公民”,可以被作为参数传递,我们定义一个increase方法,传入一个函数和一个值:
scala> def increase(x:Int, f:Int=>Int)={
| f(x)
| }
increase: (x: Int, f: Int => Int)Int

scala> increase(5,addOne)
res5: Int = 6

scala> increase(5,addTwo)
res6: Int = 7

其中第一个参数表是一个整数,第二个参数是一个函数,函数作为参数时候需要声明其类型签名。Scala中所有的函数都有返回值,对于某些不写返回值的函数,scala会先进行静态类型推导,对于空返回,其返回类型是Unit:
scala> val nonReturnFunc = ()=>()
nonReturnFunc: () => Unit = $$Lambda$3914/0x000000010132ec40@9a60a4b

Scala中的类

Scala中可以用一行代码定义那些只有属性而没有方法或构造器的简单类:

scala> class Person(firstName: String, lastName: String)
defined class Person

scala> val gump = new Person("Forest","Gump")
gump: Person = Person@4810b744

我们来定义一个稍微复杂点的类,来看看Scala中的成员变量和方法:
class Light {
var status = "on"
println("you build a light!")

def turnOn() {
status = "on"
println("you turned on the light!")
}

def turnOff() {
status = "off"
println("you turned off the light!")
}

def showStatus()= {
status
}
}

你可能发现了此处除了除了showStatus之外都没有等号,这是因为如果既没有声明返回类型,有没有使用等号,则返回的都是(),这样的方法扮演过程的角色,反回值意义不大。

我们定义了一个灯类,成员变量有灯的开关状态,其中前两句不在任何方法中,这两行实际上是构造方法,当实例化这个类时,默认执行外构造方法的语句:

scala> val light = new Light
you build a light!
light: Light = Light@13dae13

scala> val light2 = new Light()
you build a light!
light2: Light = Light@d74061f

如果构造参数为空,不需要括号,我们执行对象中的方法,可以看到如下效果:
scala> light.showStatus()
res19: String = on

scala> light.turnOff()
you turned off the light!

scala> light.showStatus()
res21: String = off

但是面向对象的构造方法往往不止一个,当遇到多个构造方法的需求时,则需要引入辅助构造方法,如下一个例子:
class Person(val firstName:String) {
println("you used outer constructor!")

def this(firstName:String,lastName:String) {
this(firstName)
println("you used inner constructor 1!")
}

def this(firstName:String, lastName:String, age:Int) {
this(firstName, lastName)
println("you used inner constructor 2!")
}
}

我们定义了一个Person类,有三个构造方法,一个是外层主构造器,参数列表直接写在类的后面,构造体直接写在类里。两个辅助构造器通过重载this方法实现多构造
实例化输出如下:
scala> val p1 = new Person("Forest")
you used outer constructor!
p1: Person = Person@7b78909a

scala> val p2 = new Person("Forest","Gump")
you used outer constructor!
you used inner constructor 1!
p2: Person = Person@5ba69762

scala> val p3 = new Person("Forest","Gump",30)
you used outer constructor!
you used inner constructor 1!
you used inner constructor 2!
p3: Person = Person@41f96dd8

我们注意到主构造器的firstName参数使用了val修饰符,这说明firstName将直接成为该实例的成员变量!
scala> p3.firstName
res4: String = Forest

scala> p3.age
<console>:13: error: value age is not a member of Person
p3.age
^

varval都可以在主构造器中快速初始化成员变量,因为类的成员变量必须是确定的,所以只有在主构造器中参数列表允许使用这两个关键字。这种写法等价于Java中的this.firstName = firstName

对于访问修饰符:Scala中,如果没有指定任何的修饰符,则默认为 public。这样的成员在任何地方都可以被访问。,private和protect的使用和Jave类似。

扩展类object

在Java中,有一种方发成为静态方法或者类方法,使用static关键字修饰。Scala没有采用这种类方法定义的策略,Scala中在class里定义的全是实例方法。当有的类只能拥有一个实例时,Scala可以将其定义为Object:

object PrinterService {
def print() {
println("I am printing...")
}
}

执行后效果如下:
scala> PrinterService.print()
I am printing...

Object类似Java设计模式中的单例(Singleton),且object和class可以有相同的名字,如果一个class需要定义类方法,可以吧类方法定义在同名的object中,这种策略叫做伙伴对象(companion objects)

继承

作为面向对象的三大特性之一:继承,在scala中的表现形式略不同于Java,我们先举个例子:一个Person类,Employee类是他的派生

class Person(val name:String) {
def talk(message:String) = println(name + " says '" + message + "'")
}

class Employee(override val name:String, val number:Int) extends Person(name) {
override def talk(message:String) = println(name + " with number " + number + " says '" + message + "'")
}

Employee扩展了Person类,增加了number字段,并重载了talk方法,一切对于父类的派生扩展,都需要用到override关键字,Scala这个规定是为了防止你无意中引入错误的方法。
实例化,输出:
scala> val person = new Person("John")
person: Person = Person@2f4ee7d8

scala> person.talk("I'm fine")
John says 'I'm fine'

scala> val employee = new Employee("aickson",114514)
employee: Employee = Employee@616a108

scala> employee.talk("thank you sir")
aickson with number 114514 says 'thank you sir'

Trait

有时一个类有多个角色,在Java中使用Interface,C++使用多继承,Ruby使用mixins,Scala则使用了trait,你可以将它理解为Java的接口哇哦加一个接口的实现,或是看作是一个部分类(partial-class)。

class Person(val name:String)

trait Nice {
def greet() = println("Howdily doodily.")
}

class Character(override val name:String) extends Person(name) with Nice

上述代码,我们先定义了一个Person简单类,有定义了一个含有greet方法的trait Nice,最后定义一个新的类Character继承Person并带上Nice,此时Character就拥有了Nice中的greet行为:
scala> val p = new Person("Bill")
p: Person = Person@28ed9a7d

scala> p.greet()
<console>:13: error: value greet is not a member of Person
p.greet()
^

scala> val c = new Character("Bill")
c: Character = Character@18cc52f5

scala> c.greet()
Howdily doodily.

集合

Scala的集合是这门语言学习的重点,因为Scala的一个重要特性是函数式编程,函数式编程由于其方便灵活的集合操作而闻名。Scala的集合主要包含表(List)集(Set)映射(map)

列表

和大多函数式编程语言一样,最常用的数据结构是列表。List是事物的有序集合,可随机访问

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

scala> val l = List("one","two","tree")
l: List[String] = List(one, two, tree)

scala> l(2)
res0: String = tree

Scala的泛型和静态类型推导使得我们可以如同脚本语言一样灵活的初始化一个列表,列表的随机访问需要用圆括号。List中的数据必须有相同的类型,如果类型不同,则List的成员类型是Any,这是Scala数据结构的公共父类,一个通用的数据类型。

更多List方法:

scala> l.length //长度
res18: Int = 3

scala> l.isEmpty //判空
res19: Boolean = false

scala> l.head //头
res20: String = one

scala> l.tail //尾
res21: List[String] = List(two, tree)

scala> Nil.isEmpty //Nil是一个空列表
res22: Boolean = true

scala> l.last //最后一个值
res23: String = tree

scala> list.reverse //倒置
res25: List[String] = List(Orange, Banana, Apple)

scala> list.drop(1) //删去指定下标,返回新List
res26: List[String] = List(Banana, Orange)

scala> List("hello","123")
res2: List[String] = List(hello, 123)

scala> List("hello",123)
res3: List[Any] = List(hello, 123)

我们来了解一下Scala中的类层次关系:

所有的Scala类型都继承自Any类,就像所有的Java对象类都继承自Object。Nothing类是所有类型的子类。譬如,对一个返回集合的函数来说,函数也可以返回 Nothing,这与给定函数的返回值类型相符。当你处理nil的概念时,这里有些细微的差别。Null是一个trait,null则是Null的一个实 例,与Java中的null类似,意思是一个空值。一个空集合是Nil,而Nothing是一个trait,是所 有类的子类。Nothing类没有实例,所以不能像Null那样对其解引用(dereference)。例如,抛出 异常的方法的返回值类型为Nothing,意思是根本没有返回值。

Scala的Set使用方法和List类似,Set是一个无序集合,增删元素直接使用+-符号。

scala> val animals = Set("lions","tigers","bears")
animals: scala.collection.immutable.Set[String] = Set(lions, tigers, bears)

scala> animals - "trigers"
res5: scala.collection.immutable.Set[String] = Set(lions, tigers, bears)

scala> animals + "pandas"
res6: scala.collection.immutable.Set[String] = Set(lions, tigers, bears, pandas)

Scala中Set使用++--来进行集合的并和差操作。

scala> animals ++ Set("armadilos", "raccons")
res7: scala.collection.immutable.Set[String] = Set(bears, tigers, lions, armadilos, raccons)

scala> animals -- Set("bears", "lions")
res8: scala.collection.immutable.Set[String] = Set(tigers)

注意:集的操作是无破坏新的,即增删改操作不会破坏原有的数据结构,而是创建一个副本,然后修改并返回。这体现了函数式编程的”无副作用“的思想。

scala> Set(1,3,2) == Set(1,2,3)
res10: Boolean = true

scala> List(1,2,3) == List(3,2,1)
res11: Boolean = false

集无序,列表有序。

数组

Scala中的数组Array类似List,有着相同的构造和使用方式。

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

scala> arr(1)
res49: Int = 2

Scala中的List是不可变的递归数据(immutable recursive data),是Scala中的一种基础结构,你应该多用List而不是Array

映射

映射(Map)是一个键值对(Key-value),和Java中的HashMap类似。

scala> val m = Map(1 -> "One", 2 -> "Two", 3 -> "Three")
m: scala.collection.immutable.Map[Int,String] = Map(1 -> One, 2 -> Two, 3 -> Three)

scala> m(3)
res12: String = Three

需要注意的是:Scala的函数式编程思想使得他默认数据类型都是不可变的,如果需要修改集合中的值,则需要使用特定的可变数据结构,这一点和默认数据结构可变的Java有较大差异。还是以Map为例,如果要可变:
scala> import scala.collection.mutable.HashMap
import scala.collection.mutable.HashMap

scala> val newMap = new HashMap[Int,String]
newMap: scala.collection.mutable.HashMap[Int,String] = Map()

scala> newMap += 4 -> "four"
res14: newMap.type = Map(4 -> four)

scala> newMap += 5 -> "five"
res15: newMap.type = Map(5 -> five, 4 -> four)

高阶函数

通俗地说,高阶函数就是一个生成或使用函数的函数。更具体点说,高阶函数是一个以其他函数作为输入参数或以函数作为返回结果的函数。这种使用其他函数来构造函数的方法是函数式编程语言家族中的关键概念,并且它还会影响使用其他语言编写代码的方式。

  1. foreach
    foreach方法接受一个代码块作为参数。在Scala中,你可以用variableName => yourCode这样 形式来表示代码块:

    scala> val list = List("Apple","Banana","Orange")
    list: List[String] = List(Apple, Banana, Orange)

    scala> list.foreach(fruit => println("I like " + fruit))
    I like Apple
    I like Banana
    I like Orange

    其中fruit => println("I like " + fruit)就是一个匿名函数,foreach接受一个匿名函数,并遍历集合内的所有值,对其执行该函数。

  2. count/filter/forall/exists

    scala> List(1,2,3,4,5,6,7,8).count(x => x > 3)
    res27: Int = 5

    scala> List(1,2,3,4,5,6,7,8).filter(x => x > 3)
    res28: List[Int] = List(4, 5, 6, 7, 8)

    scala> List(1,2,3,4,5,6,7,8).forall(x => x < 9)
    res30: Boolean = true

    scala> List(1,2,3,4,5,6,7,8).exists(x => x > 9)
    res32: Boolean = false

    count用于计算满足条件的元素的个数,返回整数,filter用于过滤出满足条件的元素,如果代码块对于集合中的所有元素都返回true的话,那么forall返回true,如果代码块仅对集合中的某一个元素返回true,那么exists方法返回true

  3. map

    scala> List(1,2,3).map(x => x + 1)
    res36: List[Int] = List(2, 3, 4)

    mapforeach类似,将匿名函数应用于每个元素上,但map返回模式下映射的新集合

  4. sort

    scala> List(2,7,9,4,7,9,2,4,6,1,9).sorted
    res2: List[Int] = List(1, 2, 2, 4, 4, 6, 7, 7, 9, 9, 9)

    scala> List(4,5,7,1,9,6,8,3,5,9,2,0).sortWith((x, y) => x > y)
    res34: List[Int] = List(9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 1, 0)

    scala> List(4,5,7,1,9,6,8,3,5,9,2,0).sortWith((x, y) => x > y)
    res35: List[Int] = List(9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 1, 0)

    sortWith可以自定义一个排序规则并返回排序结果

Scala的高阶算子还有很多,具体参考文档。以上算子可以轻易的实现单词计数,例如统计这句话中的单词的个数:

can you can a can like a canner can a can

scala的实现仅仅需要一行
scala> "can you can a can like a canner can a can".split(" ").map(x=>(x,1)).groupBy(x=>x._1).map(x=>(x._1,x._2.length))
res48: scala.collection.immutable.Map[String,Int] = Map(a -> 3, you -> 1, can -> 5, canner -> 1, like -> 1)

如果活用其语法糖技巧,还可以更加简短,这就是函数式编程的魅力!
scala> "can you can a can like a canner can a can".split(" ").map((_,1)).groupBy(_._1).map(x=>(x._1,x._2.length))
res48: scala.collection.immutable.Map[String,Int] = Map(a -> 3, you -> 1, can -> 5, canner -> 1, like -> 1)

  1. foldLeft
    cala中的foldLeft方法与Ruby中的inject方法非常相似。你只需提供一个初始值以及一个 代码块,foldLeft就会将数组中的每个元素和另外的一个值传递给代码块。
    scala> val list = List(1,2,3)
    list: List[Int] = List(1, 2, 3)

    scala> val sum = (0 /: list) {(sum, i) => sum + i}
    sum: Int = 6

XML

在解决现代编程问题的过程中我们越来越多地用到了XML(Extensible Markup Language,可 扩展标记语言)。Scala将XML抬高到语言的一等编程结构,你可以像表示字符串那样来表示XML。

scala> val movies =
| <movies>
| <movie genre="action">Pirates of the Caribbean</movie>
| <movie genre="fairytale">Edward Scissorhands</movie>
| </movies>
movies: scala.xml.Elem =
<movies>
<movie genre="action">Pirates of the Caribbean</movie>
<movie genre="fairytale">Edward Scissorhands</movie>
</movies>

scala> movies.text
res0: String =
"
Pirates of the Caribbean
Edward Scissorhands
"

scala> val movieNodes = movies \ "movie"
movieNodes: scala.xml.NodeSeq = NodeSeq(<movie genre="action">Pirates of the Caribbean</movie>, <movie genre="fairytale">Edward Scissorhands</movie>)

scala> movieNodes(0)
res3: scala.xml.Node = <movie genre="action">Pirates of the Caribbean</movie>

模式匹配

模式匹配(pattern matching)允许你基于一些数据片断有条件地执行代码。Scala经常使用模 式匹配,诸如当你解析XML或在线程间传递消息时。

scala> def matchNumber(num:Int):String = num match {
| case 1 => "one"
| case 2 => "two"
| case _ => "other"
| }
matchNumber: (num: Int)String

scala> matchNumber(1)
res5: String = one

scala> matchNumber(0)
res6: String = other

Disqus评论区没有正常加载,请使用科学上网