Scala语言入门

发布时间 2023-06-01 20:52:03作者: strongmore

为什么要学习Scala语言

  • 最直接的一点就是因为我们后面要学的Spark框架需要用到Scala这门语言,但是Spark其实是同时支持Scala语言和Java语言的,为什么非要学Scala呢,使用java它难道不香吗?
  • 这就要说第二点了:scala相比java代码量更少,更适合函数式编程。

什么是Scala

Scala是一门多范式的编程语言,它是一种类似Java的编程语言,它设计的初衷是为了实现可伸缩的语言、并集成面向对象编程和函数式编程的各种特性

Scala基于Java虚拟机,也就是基于JVM的一门编程语言。所有Scala代码,都需要编译为字节码,然后交由Java虚拟机来运行

Scala和Java可以无缝相互操作,Scala可以任意调用Java代码,这个特性是非常好的

Scala环境安装配置

注意:由于Scala是基于Java虚拟机的,所以使用 Scala 之前必须先安装 Java,Java我们已经安装过了。
那在这里我们先到官网下载Scala安装包

下载地址,这里我们使用2.12.11版本

Scala命令行

scala

Scala命令行也称为Scala解释器(REPL),它会快速编译Scala代码为字节码,然后交给JVM来执行

这里的REPL表示:Read(取值)-> Evaluation(求值)-> Print(打印)-> Loop(循环)
在Scala命令行内,输入Scala代码,解释器会直接返回结果

如果你没有指定变量来存放计算的值,那么值默认的名称会显示为res开头的变量,而且会显示结果的数据类型

如果想在scala REPL中执行多行代码,该如何操作?
使用 :pastectrl+D 的方式
:paste 表示代码块的开始
ctrl+D 表示代码块的结束

IDEA开发配置

安装Scala插件,添加Scala的SDK

image

image

之后就可以插件Scala类了。

Scala的基本使用

scala很多语法和kotlin都很类似,如定义变量,不需要分号,函数等。

变量

Scala中的变量分为两种:可变 var 和 不可变 val
可变var:可以随时修改var声明的变量的值
不可变val:val声明的变量,值不能被修改,否则会报错: error: reassignment to val

注意:在实际工作中,针对一些不需要改变值的变量,通常建议使用val,这样可以不用担心值被错误的修改(等于java中的final类型)。这样可以提高系统的稳定性和健壮性!

无论声明val变量,还是声明var变量,都可以手动指定变量的类型,如果不指定,Scala会自动根据值,进行类型推断
val c = 1 等价于 val c: Int = 1

数据类型

Scala中的数据类型可以分为两种,基本数据类型和增强版数据类型

  • 基本数据类型有: Byte、Char、Short、Int、Long、Float、Double、Boolean
  • 增强版数据类型有: StringOps、RichInt、RichDouble、RichChar 等

scala使用这些增强版数据类给基本数据类型增加了上百种增强的功能

例如:RichInt提供的有一个to函数, 1.to(10) ,此处Int会先隐式转换为RichInt,然后再调用其to函数

val inclusive = 1 to 10
for (i <- inclusive) {
  print(i + " ")
}
println()
val inclusive2 = 1.to(10)
for (i <- inclusive2) {
  print(i + " ")
}

操作符

Scala的算术操作符与Java的算术操作符没有什么区别
比如 +、-、*、/、% 等,以及 &、|、^、>>、<< 等

注意:Scala中没有提供++、–操作符
我们只能使用+和-,比如count = 1,count++是错误的,必须写做count += 1

if 表达式

在Scala中,if表达式是有返回值的,就是if或者else中最后一行语句返回的值,这一点和java中的if是不一
样的,java中的if表达式是没有返回值的
例如: val age = 20; if (age > 18) 1 else 0

val score = 90
val level = if (score > 90) "A" else "B"
println(level)

由于if表达式是有值的,而if和else子句的值的类型可能还不一样,此时if表达式的值是什么类型呢?

注意:Scala会自动进行推断,取两个类型的公共父类型

如果if后面没有跟else,则默认else的值是Unit,也可以用()表示,类似于java中的void或者null

语句终结符

Scala默认不需要语句终结符,它将每一行作为一个语句
如果一行要放多条语句,则前面的语句必须使用语句终结符,语句终结符和Java中的一样,就是我们平时使用的分号

