什么是Scala
Scala是一种将面向对象和函数式编程结合在一起的高级语言,旨在以简洁、优雅和类型安全的方式表达通用编程模式。Scala功能强大,不仅可以编写简单脚本,还可以构建大型系统。
Scala运行于Java平台,Scala程序会通过JVM被编译成class字节码文件,然后在操作系统上运行。其运行时候的性能通常与Java程序不分上下,并且Scala代码可以调用Java方法、继承Java类、实现Java接口等,几乎所有Scala代码都大量使用了Java类库。
由于Spark主要是由Scala语言编写的,为了后续更好的学习Spark以及使用Scala编写Spark应用程序,需要首先学习使用Scala语言。
安装Scala
由于Scala运行于Java平台,因此安装Scala之前需要确保系统安装了JDK。此处使用的Scala版本为2.12.7,要求JDK版本为1.8。
Windows安装Scala
1、下载Scala
到Scala官网https://www.scala-lang.org/download/下载Windows安装包scala-2.12.7.msi
2、配置环境变量
变量名:SCALA_HOME
变量值:C:\Program Files (x86)\scala
变量名:Path
变量值:%SCALA_HOME%\bin
3、测试
CMD中执行scala -version
命令
CentOS7安装Scala
1、下载Scala
到Scala官网https://www.scala-lang.org/download/下载Linux安装包scala-2.12.7.tgz
解压到指定目录:
1 |
$ tar -zxvf scala-2.12.7.tgz -C /opt/modules/ |
2、配置环境变量
1 2 |
export SCALA_HOME=/opt/modules/scala-2.12.7/ export PATH=$PATH:$SCALA_HOME/bin |
3、测试
CMD中执行scala -version
命令
Scala基础
最初学习Scala的时候建议在Scala命令行模式中操作,最终程序的编写可以在IDE中进行。在Windows CMD窗口中或CentOS的Shell命令中执行“scala”命令,即可进入Scala的命令行操作模式(REPL)。
变量声明
Scala中变量的声明使用关键字val和var。
声明一个val字符串变量str:
1 2 |
scala> val str="hello scala" str: String = hello scala |
声明变量时指定数据类型:
1 2 |
scala> val str:String="hello scala" str: String = hello scala |
将多个变量放在一起进行声明:
1 2 3 |
scala> val x,y="hello scala" x: String = hello scala y: String = hello scala |
Scala变量的声明,需要注意的地方总结如下:
- 定义变量需要初始化,否则会报错。
- 定义变量时可以不指定数据类型,系统会根据初始化值推断变量的类型。
- Scala中鼓励优先使用val(常量),除非确实需要对其进行修改。
- Scala语句不需要写结束符,除非同一行代码使用多条语句时才需要使用分号隔开。
数据类型
在Scala中,所有的值都有一个类型,包括数值和函数。
Any是Scala类层次结构的根,也被称为超类或顶级类。Scala执行环境中的每个类都直接或间接地从该类继承。该类中定义了一些通用的方法,例如equals()、hashCode()和toString()。Any有两个直接子类:AnyVal和AnyRef。
AnyVal表示值类型。有9种预定义的值类型,它们是非空的Double、Float、Long、Int、Short、Byte、Char、Unit和Boolean。Unit是一个不包含任何信息的值类型,和Java语言中的void等同,用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成()。
AnyRef表示引用类型。所有非值类型都被定义为引用类型。Scala中的每个用户定义类型都是AnyRef的子类型。AnyRef对应于Java中的Java.lang.Object。
Nothing是所有类型的子类,在Scala类层级的最低端。Nothing没有对象,因此没有具体值,但是可以用来定义一个空类型,类似于Java中的标示性接口(如:Serializable,用来标识该类可以进行序列化)。举个例子,如果一个方法抛出异常,则异常的返回值类型就是Nothing(虽然不会返回)。
Null是所有引用类型(AnyRef)的子类,所以Null可以赋值给所有的引用类型,但不能赋值给值类型,这个和Java的语义是相同的。Null有一个唯一的单例值null。
下面的例子定义了一个类型为List[Any]的变量list,list中包括字符串、整数、字符、布尔值和函数,由于这些元素都属于对象Any的实例,因此可以将它们添加到list中。
1 2 3 4 5 6 7 |
val list: List[Any] = List( "a string", 732, //an integer 'c', //a character true, //a boolean value () => "an anonymous function returning a string" ) |
Scala中的值类型可以进行转换,且转换是单向的。
例如下面的例子,允许将Long型转换为Float型,Char型转换为Int型:
1 2 3 4 5 |
val x: Long = 987654321 val y: Float = x //9.8765434E8 (注意在这种情况下会丢失一些精度) val face: Char = '☺' val number: Int = face //9786 |
表达式
Scala中常用的表达式主要有条件表达式和块表达式。
1、条件表达式
条件表达式主要是含有if/else的语句块:
1 2 3 4 |
scala> val i=1 i: Int = 1 scala> val result=if(i>0) 100 else -100 result: Int = 100 |
也可以在一个表达式中进行多次判断:
1 2 |
scala> val result=if(i>0) 100 else if(i==0) 50 else 10 result: Int = 100 |
2、块表达式
块表达式为包含在符号{}中的语句块:
1 2 3 4 5 6 |
scala> val result={ | val a=10 | val b=10 | a+b | } result: Int = 20 |
Scala中的返回值是最后一条语句的执行结果,而不需要像Java一样单独写return关键字。如果表达式中没有执行结果,则返回一个Unit对象,类似Java中的void:
1 2 3 4 |
scala> val result={ | val a=10 | } result: Unit = () |
循环
Scala中的循环主要有for循环、while循环和do while循环三种。
1、for循环
for循环的语法:
for(变量<-集合或数组){
方法体
}
例如,循环从1到5输出变量i的值:
1 |
scala> for(i<- 1 to 5) println(i) |
若不想包括5,可使用关键字until:
1 2 |
scala> for(i<- 1 until 5) println(i) |
将字符串“hello”中的字符循环输出:
1 2 |
scala> val str="hello" scala> for(i<-0 until str.length) println(str(i)) |
将字符串看做一个由多个字符组成的集合,简化写法:
1 |
scala> for(i<-str) println(i) |
2、while循环
while循环的语法:
while(条件)
{
循环体
}
例如:
1 2 3 4 5 6 7 |
scala> var i=1 i: Int = 1 scala> while(i<5){ | i=i+1 | println(i) | } |
3、do while循环
do while 循环与while循环类似,但是do while循环会确保至少执行一次循环。语法:
1 2 3 |
do { 循环体 } while(条件) |
例如:
1 2 3 4 |
scala> do{ | i=i+1 | println(i) | }while(i<5) |
方法与函数
Scala中有方法与函数。Scala 方法是类或对象中定义的成员,而函数是一个对象,可以将函数赋值给一个变量。换句话说,方法是函数的特殊形式。
1、方法
方法的定义使用def关键字,语法:
def 方法名 (参数列表):返回类型={
方法体
}
例如,将两个数字求和然后返回,返回类型为Int:
1 2 3 4 5 |
def addNum( a:Int, b:Int ) : Int = { var sum = 0 sum = a + b return sum } |
代码简写,去掉返回类型和return关键字:
1 2 3 4 5 |
def addNum( a:Int, b:Int ) = { var sum = 0 sum = a + b sum } |
如果方法没有返回结果,可以将返回类型设置为Unit,类似Java中的void:
1 2 3 4 5 |
def addNum( a:Int, b:Int ) : Unit = { var sum = 0 sum = a + b println(sum) } |
在定义方法参数时,可以为某个参数指定默认值,在方法被调用时可以不为带有默认值的参数传入实参:
1 2 3 4 5 |
def addNum( a:Int=5, b:Int ) = { var sum = 0 sum = a + b sum } |
方法的调用,通过指定参数名称,只传入参数b:
addNum(b=2)
也可以将a,b两个参数都传入:
addNum(1,2)
2、函数
函数的定义与方法不一样,语法:
(参数列表)=>函数体
定义一个匿名函数,参数为a和b,且都是Int类型,函数体为a+b:
( a:Int, b:Int ) =>a+b
如果函数体有多行,可以将函数体放入一对{}中,并且可以通过一个变量来引用函数,变量相当于函数名称:
val f1=( a:Int, b:Int ) =>{ a+b }
对上述函数进行调用:
f1(1,2)
函数也可以没有参数:
val f2=( ) =>println("hello scala")
对上述函数进行调用:
f2()
3、方法与函数的区别
(1)方法是类的一部分,而函数是一个对象并且可以赋值给一个变量。
(2)函数可以作为参数传入到方法中。
例如,定义一个方法m1,参数f要求是一个函数,该函数有两个Int类型参数,且函数的返回类型为Int,方法体中直接调用该函数:
1 2 3 |
def m1(f: (Int, Int) => Int): Int = { f(2, 6) } |
定义一个函数f1:
val f1 = (x: Int, y: Int) => x + y
调用方法m1,并传入函数f1:
1 2 |
val res = m1(f1) println(res) |
输出结果为8。
(3)方法可以转换为函数
当把一个方法作为参数传递给其它的方法或者函数时,系统将自动将该方法转换为函数。
例如,有一个方法m2:
def m2(x:Int,y:Int) = x+y
调用(2)中的m1方法,并将m2作为参数传入,此时系统会自动将m2方法转为函数:
1 2 |
val res = m1(m2) println(res) |
输出结果为8。
除了系统自动转换外,也可以手动进行转换。在方法名称后加入一个空格和一个下划线,即可将方法转换为函数:
1 2 3 |
val f2=m2 _ val res=m1(f2) println(res) |
输出结果为8。
集合
Scala集合分为可变集合和不可变集合。可变集合可以对其中的元素进行修改、添加、移除;而不可变集合,永远不会改变,但是仍然可以模拟添加、移除或更新操作。这些操作都会返回一个新的集合,原集合的内容不发生改变。
Array数组
Scala中的数组分为定长数组和变长数组,定长数组初始化后不可对数组长度进行修改,而变长数组则可以修改。
1、定长数组
定义数组的同时可以初始化数据:
1 |
val arr=Array(1,2,3)//自动推断数组类型 |
或者
1 |
val arr=Array[Int](1,2,3)//手动指定数据类型 |
也可以定义时指定数组长度,稍后对其添加数据:
1 2 3 4 |
val arr=new Array[Int](3) arr(0)=1 arr(1)=2 arr(2)=3 |
可以使用for循环对数组进行遍历:
1 2 3 4 |
val arr=Array(1,2,3) for(i<-arr){ println(i) } |
Scala对数组提供了很多常用的方法,使用起来非常方便:
1 2 3 4 5 6 7 8 9 10 11 |
val arr=Array(1,2,3) //求数组中所有数值的和 val arrSum=arr.sum //求数组中的最大值 val arrMax=arr.max //求数组中的最小值 val arrMin=arr.min //对数组进行升序排序 val arrSorted=arr.sorted //对数组进行降序排序 val arrReverse=arr.sorted.reverse |
2、变长数组
变长数组使用类scala.collection.mutable.ArrayBuffer进行定义:
1 2 3 4 5 6 |
//定义一个变长Int类型数组 val arr=new ArrayBuffer[Int]() //向其中添加三个元素 arr+=1 arr+=2 arr+=3 |
也可以使用-=符号对变长数组中的元素进行删减,例如,去掉数组arr中值为3的元素:
1 |
arr-=3 |
若数组中有多个值为3的元素,将从前向后删除第一个匹配的值。
在数组arr的下标为0的位置插入两个元素1和2:
// arr.insert(0,1,2) // 经测试scala2.13.6版本这个方法没有第三个参数,只能:
1 2 |
arr.insert(0,1) arr.insert(0,2) |
从数组arr的下标为1的位置开始移除两个元素:
1 |
arr.remove(1, 2) |
List列表
Scala中的List分为可变List和不可变List,默认使用的List为不可变List。不可变List也可以增加元素,但实际上生成了一个新的List,原List不变。
1、不可变List
创建一个Int类型的List,名为nums:
1 |
val nums: List[Int] = List(1, 2, 3, 4) |
在该List的头部追加一个元素1,生成一个新的List:
1 |
val nums2=nums.+:(1) |
在该List的尾部追加一个元素5,生成一个新的List:
1 |
val nums3=nums:+5 |
List也支持合并操作,将两个List合并为一个新的List:
1 2 3 4 |
val nums1: List[Int] = List(1, 2, 3) val nums2: List[Int] = List(4, 5, 6) val nums3=nums1++:nums2 println(nums3) |
输出结果:
1 |
List(1, 2, 3, 4, 5, 6) |
Map映射
Scala中的Map也分可变的Map和不可变的Map,默认为不可变Map。
1、不可变Map
创建一个不可变Map:
1 2 3 4 5 |
val mp = Map( "key1" -> "value1", "key2" -> "value2", "key3" -> "value3" ) |
也可以使用以下写法:
1 2 3 4 5 |
val mp = Map( ("key1" , "value1"), ("key2" , "value2"), ("key3" , "value3") ) |
循环输出上述Map中的键值数据:
1 2 3 |
for((k,v)<-mp){ println(k+":"+v) } |
2、可变Map
创建可变Map需要引入类scala.collection.mutable.Map,创建方式与不可变Map相同。访问Map中key1的值,代码:
1 2 3 4 5 |
val mp = Map( ("key1" , "value1"), ("key2" , "value2") ) println(mp("key1")) |
修改键key1的值为value2,代码:
1 |
mp("key1")="value2" |
上述代码当key1存在时执行修改操作,若key1不存在则执行添加操作。
向Map中添加元素也可以使用+=符号:
1 |
mp+=("key3" -> "value3") |
或
1 |
mp+=(("key3","value3")) |
相对应的,从Map中删除一个元素可以使用-=符号:
1 |
mp-="key3" |
Tuple元组
元组是一个可以存放不同类型对象的集合,元组中的元素不可以修改。
1、定义元组
定义一个元组t:
1 |
val t=(1,"scala",2.6) |
或使用以下方式,其中Tuple3是一个元组类,代表元组的长度为3:
1 |
val t2 = new Tuple3(1,"scala",2.6) |
2、访问元组
可以使用方法_1、_2、_3访问其中的元素,例如,取出元组中第一个元素:
1 |
println(t._1) |
和数组、字符串的位置不同,元组的元素下标从1开始。
3、迭代元组
使用 Tuple.productIterator() 方法可以迭代输出元组的所有元素:
1 2 |
val t = (4,3,2,1) t.productIterator.foreach{ i =>println("Value = " + i )} |
Set哈希表
Set集合存储的对象不可重复。Set集合分为可变集合和不可变集合,默认情况下使用的是不可变集合,如果要使用可变集合,则需要引用 scala.collection.mutable.Set
包。
1、定义Set
定义一个不可变集合:
1 |
val set = Set(1,2,3) |
2、元素增减
与List集合一样,对于不可变Set进行元素的增加和删除,实际上会产生一个新的Set,原来的Set并没有改变:
1 2 3 4 5 6 |
//定义一个不可变set集合 val set = Set(1,2,3) //增加一个元素 val set1=set+4 //减少一个元素 val set2=set-3 |
3、常用方法
1 2 3 4 5 |
val site = Set("Ali", "Google", "Baidu") println(site.head) // 输出第一个元素 val set2=site.tail // 取得除了第一个元素的所有元素的集合 println(set2) println(site.isEmpty) //查看元素是否为空 |
使用++运算符可以连接两个集合:
1 2 3 4 |
val site1 = Set("Ali", "Google", "Baidu") val site2 = Set("Faceboook", "Taobao") val site=site1++site2 println(site) |
输出结果:
1 2 3 4 5 |
Set(Faceboook, Taobao, Google, Ali, Baidu) val num = Set(5,8,7,20,10,66) println(num.min) //输出集合中的最小元素 println(num.max) //输出集合中的最大元素 |
类和对象
1、类的定义
对象是类的具体实例,类是抽象的,不占用内存,而对象是具体的,占用存储空间。Scala中一个最简单的类定义是使用关键字class,类名必须大写。类中的方法用关键字def定义,代码:
1 2 3 4 5 6 |
class User{ private var age=20 def count(){ age+=1 } } |
如果一个类不写访问修饰符,则默认访问级别为Public。这与Java是不一样的。关键字new用于创建类的实例。例如,调用上述代码中的count()方法:
1 |
new User().count() |
2、单例对象
Scala中没有静态方法或静态字段,但是可以使用关键字object定义一个单例对象,单例对象中的方法相当于Java中的静态方法,可以直接使用“单例对象名.方法名”方式进行调用。单例对象除了没有构造器参数外,可以拥有类的所有特性。
例如,定义一个单例对象Person,该对象中定义了一个方法showInfo():
1 2 3 4 5 6 7 |
object Person{ private var name="zhangsan" private var age=20 def showInfo():Unit={ println("姓名:"+name+",年龄:"+age) } } |
可以在任何类或对象中使用代码Person.showInfo()对方法showInfo()进行调用。
3、伴生对象
当单例对象的名称与某个类的名称一样时,该对象被称为这个类的伴生对象。类被称为该对象的伴生类。类和它的伴生对象必须定义在同一个文件中,且两者可以互相访问其私有成员。例如以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Person() { private var name="zhangsan" def showInfo(){ println("年龄:"+Person.age) //访问伴生对象的私有成员 } } object Person{ private var age=20 def main(args: Array[String]): Unit = { var per=new Person() println("姓名:"+per.name) //访问伴生类的私有成员 per.showInfo() } } |
4、get和set方法
Scala默认会根据类的属性的修饰符生成不同的get和set方法,生成原则:
val修饰的属性,系统会自动生成一个私有常量属性和一个公有get方法。
var修饰的属性,系统会自动生成一个私有变量和一对公有get/set方法。
private var修饰的属性,系统会自动生成一对私有get/set方法,相当于类的私有属性,只能在类的内部和伴生对象中使用。
private[this]修饰的属性,系统不会生成get/set方法,即只能在类的内部使用该属性。
在Scala中,get和set方法并非被命名为getName和setName,而是被命名为name和name_=,由于JVM不允许在方法名中出现=,因此=被翻译成$eq。
1 2 3 4 5 6 7 |
private String name = "zhangsan"; public String name() { return this.name; } public void name_$eq(String x$1) { this.name = x$1; } |
除了系统自动生成get和set方法外,也可以手动进行编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Person { //声明私有变量 private var privateName="zhangsan" def name=privateName //定义get方法 def name_=(name:String): Unit ={ //定义set方法 this.privateName=name } } object Test{ def main(args: Array[String]): Unit = { var per:Person=new Person() //访问变量 per.name=“lisi” //修改 println(per.name) //读取 } } |
5、构造器
Scala中的构造器分为主构造器和辅助构造器。
主构造器的参数直接放在类名之后,且将被编译为类的成员变量,其值由初始化类时进行传入:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//定义主构造器,年龄age默认为18 class Person(val name:String,var age:Int=18) { } object Person{ def main(args: Array[String]): Unit = { //调用构造器并设置name和age字段 var per=new Person("zhangsan",20) println(per.age) println(per.name) per.name="lisi"//错误,val修饰的变量不可修改 } } |
将参数age设置为私有的,参数name设置为不可修改(val):
1 2 |
class Person(val name:String, private var age:Int) { } |
5、构造器
构造参数也可以不带val或var,此时默认为private[this] val:
1 2 |
class Person(name:String,age:Int) { } |
如果需要将整个主构造器设置为私有的,只需要添加private关键字即可:
1 2 |
class Person private(var name:String,var age:Int) { } |
除了可以有主构造器外,还可以有任意多个辅助构造器。辅助构造器的定义需要注意以下几项:
(1)辅助构造器的方法名称为this。
(2)每一个辅助构造器的方法体中必须首先调用其它已定义的构造器。
(3)辅助构造器的参数不能使用var或val进行修饰。
定义两个辅助构造器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Person { private var name="zhangsan" private var age=20 //定义辅助构造器一 def this(name:String){ this()//调用主构造器 this.name=name } //定义辅助构造器二 def this(name:String,age:Int){ this(name)//调用辅助构造器一 this.age=age } } |
上述构造器可以使用如下三种方式进行调用:
1 2 3 |
var per1=new Person//调用无参主构造器 var per2=new Person("lisi")//调用辅助构造器一 var per3=new Person("lisi",28)//调用辅助构造器二 |
除此之外,主构造器还可以与辅助构造器同时使用,在这种情况下,一般辅助构造器的参数要多于主构造器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//定义主构造器 class Person(var name:String,var age:Int) { private var gender="" //定义辅助构造器 def this(name:String,age:Int,gender:String){ this(name,age)//调用主构造器 this.gender=gender } } object Person{ def main(args: Array[String]): Unit = { //调用辅助构造器 var per=new Person("zhangsan",20,"male") println(per.name) println(per.age) println(per.gender) } } |
输出结果
1 2 3 |
zhangsan 20 male |
抽象类
Scala的抽象类使用关键字abstract定义,具有以下特征:
(1)抽象类不能被实例化。
(2)抽象类中可以定义抽象字段(没有初始化的字段)和抽象方法(没有被实现的方法),也可以定义被初始化的字段和被实现的方法。
(3)若某个子类继承了一个抽象类,则必须实现抽象类中的抽象字段和抽象方法。且实现的过程中可以添加override关键字也可以省略。若重写了抽象类中已经实现的方法,则必须添加override关键字。
定义一个抽象类Person:
1 2 3 4 5 6 7 8 9 |
abstract class Person { var name:String //抽象字段 var age:Int var address:String="北京" //普通字段 def speak() //抽象方法 def eat():Unit={ //普通方法 println("吃东西") } } |
定义一个普通类Teacher,并继承抽象类Person,实现Person中的抽象字段和抽象方法,并重写方法eat():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//继承了抽象类Person class Teacher extends Person{ //实现抽象字段 var name: String = "王丽" var age: Int = 28 //实现抽象方法 def speak(): Unit = { println("姓名:"+this.name) println("年龄:"+this.age) println("地址:"+this.address)//继承而来 println("擅长讲课") } //重写非抽象方法,必须添加override关键字 override def eat():Unit={ println("爱吃中餐") } } |
定义一个测试对象,调用Teacher类中的方法,代码:
1 2 3 4 5 6 7 8 |
object AppTest{ def main(args: Array[String]): Unit = { val teacher=new Teacher() //调用方法 teacher.speak() teacher.eat() } } |
输出结果:
1 2 3 4 5 |
姓名:王丽 年龄:28 地址:北京 擅长讲课 爱吃中餐 |
Trait特质
Scala特质使用关键字trait定义,类似Java中使用interface定义的接口。特质除了有Java接口的功能外,还有一些特殊的功能。定义了一个特质Pet:
1 2 3 4 5 6 7 8 9 |
//定义特质(宠物) trait Pet { var name:String //抽象字段 var age:Int def run //抽象方法 def eat: Unit ={ //非抽象方法 println("吃东西") } } |
定义一个普通类Cat,实现了上述特质Pet(必须实现未实现的字段和方法):
1 2 3 4 5 6 7 8 9 10 |
class Cat extends Pet var name:String="john" { //实现抽象字段 var age:Int=3 def run: Unit = {//实现抽象方法 println("会跑") } override def eat: Unit ={ //重写非抽象方法 println("吃鱼") } } |
若需要实现多个特质,可以通过with关键字添加额外特质,但位于最左侧的特质必须使用extends关键字:
1 2 3 4 5 6 7 8 |
trait Animal{ } trait Runable{ } //类Dog实现了三个特质 class Dog extends Pet with Animal with Runable{ //省略... } |
在类实例化的时候,可以通过with关键字混入多个特质,从而使用特质中的方法。例如,定义两个特质Runable、Flyable和一个类Bird:
1 2 3 4 5 6 7 8 9 10 |
//定义两个特质 trait Runable{ def run=println("会跑") } trait Flyable{ def fly=println("会飞") } //定义一个类 class Bird{ } |
在类Bird实例化时混入特质Runable和Flyable:
1 2 3 |
val bird=new Bird() with Runable with Flyable bird.run //输出结果“会跑” bird.fly //输出结果“会飞” |
谢谢!
Views: 73