scala入门笔记

819 阅读15分钟

scala概述

scala特性

  1. 函数式
  2. 静态类型
    见下文对象类型
  3. 运行于JVM上
  4. 可以执行Java代码
  5. 更加支持并发和同步
    Scala通过强调对象的不变性,以及使用基于事件的模型进行线程间通信使得实现并发应用变得简单
  6. 对象类型
    class对象如java,可以new对象;
    object类型在编译之后构造方法为private(单例模式),无法在外部生成实例对象,方法编译为static的静态方法,成员变量也是静态变量,可以通过类名直接调用/访问. 在对Object进行编译的时候会同时生成一个XXXX$.class的文件,该类是一个单例类,内部会构造一个 public static XXXX$ MODULE$; 单例对象
  7. 类型推断 可以手动指定其类型,若不指定,scala会自动进行类型推断
    val name1: String = "str1"
    val name2 = "str2"
    

scala基础

运算符标识符

Scala编译器将在内部将操作符标识符转换成具有嵌入式$字符的合法Java标识符。
如标识符:->将被内部表示为$colon$minus$greater

数据类型

Scala类关系

数据类型 含义 使用场景
Unit 对应于无值 java void
Null null或空引用
Nothing 所有类的子类 方法不返回/不正常返回,如抛异常
Any 所有类的父类
AnyRef 任何引用类型的超类型
AnyVal AnyVal 所有值类型的基类
  1. Nothing
    没有任何实例,其类似于java中的标示性接口,只是用来标识一个空类型,用来标识no instances类型,该类型是其它所有类型的子类型

    /**
     *  虚拟类,只存在于JVM中,对应scala.Nothing
     * 如果该Nothing出现在方法签名中则将会被抹掉,然后替换为Nothing$
    */
    sealed abstract class Nothing$ extends Throwable
    

    Nothing$的作用,也即是代码中如果出现Nothing类型的时候,在load到JVM运行的时候将会把Nothing替换为Nothing$类型。也即在JVM之外以Nothing的身份进行显示,在JVM中以Nothing$的身份进行显示,两者表示同一个含义。

    Nothing$ extends ThrowableNothing可以当做throw new XXXXXException()方法的接收类型

    def head: Nothing =
    throw new NoSuchElementException("head of empty list")
    
    def get(index:Int):Int = {  
        //Nothing也是Int的子类
        if(x < 0) throw new Exception(...)  
        else ....  
    }  
    
  2. Null
    Null和Nothing几乎一致,Null在运行期间被替换了Null$

    sealed abstract class Null$ private ()
    

    不同点如下
    Null有唯一的实例null, 而Nothing没有任何实例
    Null是所有引用类型(AnyRef)的子类型,而Nothing是所有类型的子类型
    Nothing是Null 的子类型

  3. Nil
    表示一个的list,可以同时表示List[T]的空集合。也即是Nil可以同时表示List[Int]类型的空集合和List[String]类型的空集合。

    case object Nil extends List[Nothing] {....}
    //unit usage
    def execute(code: => Unit):Unit = {
      // do something before
      code
      // do something after
    }
    
  4. Unit

    final abstract class Unit private extends AnyVal
    

    Unit是所有AnyVal 的子类,只有一个唯一的Value(注意这里是Value依旧是实例/对象)。如果方法的返回值类型为Unit,则类似于java中void
    scala只是使用Unit对java中的void进行了包装,用Unit来表示void的类型

  5. AnyVal
    基础数据类型的加强类型给数据类型提供了更多功能函数

    String - StringOps
    Int - RichInt
    Double - RichDouble
    Char - RichChar
    

    1.to(10) -> Int 隐式转换成RichInt -> 调用to函数

    scala中没有提供++、--操作符,我们只能使用+和-

特殊类

  • Option[X]
    scala推荐在可能返回空的方法使用Option[X]作为返回类型。如果有值就返回Some[x](Some也是Option的子类),否则返回None
    //可以看出None和some都是Option的子类
    def apply[A](x: A): Option[A] = if (x == null) None else Some(x)
    