循环

for (i <- 5 until 10) {
  print(i + " ")
}
//迭代字符串
for (i <- "hello") {
  print(i + " ")
}
println()
var n = 10
while (n > 0) {
  print(n + " ")
  n -= 1
}

1 to 10 可以获取1~10之间的所有数字
1 until 10可以获取1~9之间的所有数字

高级for循环

//if守卫
for (i <- 1 to 10 if i % 2 == 0) {
  print(i + " ")
}
//for推导式
val ages = for (i <- 1 to 5) yield i * i
println(ages.toList)

Scala的集合体系

image

Scala中的集合是分成可变和不可变两类集合的

  • 其中可变集合就是说,集合的元素可以动态修改,在 scala.collection.mutable 这个包下面
  • 而不可变集合就是说,集合的元素在初始化之后,就无法修改了,在 scala.collection.immutable 这个包下面
//不可变集合
val set = Set(1, 1, 2, 3)
println(set)
//可变集合
val set2 = scala.collection.mutable.Set(1, 2, 3)
set2.add(4)
println(set2)
//不可变列表
val list = List(1, 2, 3)
println(list)
//可变列表
val listBuffer = scala.collection.mutable.ListBuffer(1, 2, 3)
listBuffer.append(4)
println(listBuffer.toList)
//不可变Map
val map = Map("name" -> "lisi", "age" -> 20)
println(map)
val map2 = scala.collection.mutable.Map("name" -> "lisi", "age" -> 20)
map2.put("address", "上海")
println(map2)
println(map2.get("name1")) # 如果存在,返回的是Option对象,类似java中的Optional,get()方法获取数据
//println(map2("name1")) 不存在抛异常
println(map2.getOrElse("name1", "lisi2"))
//不可变数组 求和 排序
val array = Array(2, 1, 3)
println(array.sum)
println(array.max)
println(array(0))
Sorting.quickSort(array)
println(array.toList)
//可变数组
val arrayBuffer = scala.collection.mutable.ArrayBuffer(2, 1, 3)
arrayBuffer.append(4)
println(arrayBuffer.toList)
//元组 数据不可变
val tuple = Tuple3(1, 2, "12")
println(tuple)

Scala中函数的使用

在Scala中定义函数需要使用 def 关键字,函数包括函数名、参数、函数体

Scala要求必须给出函数所有参数的类型,但是函数返回值的类型不是必须的,因为Scala可以自己根据函数体中的表达式推断出返回值类型。
函数中最后一行代码的返回值就是整个函数的返回值,不需要使用return,这一点与Java不同,java中函数的返回值是必须要使用return的

//单行函数
  def sum(a: Int, b: Int): Int = a + b

  //多行函数 包含默认参数
  def sum2(a: Int, b: Int = 0): Int = {
    //默认最后一行代码的返回值就是函数的返回值
    a + b
  }

  //可变参数
  def sum3(nums: Int*): Int = {
    var sum = 0
    for (num <- nums) {
      sum += num
    }
    sum
  }

  //特殊的函数-过程 没有使用= 返回值就是Unit
  def sum4(a: Int, b: Int) {
    println(a + b)
  }

  //特殊的函数-过程
  def sum5(a: Int, b: Int): Unit = {
    println(a + b)
  }
  
println(sum(1, 4))
//使用参数名来传参
println(sum2(b = 6, a = 5))
println(sum3(1, 2, 3, 4))

过程通常用于不需要返回值的函数

def sayHello(name: String) = "Hello, " + name
def sayHello(name: String): String = "Hello, " + name
def sayHello(name: String) { "Hello, " + name }
def sayHello(name: String): Unit = "Hello, " + name

前面两种写法的效果是一样的,都是函数
后面两种写法的效果是一样的,都是过程

//延迟读取文件
lazy val fileSource = Source.fromFile("C:/D-myfiles/testjar/test.txt")
val lines = fileSource.mkString
fileSource.close()
println(lines)

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

即使文件不存在,代码也不会报错,只有变量使用时才会报错,这就是lazy这个特性。

Scala面向对象编程

Scala中类和java中的类基本是类似的
Scala中的对象时需要定义的,而java中的对象是通过class new出来的
Scala中的接口是trait,java中的接口是interface