特殊关键字

  1. sealed
    类名定义前有sealed 关键字,则表明该类的所有子类必须在同一个文件中定义

  2. case
    让编译器帮忙自动生成常用方法,Case Class的一个典型应用场景就是DTO。 Scala自动为case class定义了伴生对象,也就是object,并且定义了apply()方法,该方法接收主构造函数中相同的参数,并返回case class对象
    默认情况下,case类中列出的所有参数默认使用public和immutable修辞符

    名称 location 含义
    equals&hashCode&toStrng class
    copy class 基于当前实例的所有字段值复制一个新的实例
    apply object 在一个类的伴生对象中定义apply方法,在生成这个类的对象时,就省去了new关键字
    unapply object unapply方法是apply方法的反向操作,apply方法接受构造参数变成对象,而unapply方法接受一个对象,从中提取值,一般用于模式匹配

    eg:

    //伴生类
    class Person2(val name: String)
    //伴生对象
    object Person2 {
      def apply(name: String) = new Person2(name)
    }
    //apply方法可以省去new关键字
    var pers = Person2("vvv")
    println(pers.name)
    

    伴生类和伴生对象,最大的特点就在于,互相可以访问private field

      def main(args: Array[String]): Unit = {
        //有apply方法,不使用new
        val currency = Currency(30.2, "EUR")
        //用的是unapply方法提取
        currency match {
          case Currency(amount, "USD") => println("$" + amount)
          case _ => println("No match.")
        }
      }
    
    // 编译器会自动将构造函数转换为不可变字段(val),如果想要可变字段,需使用var关键字
    case class Person(name: String, age: Int)
    
  3. trait
    类似interface,但trait支持部分实现,可以通过多个with进行多重继承

    trait Flyable {
    //部分实现
      def hasFeather = true
      def fly
    }
    
  4. implicit
    隐式转换可以允许手动指定将某种类型的对象转换成其他类型的对象

    1. Scala默认会使用两种隐式转换,一种是源类型,或者目标类型的伴生对象内的隐式转换函数;一种是当前程序作用域内的可以用唯一标识符表示的隐式转换函数
    2. 隐式转换函数的同一个作用域中不能存在参数和返回值完全相同的2个implicit函数。
    3. 隐式转换函数只在意输入类型,返回类型,与函数名无关,由Scala进行调用
    object ImplicitDemo {
      def display(input:String):Unit = println(input)
      implicit def typeConvertor(input:Int):String = input.toString
      implicit def typeConvertor(input:Boolean):String = if(input) "true" else "false"
      //只在意 输入类型,返回类型,会报错
    //  implicit def booleanTypeConvertor(input:Boolean):String = if(input) "true" else "false"
      def main(args: Array[String]): Unit = {
        display("1212")
        //隐式转换
        display(12)
        display(true)
        }
    }
    

    隐式类

    import scala.io.Source
    import java.io.File
    object Implicit_Class {
      import Context_Helper._
      def main(args: Array[String]) {
        //在当前作用域中寻找 将Int(1)作为变量的类同时具有add 的方法的类,如有,则执行
        println(1.add(2))
        //在当前作用域中寻找 将File 作为变量的类同时具有read的方法的类,如有,则执行
        println(new File("I:\\aa.txt").read()) 
      }
    }
    
    object Context_Helper{
      implicit class ImpInt(tmp:Int){
        def add(tmp2: Int) = tmp + tmp2
      }
      implicit class FileEnhance(file:File){
        def read() = Source.fromFile(file.getPath).mkString
      }
    }
    
    class AbstractClass{    
        var id: Int = _   //注意 这种方式必须声明为var 
        //不可以定义为val 原因是因为_的值不明确  
    }  
    

    隐式转换的发生时机

    1. 函数参数类型不匹配
    2. 对象方法参数类型不匹配
    3. 方法不存在类型对象中
    • var
      可变变量
    • val
      不变变量,常量

    建议使用val,在类似spark的大型复杂系统中,需要大量的网络传输数据,若使用var,可能会有var变量修改

    scala方法参数是不可变的,默认定义为val

保护作用域

private[x] 
protected[x]

这里的x指代某个所属的包、类或单例对象。
如果写成private[x],读作"这个成员除了对[…]中的类或[…]中的包中的类及它们的伴生对像可见外,对其它所有类都是private。

scala函数

如果定义方法时不带括号,则调用方法时也不能带括号

def getName = name
object.getName
object Demo {
   def main(args: Array[String]) {
      printStrings("Hello", "Scala", "Python")
      addInt(2) // 结果是2+7
   }
    //可变参数 , 实际上是Array [String]
   def printStrings( args:String* ) = {
      var i : Int = 0
      for( arg <- args ){
         println("Arg value[" + i + "] = " + arg )
         i = i + 1
      }
   }
   //默认函数
   def addInt( a:Int = 5, b:Int = 7 ) : Int = {
      var sum:Int = 0
      sum = a + b
      return sum
   }
}

部分应用函数/偏应用函数(Partial Applied Function)

是指缺少部分参数的函数

object PartialAppliedFunctionLearn {
  def main(args: Array[String]) {
    //第一种常见样式
    val result = sum _ //表明是一个偏应用函数,参数一个都没定
    val r = result(2)(3)(4)
    println(r)  //9
    val r1 = result(2)(_: Int)(4)   //偏应用函数
    println(r1)     //<function1>
    val r3 = r1(5)//完整
    println(r3) //11

    //第二种常见样式
    val s = sum(4)(8)(_:Int)//注意这里用_通配符
    val s1 = s(7)
    println(s1) //19
  }

  /**
    * 定义一个柯里化函数,要求传入三个Int类型参数
    */
  def sum(i: Int)(j: Int)(k: Int): Int ={
    i + j + k
  }
}

只要凑齐了参数就可以了

柯里化函数

有多个参数列表的函数就是柯里化函数,所谓的参数列表就是使用小括号括起来的函数参数列
curry化最大的意义在于把多个参数的function等价转化成多个单参数function的级联,这样所有的函数就都统一了,方便做lambda演算。
在scala里,curry化对类型推演也有帮助,scala的类型推演是局部的,在同一个参数列表中后面的参数不能借助前面的参数类型进行推演,curry化以后,放在两个参数列表里,后面一个参数列表里的参数可以借助前面一个参数列表里的参数类型进行推演

 //定义为柯里化函是没有问题的,b的类型可以由a参与得出  
  def method(a: A)(b: a.B) {  }  

递归函数

如果在函数体内递归调用函数自身,则必须手动给出函数的返回类型。

//手动给出函数的返回类型Int
def fab(n: Int): Int = {
  if(n <= 1) 1
  else fab(n - 1) + fab(n - 2)
}

变长参数

不能将已有序列直接当做变长参数入参,需要使用Scala特殊的语法将参数定义为序列

def sum(nums: Int*) = {
  var res = 0
  for (num <- nums) res += num
  res
}
sum(1, 2, 3, 4, 5)
//val s = sum(1 to 5) wrong
val s = sum(1 to 5: _*)

将函数赋值给变量

def sayHello(name: String) { println("Hello, " + name) }
val sayHelloFunc = sayHello _   // 注意要加' _'(有空格)
sayHelloFunc("xxx")

匿名函数

// (参数名: 参数类型) => 函数体
val func = (name: String) => println("Hello, " + name)

闭包

函数在变量不处于其有效作用域时,还能够对变量进行访问,即为闭包

def getGreetingFunc(msg: String) = (name: String) => println(msg + ", " + name)

// 两次调用getGreetingFunc函数,传入不同的msg,并创建不同的函数返回
val greetingFuncHello = getGreetingFunc("hello")
greetingFuncHello("greetingFuncHello") //hello, greetingFuncHello

val greetingFuncHi = getGreetingFunc("hi")
greetingFuncHi("greetingFuncHi")  //hi, greetingFuncHi

msg只是一个局部变量,却在getGreetingFunc执行完之后,还可以继续存在创建的函数之中;greetingFuncHello("hello"),调用时,值为"hello"的msg被保留在了函数体内部,可以反复的使用
这种变量超出了其作用域,还可以使用的情况,即为闭包

Scala通过为每个函数创建对象来实现闭包,实际上对于getGreetingFunc函数创建的函数,msg是作为函数对象的变量存在的,因此每个函数才可以拥有不同的msg

集合

集合类图
默认的list set map都是不可变对象,需要使用可变的集合对象时,必须明确导入scala.collection.mutable.中的类