类-class

object Demo2 {
  def main(args: Array[String]): Unit = {
    val person = new Person
    person.printName()
    person.printName
  }
  class Person {
    var name = "lisi"
    def printName(): Unit = {
      println(name)
    }
  }
}

如果在定义方法的时候指定了(),那么在调用的时候()可写可不写,如果在定义方法的时候
没指定(),则调用方法时肯定不能带()

Scala类中的构造函数可以分为主构造函数和辅助构造函数
这两种构造函数有什么区别呢?
主constructor:类似Java的默认构造函数 this()
辅助constructor:类似Java的重载构造函数 this(name,age)

object Demo2 {
  def main(args: Array[String]): Unit = {
    val student = new Student("lisi")
    println(student)
    val student2 = new Student("lisi", 23)
    println(student2)
  }

  class Student(name: String) {
    var age = 0

    def this(name: String, age: Int) {
      this(name)
      this.age = age
    }

    override def toString: String = {
      "%s,%s".format(name, age)
    }
  }
}

Scala中,可以给类定义多个辅助constructor,类似于java中的构造函数重载
辅助constructor之间可以互相调用,但是第一行必须调用主constructor

对象-object

object:相当于class的单个实例,通常在里面放一些静态的field或者method

object不能定义带参数的constructor,只有空参的constructor
第一次调用object的方法时,会执行object的constructor,也就是执行object内部不在任何方法中的代码,因为它只有空参的构造函数

但是注意,object的constructor的代码只会在他第一次被调用时执行一次,以后再次调用就不会再执行了

object通常用于作为单例模式的实现,或者放class的一些静态成员,比如工具方法

object可以直接使用,不能new

object Demo2 {
  def main(args: Array[String]): Unit = {
    Teacher.teach()
  }
  object Teacher {
    var name = "Tom"

    def teach(): Unit = {
      println(name + " teach")
    }
  }
}

伴生对象

如果有一个class,还有一个与class同名的object,那么就称这个object是class的 伴生对象 ,class是 object的 伴生类

import scala.collection.mutable.ListBuffer

object Demo2 {
  def main(args: Array[String]): Unit = {
    val list = new IntList(1, 2, 3)
    println(list)
    val list2 = IntList(1, 2, 3)
    println(list2)
  }

  class IntList() {
    var delegate: ListBuffer[Int] = ListBuffer()

    def this(nums: Int*) {
      this()
      for (elem <- nums) {
        this.delegate.append(elem)
      }
    }

    override def toString: String = delegate.toString
  }

  object IntList {
    def apply(nums: Int*): IntList = {
      //new IntList(nums) 这种方式不行 原因未知
      val list = new IntList()
      for (elem <- nums) {
        list.delegate.append(elem)
      }
      list
    }
  }

}

apply是object中非常重要的一个特殊方法,通常在伴生对象中实现apply方法,并在其中实现构造伴生类对象的功能
在创建对象的时候,就不需要使用new Class的方式,而是使用Class()的方式,隐式调用伴生对象的apply方法,这样会让对象创建更加简洁

例如:Array的伴生对象的apply方法就实现了接收可变数量的参数,以及会创建一个Array对象
val a = Array(1, 2, 3, 4, 5)

接口-trait

Scala中的接口称为trait,trait类似于Java中的interface
在triat中可以定义抽象方法

类可以使用 extends 关键字继承trait,无论继承类还是trait统一都是使用 extends 这个关键字

类继承trait后,必须实现trait中的抽象方法,实现时不需要使用 override 关键字

scala不支持对类进行多继承,但是支持对trait进行多重继承,使用 with 关键字即可

class MyRunner extends Runnable with MyRunnable {
    override def run(): Unit = {
      println("run ...")
    }
    override def run2(): Unit = {
      println("run2 ...")
    }
  }
  trait Runnable {
    def run(): Unit
  }
  trait MyRunnable {
    def run2(): Unit
  }

main方法

Scala中的main方法必须定义在object中,格式为 def main(args: Array[String])

函数式编程

在Scala中,函数与类、对象一样,都是一等公民,所以说scala的面向过程其实就重在针对函数的编程了,所以称之为函数式编程