//map
val colors = Map("red" ->12, "azure" -> 11)
//colors += ("dd"->44) 不可变

// 创建一个可变的Map
val ages = scala.collection.mutable.Map("Leo" -> 30, "Jen" -> 25, "Jack" -> 23)
ages("Leo") = 31
// 移除元素
ages -= "Mike"

ArrayBuffer 即java中的ArrayList
使用+=操作符,可以添加一个元素,或者多个元素
使用++=操作符,可以添加其他集合中的所有元素

:: , +:, :+, :::, +++的区别

::: 向队列的头部追加数据,创造新的列表,x::listx加入到list作为头部
:+和+::list:+x尾部追加元素,list+:x头部追加元素

:当做集合,+在哪边就追加到哪边 ++:连接两个集合
::::连接两个list类型的集合

val alist = List(1,2)
val blist = List(3,4)
val cmap = Map(1->"a",2->"b")

println(cmap.+(3->"c"))     //Map(1 -> a, 2 -> b, 3 -> c)
println(cmap.-(1))          //Map(2 -> b)

println(alist.::(3))        //List(3, 1, 2)
println(alist.:::(blist))   //List(3, 4, 1, 2)

println(alist.++(blist))    //List(1, 2, 3, 4)
println(alist.+:(3))        //List(3, 1, 2)

println(alist ++ blist)     //List(1, 2, 3, 4)
println(alist +: blist)     //List(List(1, 2), 3, 4)
println(alist :+ blist)     //List(1, 2, List(3, 4))
操作 含义 适用对象
col :+ ele 将元素添加到集合尾部 Seq
ele +: col 将元素添加到集合头部 Seq
col + ele 在集合尾部添加元素 Set、Map
col + (ele1, ele2) 将其他集合添加到集合的尾部 Set、Map
col - ele 将元素从集合中删除 Set、Map、ArrayBuffer
col - (ele1, ele2) 将子集合从集合中删除 Set、Map、ArrayBuffer
col1 ++ col2 将其他集合添加到集合尾部 Iterable
col2 ++: col1 将其他集合添加到集合头部 Iterable
ele :: list 将元素添加到list的头部 List
list2 ::: list1 将其他list添加到list的头部 List
list1 ::: list2 将其他list添加到list的尾部 List
set1 | set2 取两个set的并集 Set
set1 & set2 取两个set的交集 Set
set1 &~ set2 取两个set的diff Set
col += ele 给集合添加一个元素 可变集合
col += (ele1, ele2) 给集合添加一个集合 可变集合
col ++= col2 给集合添加一个集合 可变集合
col -= ele 从集合中删除一个元素 可变集合
col -= (ele1, ele2) 从集合中删除一个子集合 可变集合
col -= col2 从集合中删除一个子集合 可变集合
ele +=: col 向集合头部添加一个元素 ArrayBuffer
col2 ++=: col 向集合头部添加一个集合 ArrayBuffer

元组

Scala元组将固定数量的项目组合在一起,以便它们可以作为一个整体传递,元组可以容纳不同类型的对象,但它们也是不可变
Scala中只能有22个上限Tuple22

val t = (1, "hello", Console)
//等价于
val t = new Tuple3(1, "hello", Console)

语言特性

if表达式

在Scala中,if表达式是有值的,就是if或者else中最后一行语句返回的值,scala会根据返回值类型进行类型推断

val aa = if(age > 18) "adult" else 0

此时if和else的值分别是String和Int,则表达式的值是Any,Any是String和Int的公共父类型
如果if后面没有跟else,则默认else的值是Unit,也用()表示,类似于java中的void或者null

val age = 12; 
if(age > 18) "adult"

此时就相当于if(age > 18) "adult" else ()

循环

Scala里面没有break和continue关键字,Scala里面推荐使用函数式的风格解决break和contine的功能,而不是一个关键字

import util.control.Breaks._
breakable(
    for(i <- 0 until 10) {
      println(i)
      if(i==5){
        break()
      }
    }
  )
 // 0,1,2,3,4,5
 
 for(i<-0 until 10){
      breakable{
          if(i==3||i==6) {
            break   // 类似continue
          }
          println(i)
      }
    }
    //0,1,2,4,5,7,8,9

lazy

将一个变量声明为lazy,则只有在第一次使用该变量时,变量对应的表达式才会发生计算

lazy val lines =  fromFile("test.txt").mkString

即使文件不存在,也不会报错,只有第一次使用变量时会报错

对象

scala中,类默认是public的

class Student {
  var varValue = "varValue"  //scala生成的面向JVM的类时,会定义为private字段,生成public的getter和setter方法
  private var privateVarValue = "privateVarValue"  //生成private的getter和setter方法
  
  val valValue = "valValue"  //只会生成getter方法
  private val privateValValue = "privateValValue"  //生成getter方法
  
  //对象私有的字段(不是类私有字段),只有这个实例的方法才能访问privateThisVarValue,其他Student实例不能访问,限定到了实例级别
  //应用场景太少,默认还是使用private就可以了
  private[this] val privateThisVarValue = "privateThisVarValue"     // 不生成getter和setter方法
}
// Scala的getter和setter方法的命名与java是不同的,是field和field_=的方式
// 如果要让scala自动生成java风格的getter和setter方法,只要给field添加@BeanProperty注解即可
// 此时会生成4个方法,name: String、name_=(newValue: String): Unit、getName(): String、setName(newValue: String): Unit
import scala.reflect.BeanProperty
class Student {
  @BeanProperty var name: String = _
}
//class Student(@BeanProperty var name: String)

val s = new Student
s.setName("leo")
s.getName()

构造函数

非方法内部的代码都认为是构造方法中的一部分,都会执行

//由val修饰name
class Student3(val name: String) {
  println("your name is " + name )
}
//若没有val/var修饰name.当做没有这个field,s.name 访问不了
s.name  //可以这样访问

object单例对象构造方法不接受参数

Scala中的apply函数是非常特殊的一种函数,在Scala的object中,可以声明apply函数。当使用类名()的形式时,其实就是类名.apply()的一种缩写。通常使用这种方式来构造类的对象,而不是使用new 类名()的方式。 Array(1, 2, 3, 4)实际上是用Array object的apply()函数来创建Array类的实例

继承

Scala中,如果子类要覆盖一个父类中的非抽象方法,则必须使用override关键字

// 注意!如果是父类中接收的参数,比如name和age,子类中接收时,就不要用任何val或var来修饰了,否则会认为是子类要覆盖父类的field
class Person(val name: String, val age: Int)
//name和age不用修饰了
class Student(name: String, age: Int, var score: Double) extends Person(name, age) {
  def this(name: String) {
    this(name, 0, 0)
  }
}

抽象field

// 如果在父类中,定义了field,但是没有给出初始值,则此field为抽象field
abstract class Person {
  val name: String
}

默认情况下,如果父类中的构造函数代码,用到了会被子类重写的field; 那么会出现错误:

  1. 子类的构造函数(无参)调用父类的构造函数(无参)
  2. 父类的构造函数初始化field
  3. 父类的构造函数使用field执行其他构造代码,,但父类构造方法中用到的field还没被子类赋值,此时父类其他构造代码如果使用了该field(如Int),而且field要被子类重写,那么它的getter方法被重写,返回0而不是子类重写后的值
  4. 子类的构造函数再执行,重写field(晚了,父类已经使用过了)
  5. 但是此时子类从父类继承的其他构造代码,已经出现了错误了
object Student2 {
  def main(agrs: Array[String]) {
    val pes = new PEStudent
    val pes2 = new PEStudent2
    println("pes:" + pes.classScores.toBuffer.toString())   //pes:ArrayBuffer()
    //子类正确重写父类field
    println("pes2:" + pes2.classScores.toBuffer.toString()) //pes2:ArrayBuffer(0, 0, 0)
  }
}
class Student2 {
  val classNumber: Int = 10 //会被子类重写
  val classScores: Array[Int] = new Array[Int](classNumber)
}

class PEStudent extends Student2 {
  override val classNumber: Int = 3
}

解决方法:提前定义

class PEStudent2 extends {
  override val classNumber: Int = 3
} with Student2

trait

可以将Trait作为接口来使用,此时的Triat就与Java中的接口非常类似
类可以使用extends关键字继承trait,注意,这里不是implement,而是extends,在scala中没有implement的概念,无论继承类还是trait,统一都是extends
scala不支持对类进行多继承,但是支持多重继承trait,使用with关键字即可
trait也可以继承自class