object Demo3 {
  def main(args: Array[String]): Unit = {
    //函数赋值给变量
    var mySum = sum _
    println(mySum(2, 4))
    //匿名函数
    var mySum2 = (a: Int, b: Int) => a + b
    println(mySum2(3, 6))
    //函数当做参数
    println(sum2(a => a * a, 1, 2, 3))
    //内置高阶函数 类似java中的StreamAPI
    val listSum = List(1, 2, 3)
      .filter(a => a % 2 != 0) //可以简写为 _ % 2 != 0   _就表示遍历中的元素
      .map(a => a * a)
      .sum
    println(listSum)
    val listJoin = List("hello you", "hello me")
      .flatMap(a => a.split(" ").toList)
      .reduceLeft((a: String, b: String) => a + "-" + b)
    println(listJoin)
  }
  def sum(a: Int, b: Int): Int = {
    a + b
  }
  def sum2(func: Int => Int, nums: Int*): Int = {
    var sum = 0
    for (elem <- nums) {
      sum += func(elem)
    }
    sum
  }
}

模式匹配

模式匹配是Scala中非常有特色,非常强大的一种功能。
模式匹配,其实类似于Java中的 switch case 语法,即对一个值进行条件判断,然后针对不同的条件,进行不同的处理

object Demo4 {
  def main(args: Array[String]): Unit = {
    //值匹配
    println(week(1))
    println(week(2))
    println(week(3))
    //类型匹配
    processException(new IllegalArgumentException("error"))
    processException(new IOException("error"))
    processException(new Exception("error"))
    //case class 匹配
    println(checkPerson(Teacher("Tom")))
    println(checkPerson(Student("Jack")))
    println(checkPerson(new Person()))
  }

  def checkPerson(p: Person): String = {
    p match {
      case Teacher(name) => "Teacher"
      case Student(name) => "Student"
      case _ => "None"
    }
  }

  class Person

  case class Teacher(name: String) extends Person

  case class Student(name: String) extends Person

  def processException(e: Exception): Unit = {
    e match {
      case e1: IllegalArgumentException => println("IllegalArgumentException: " + e)
      case e2: IOException => println("IOException: " + e)
      case e3: Exception => println("Exception: " + e)
    }
  }
  def week(day: Int): String = {
    day match {
      case 1 => "Monday"
      case 2 => "Tuesday"
      case _ => "None"
    }
  }
}

当一个类被声明为case class的时候,scala会帮助我们做下面几件事情:

  1. 构造器中的参数如果不被声明为var的话,它默认的话是val类型的,但一般不推荐将构造器中的参数声明为var;
  2. 自动创建伴生对象,同时在里面给我们实现apply方法,使得我们在使用的时候可以不直接显示地new对象;
  3. 伴生对象中同样会帮我们实现unapply方法,从而可以将case class应用于模式匹配;
  4. 实现自己的toString、hashCode、copy、equals方法。

除此之此,case class与其它普通的scala类没有区别。

隐式转换

Scala的隐式转换,允许手动指定将某种类型的对象转换成其它类型的对象

Scala的隐式转换,最核心的就是定义隐式转换函数,即implicit conversion function

隐式转换函数与普通函数唯一的语法区别是要以implicit开头而且最好要定义函数返回类型

隐式转换非常强大的一个功能,就是可以在不知不觉中加强现有类型的功能。也就是说,我们可以为某个普通类定义一个加强类,并定义对应的隐式转换函数,这样我们在使用加强类里面的方法的时候,Scala会自动进行隐式转换,把普通类转换为加强类,然后再调用加强类中的方法

object Demo5 {
  def main(args: Array[String]): Unit = {
    val dog = new Dog("Tom")
    dog.catchMouse()
  }

  class Cat(name: String) {
    def catchMouse() {
      println(this.name + " catch mouse")
    }
  }

  class Dog(val name: String)

  implicit def object2Cat(obj: Object): Cat = {
    if (obj.getClass == classOf[Dog]) {
      val dog = obj.asInstanceOf[Dog]
      new Cat(dog.name)
    }
    else Nil
  }
}

我们后续在工作中一般很少需要我们自己去定义隐式转换函数,大部分的场景是我们只需要使用import导入对应的隐式转换函数就可以了,在这个案例中我们是自己手工实现了一个隐私转换函数,因为他们都在一个作用域内,所以就不需要import了。