trait HelloTrait {
  //抽象域
  val msg: String
  //加入成员变量
  val eyeNum: Int = 2
  def sayHello(name: String)
  //可以有具体方法实现,类似java中的抽象类,可以使用抽象域msg
  def log(message: String) = println(message+msg)
}
trait MakeFriendsTrait {
  def makeFriends(p: Person)
}
//类可以使用extends关键字继承trait
class Person(val name: String) extends HelloTrait with MakeFriendsTrait with Cloneable with Serializable {
  //必须覆写抽象域
  val msg: String = "hello"
  //不需要使用override关键字
  def sayHello(name: String) = println("Hello, " + name)
  def makeFriends(p: Person) = println("Hello, my name is " + name + ", your name is " + p.name)
}

extend trait获取field的方式与继承class是不同的:如果是继承class获取的field,实际是定义在父类中的;而继承trait获取的field,就直接被添加到了类中

动态绑定

trait Logged {
  //默认空实现
  def log(msg: String) {}
}
trait MyLogger extends Logged {
  override def log(msg: String) { println("MyLogger: " + msg) }
}
class Person(val name: String) extends Logged {
  def sayHello { 
    println("Hi, I'm Person:" + name);
    //被动态绑定的类覆写
    log("person") 
  }
}

//p2实例动态绑定MyLogger
val p2 = new Person("jack") with MyLogger
p2.sayHello     //Hi, I'm Person:jack   MyLogger: person
p2.log("p2")    // MyLogger: p2

在trait中覆盖抽象方法

trait Logger {
  def log(msg: String)
}
trait MyLogger extends Logger {
  //使用了super.方法的代码,则无法通过编译。因为super.方法就会去调用父trait的抽象方法
  //此时子trait的该方法还是会被认为是抽象的,要加上abstract override修饰
  abstract override def log(msg: String) { super.log(msg) }
}

协变和逆变

对于一个带类型参数的类型,比如 List[T],如果对A及其子类型B,满足 List[B]也符合 List[A]的子类型,那么就称为covariance(协变)(正相关关系)
如果 List[A]List[B]的子类型,即与原来的父子关系正相反,则称为contravariance(逆变)(逆相关关系)
如Professional是Master的子类,那么Card[Professionnal]也是Card[Master]的子类

内部类

内部类作用域

import scala.collection.mutable.ArrayBuffer

class StudentClass(val className:String) {studentClass=>    ///通过studentClass引用外部类
  class Student(val name: String)
  val students = new ArrayBuffer[Student]
  def register(name: String) = {
    ///通过studentClass引用外部类
    new Student(studentClass.className+name)
  }

  def test:Unit= {
    val c1 = new StudentClass("c1")
    val leo = c1.register("leo")
    c1.students += leo

    val c2 = new StudentClass("c2")
    val jack = c2.register("jack")
//    c1.students += jack  //jack是c2对象,不能注入到c1中
    c2.students += jack     //可以
  }
}

解决同一类型内部类中的类型不一致问题,可以理解为实例中的内部类为成员变量,只有实例可用
c1.students += jack可用

  1. 类型投影
    对类型加上外部类引用,当做类变量,而非实例变量
    //把val students = new ArrayBuffer[Student]替换
    val students = new ArrayBuffer[StudentClass#Student]
    
  2. 伴生对象
    把Student对象抽出来为伴生对象
    object Class {
      class Student(val name: String)
    }
    

异常

class ExceptionExample{  
    @throws(classOf[NumberFormatException])  
    def validate()={  
        "abc".toInt  
    }  
}  

object Demo{  
    def main(args:Array[String]){  
        var e = new ExceptionExample()  
        try{  
            e.validate()  
        }catch{  
            case ex : NumberFormatException => println("NumberFormatException handeled here")  
            case _ : println("Exception")
        }  
        println("Rest of the code executing...")  
    }  
}

参考文献

  1. 易百教程-Scala教程
  2. 北风网-scala教程
  3. scala集合类详解
  4. Scala和并发编程
  5. 让并发和容错更容易:Akka示例教程