由于Spark是由Scala编写的,为了使用Spark, 需要掌握Scala这门语言。由于Scala与Java有很多相似的地方,所以在Java的基础上注意其特性就能很快熟悉Scala。
Scala是一门以Java虚拟机(JVM)为运行环境并将面向对象和函数式编程的最佳特性结合在一起的静态类型编程语言,它具有以下特点:
- Scala是一门多范式的编程语言,Scala支持面向对象和函数式编程。多范式,就是多种编程方法的意思。有面向过程、面向对象、泛型、函数式四种程序设计方法。
- Scala在设计时参考了Java的设计思想,将函数式编程语言的特点融合到JAVA中。Scala源代码(.scala)会被编译成Java字节码(.class),然后运行于JVM之上,并可以调用现有的Java类库,实现两种语言的无缝对接。
- Scala单作为一门语言来看,非常的简洁高效。
1 安装配置
1.1 环境配置
从Scala官网下载对应版本的压缩包:https://www.scala-lang.org/download/
解压到指定目录后配置环境变量
之后在命令行输入scala启动Scala环境成功
1.2 IDEA创建Scala项目
- 在IDEA中安装Scala插件:
点击 File->Setting->点击 Plugins->在搜索框输入 Scala->点击 Install->点击 ok->点击 apply,重启IDEA后生效
- 为项目引入Scala框架支持:
在项目文件夹上点击右键-> Add Framework Support… ->选择 Scala后点击确定。如果第一次使用需要点击创建按钮,引入scala库后才会显示scala-sdk库
3. 创建scala源文件目录
先创建普通目录,右键点击 main 目录->New->点击 Diretory ->命名为scala
将该目录标记为源文件目录,右键点击 scala 目录->Mark Directory as->选择 Sources root
4. 创建类
右键点击 scala 目录->New->Scala Class->Kind 项选择 Object->Name 项输入类名HelloScala,就生成了一个scala对象。输入以下代码运行控制台输出"hello scala"
object HelloScala {
def main(args: Array[String]): Unit = {
println("hello scala")
}
}
查看HelloScala生成的target文件夹可以看到Object编译后生成HelloScala$.class和HelloScala.class两个文件
HelloScala类中有一个main()函数,其中调用HelloScala$
的静态对象MODULE$
public final class HelloScala (
public static void main(String[] paramArrayOfString) (
Hello$.MODULE$.main(paramArrayOfString);
}
}
HelloScala$.class
文件中通过静态对象调用HelloScala$的main()函数,Scala是一个完全面向对象的语言,所以没有静态语法,而是通过伴生对象单例的方式来调用静态方法
import scala.Predef.;
public final class HelloScala$ {
public static HelloScala$ MODULE$;
static {
new HelloScala$();
}
public void main(final String[] args) {
.MODULE$.println("hello scala");
}
private HelloScala$() {
MODULE$ = this;
}
}
2 变量
通过官网可以查看Scala的API:https://www.scala-lang.org/api/2.12.15/scala/index.html
2.1 注释
Scala 注释使用和 Java 完全一样。
//单行注释
/* 多行注释 */
/**
* 文档注释
*/
2.2 输入输出
通过StdIn
读取键盘的标准输入
// 读取一行字符串
var name = StdIn.readLine()
// 读取short
var age = StdIn.readShort()
// 读取double类型
var sal = StdIn.readDouble()
Scala的输出除了和Java类似使用println()
按行输出和printf()
格式化输出外,还可以使用三个双引号进行多行字符串输出,其中使用|
进行拼接
val s =
"""
|select
| name,
| age
|from user
|where name="zhangsan"
""".stripMargin
println(s)
还可以直接在字符串中通过$
引用变量,在${}
内进行变量运算,注意要在字符串前面加s
var age=22
println(s"今年我 $age 岁,明年我${age+1}岁")
//输出:今年我 22 岁,明年我23岁
2.3 变量
Scala用关键字var
来声明变量,val
声明常量,常量声明后不可更改,具有以下几个特点
- 类型推导:声明变量时,类型可以省略,编译器自动推导
- 强数据类型:类型确定后,就不能修改
- 变量声明时,必须要有初始值
//var 变量名 [: 变量类型] = 初始值
var i:Int = 10
//val 常量名 [: 常量类型] = 初始值
val j:Int = 20
虽然val对象本身不变,但是在使用时,其指向的内容却可以改变
class Person{
var name : String = "jinlian"
}
object TestVar {
def main(args: Array[String]): Unit = {
// p1 是 var 修饰的,p1 的属性可以变,而且 p1 本身也可以变
var p1 = new Person()
p1.name = "dalang"
p1 = null
// p2 是 val 修饰的,那么 p2 本身不可变,它固定地指向一个内存地址
val p2 = new Person()
// p2 = null // 错误的,因为 p2 是 val 修饰的
p2.name="jinlian" //但是p2指向的内容可以变,内存地址中的内容可变
}
}
Scala 中的标识符声明,基本和 Java 一样以字母或者下划线开头,后接字母、数字、下划线,但是细节上会有所变化。
// 若以操作符开头,则只能包含操作符
var +*-/#! : String = "" // ok
var +*-/#!1 : String = "" // 错误
//用反引号`....`包括的任意字符串,即使是关键字
var if : String = "" // error 不能用关键字
var `if` : String = "" // ok 用反引号包裹
2.4 数据类型
Java有基本类型char、int、double等,为了便于使用分别对其进行了包装产生了包装类Character、Integer和Double等,所以Java语言并不是真正的面向对象。Scala中的一切数据都是对象,基本数据类型如下所示:
- Scala中一切数据都是Any的子类,整体上分为两大类:数值类型AnyVal和引用类型AnyRef。
- 其中数值类型中的Byte、Short、Int、Long、Float、Double低精度的类型可以向高精度类型进行自动转换。
- StringOps用于字符串处理,可以看作是Java的String类的增强
整数类型有Byte、Short、Int、Long,长度分别为1、2、4、8字节,对应最大可以表示8、16、32、64位有符号补码整数。Scala 的整型,默认为 Int 型,声明 Long 型,须后加l
或L
。
var n2:Byte = -128
var n3:Byte = 128 // 报错,超过Byte有符号补码所能表示的最大范围
var n6 = 9223372036854775807L // 声明Long类型
浮点类型有4字节32位的单精度浮点类型Float
、8字节64位双精度浮点数Double
,均遵循IEEE 754标准。默认为 Double 型,声明 Float 型常量,须后加f
或F
。
var n7 = 2.2345678912f
var n8 = 2.2345678912
println("n7=" + n7)
println("n8=" + n8)
/*运行的结果
n7=2.2345679
n8=2.2345678912
*/
字符类型Char
表示单个字符,长度为2字节。字符常量是用单引号 ’ ’ 括起来的单个字符
var c1: Char = 'a'
println("c1=" + c1) // 输出c1=a
var c2: Char = 'a' + 1
println("c2=" + c2) //输出c2=b
布尔类型Boolean
占 1 个字节,数据只允许取值 true 和 false
数据类型Unit
表示无值,只有一个对象()
。用作方法的返回值,对应于Java中的返回值void,表示方法没有返回值。
def main(args: Array[String]): Unit = { // main函数没有返回值
}
Null
只有一个对象就是null。它是所有引用类型(AnyRef)的子类,因此Null 可以赋值给任意引用类型(AnyRef),但是不能赋值给值类型(AnyVal)
var cat = new Cat();
cat = null // 正确
var n1: Int = null // 错误
Nothing
是所有数据类型的子类,主要在函数没有明确返回值时将结果返回给任意变量接收,可以作为没有正常返回值的方法的返回类型
def test() : Nothing={ // 返回抛出的异常
throw new Exception()
}
自动类型转换:当 Scala 程序在进行赋值或者运算时,精度小的类型自动转换为精度大的数值类型,按照Byte、Short、Int、Long精度由低到高逐阶提升。Byte不会向Char转,但是Char可以在进行计算时转为Int类型参与计算。
//多种类型的数据混合运算时,数据转换成高精度类型再进行计算。
var n = 1 + 2.0
println(n) // n为 Double
// Byte和Char转为Int
var b: Byte = 1
var c: Char = 1
var i = b + c
println(i)
强制类型转换可以将数据强制转换为目标类型,例如可以将数据由高精度转换为低精度,使用时要加上强制转函数,但可能造成精度降低或溢出。强制转换为数值类型时的函数为toInt、toDouble、toByte等类似函数。如果希望转换为String类型只需要在基本类型值后+“”
// 向低精度转换
var i: Int = 2.5.toInt
println(i) // 输出2,精度损失
// 字符串转数值类型
var s1 : String = "12"
var n1 : Byte = s1.toByte
// 转换为字符串
var str1 : String = true + ""
2.5 运算符
Scala 运算符的使用和 Java 运算符的使用基本相同
算术运算符+、-、*、/、%,逻辑运算符&&、||、!和Java一样
关系运算符 >、<、=、!=也相同,Scala中的==会比较二者的内容,类似于Java中的equals
def main(args: Array[String]): Unit = {
val s1 = "abc"
val s2 = new String("abc")
println(s1 == s2) // true 二者中的内容相同
println(s1.eq(s2)) // false 但不是同一块内存地址
}
需要注意的是Scala 中没有++、–操作符,可以通过+=、-=来实现同样的效果;
在 Scala 中其实是没有运算符的,所有运算符都是方法,只不过在使用时省略了调用的.
和()
// 标准的加法运算
val i:Int = 1.+(1)
// (1)当调用对象的方法时,.可以省略
val j:Int = 1 + (1)
// (2)如果函数参数只有一个,或者没有参数,()可以省略
val k:Int = 1 + 1
println(1.toString())
println(1 toString()) //省略.
println(1 toString) //省略()
2.6 流程控制
分支结构
Scala的if
分支控制的使用和Java相似
if (age < 18){
println("童年")
}else if(age>=18 && age<30){
println("中年")
}else{
println("老年")
}
Scala的if else表达式可以用于返回值
val res :String = if (age < 18){
"童年"
}else if(age>=18 && age<30){
"中年"
}else{
"老年"
}
可以用if else实现三元运算符的效果
val res:Any = if (age < 18) "童年" else "成年"
需要注意的是在 Scala 中没有 Switch,而是使用模式匹配来处理。模式匹配采用 match
关键字声明,每个分支采用 case
关键字进行声明,匹配时从第一个 case 分支开始,如果匹配成功那么执行对应的逻辑代码;否则继续执行下一个分支进行判断。如果所有 case 都不匹配,那么会执行 case _ 分支。
var result = operator match {
case '+' => a + b
case '-' => a - b
case '*' => a * b
case '/' => a / b
case _ => "illegal"
}
模式匹配可以匹配所有的字面量,包括字符串,字符,数字,布尔值等,还可以对对象、集合等复杂类型进行精确的匹配
val result = arr match {
case Array(0) => "0" //匹配 Array(0) 这个数组
case Array(x, y) => x + "," + y //匹配有两个元素的数组,然后将将元素值赋给对应的 x,y
case Array(0, _*) => "以 0 开头的数组" //匹配以 0 开头和数组
case _ => "something else"
}
循环结构
Scala中的for循环有多种推导式
使用关键字to
实现前后闭合的范围内数据遍历
// i 取1、2、3
for(i <- 1 to 3){
print(i + " ")
}
until
实现前闭后开的遍历
// i取1、2
for(i <- 1 until 3) {
print(i + " ")
}
循环守卫,即循环保护式(也称条件判断式,守卫)。保护式为 true 则进入循环体内部,为 false 则跳过,类似于 continue。
//打印10以内的偶数
for(i <- 1 to 10 if i % 2 == 0) {
print(i + " ")
}
循环步长by
,搁几个数遍历
// 搁3个数打印一次
for (i <- 1 to 10 by 3) {
println("i=" + i)
}
此外,在for的括号中可以进行嵌套表达
for(i <- 1 to 3; j <- 1 to 3) {
println(" i =" + i + " j = " + j)
}
// 上面代码等价于
for (i <- 1 to 3) {
for (j <- 1 to 3) {
println("i =" + i + " j=" + j)
}
}
还可以在for推导式中引入变量
for(i <- 1 to 3; j = 4 - i) {
println("i=" + i + " j=" + j)
}
使用yield
关键字可以将遍历过程中处理的结果返回到一个新 Vector 集合中。
var res = for(i <-1 to 10) yield {
i * 2
}
println(res) //输出Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
使用reverse
关键字可以从后往前遍历
// 倒序打印10到1
for(i <- 1 to 10 reverse){
println(i)
}
While 和 do…While 的使用和 Java 语言中用法相同。与 for 语句不同,while 语句没有返回值.
Scala 内置控制结构特地去掉了 break 和 continue,是为了更好的适应函数式编程,推荐使用函数式的风格解决break和continue的功能,而不是一个关键字。Scala中使用breakable
控制结构来实现 break 和 continue 功能。
Breaks.breakable(
for (elem <- 1 to 10) {
println(elem)
if (elem == 5) Breaks.break()
}
)
//简略写法
breakable {
for (elem <- 1 to 10) {
println(elem)
if (elem == 5) break
}
}
3 函数式编程
Scala是一个将面向函数和面向对象编程完美融合的语言。函数式编程是指将问题分解成一个一个的步骤,将每个步骤封装为函数,通过调用这些封装好的步骤解决问题。Scala中的万物皆函数,函数也可以当作一个值进行传递。
Scala中函数的定义语法如下
//关键字 函数名(参数名:参数类型):返回值类型
def sum(x: Int, y: Int): Int = {
// 函数体
x + y
}
3.1 函数与方法的区别
函数是指更为广泛的未完成某一功能的程序语句的集合。具体地,和Java一样,将类中的定义的函数称之为方法,只能通过对象进行调用。而Scala中的函数可以定义在任何地方,并且可以不借助对象直接使用。
如下所示,在Person类中首先定义了属于该类的方法sayHi(),之后在main()方法中又定义了一个函数sayHi()。首先通过直接调用函数的方式对函数sayHi()进行调用,之后通过对象调用方法sayHi()
object Person {
// 定义对象的方法
def sayHi(): Unit = {
println("调用对象方法sayHi")
}
def main(args: Array[String]): Unit = {
// 定义函数
def sayHi(): Unit = {
println("调用函数sayHi")
}
sayHi() // 输出:调用函数sayHi
Person.sayHi() // 输出:调用对象方法sayHi
}
}
区分清楚方法和函数之后需要注意,对象的方法可以进行重载,但是函数却没有重载和重写的概念
object Person {
def sayHi(): Unit = {
println("调用对象方法sayHi")
}
// 方法可以进行重载
def sayHi(name: String): Unit = {
println("重载sayHi,我是" + name)
}
def main(args: Array[String]): Unit = {
def sayHi(): Unit = {
println("调用函数sayHi")
}
// 函数无法重载,报错:此范围已定义sayHi
def sayHi(name: String): Unit = {
println("重载sayHi,我是" + name)
}
}
}
Scala 语言可以在任何的语法结构中声明任何的语法,因此可以在随时进行引入,并且进行函数的嵌套
def main(args: Array[String]): Unit = {
// 随时引入
import java.util.Date
new Date()
def test(): Unit ={
def test1(name:String):Unit={
println("函数可以嵌套定义")
}
}
}
函数的参数:一般将有默认值的参数放置在参数列表的后面,
def Person(name: String, sex: String = "男"): Unit = {
}
可变参数一般放置在最后
def Person(name: String, age: Int*): Unit = {
}
3.2 函数的省略
为了简便,Scala中经常遇到函数省略写法
// 1 return 可以省略,默认使用函数体的最后一行代码作为返回值
def sum(x: Int, y: Int): Int = {
x + y
}
// 2 如果函数体只有一行代码,可以省略花括号
def sum(x: Int, y: Int): Int = x + y
// 3 返回值类型如果能够推断出来,那么可以省略
def sum(x: Int, y: Int) = x + y
// 如果有 return,则不能省略返回值类型
def sum(x: Int, y: Int): Int = {
return x + y
}
// 如果函数明确声明 unit,那么 return 关键字不起作用
def fun(): Unit = {
return "return返回"
}
println(fun()) //输出:()
// 4 如果无返回值类型,可以省略等号。无返回值的函数称为过程
def fun() {
println("无返回值")
}
// 5 在调用具有参数列表的无参函数时,括号可以省略
def fun() = "无参函数"
println(fun)
// 没有参数列表的无参函数,括号必须省略
def fun = "无参函数"
println(fun()) //报错
println(fun)
3.3 函数传递
函数不仅可以调用,还可以作为整体进行传递。
如下所示定义函数func,在函数内打印“直接调用函数”,并将字符串“函数返回值”返回。当直接用变量res接收时会得到func的返回值;而在函数后面加上_
相当于把函数作为一个整体传递给变量f,之后f也可作为函数执行。此外,通过() => String
指定变量f2的类型为:输入为空、返回值为String类型的函数,那么下划线也可以省略。
def func(): String = {
println("直接调用函数")
return "函数返回值"
}
val res = func()
println(res) // 输出“函数返回值”
val f = func _
f() // 输出:直接调用函数
// 直接传递函数
val f2: () => String = func
f2()
函数也可以作为参数值进行传递。如下所示定义函数func1,函数func2接受一个函数作为参数,并将其作为函数在其内部调用。之后调用func2并将func1作为参数传入,执行结果如下。
def func1(): Unit = {
println("调用func1")
}
def func2(f: () => Unit) = {
print("在func2内")
f() // 在函数内部调用传入的参数函数
}
func2(func1) // 输出:在func2内调用func1
函数也能作为返回值返回
def f1() = {
def f2() = {
}
f2 _ // 将函数作为返回值
}
匿名函数
就是指没有名字的函数,只关心函数的实现而不关注函数名。其形式为 (参数名:参数类型) => {函数体}
如下所示首先定义函数func,以输入为String输出为Unit的函数f作为形参,在其函数体内调用作为参数的函数f。之后在使用func函数时,将匿名函数作为参数传入,这样func在接收到参数后进行执行,输出“在func内调用匿名函数”
def func(f: String => Unit): Unit = {
f("在func内调用")
}
func((s: String) => { println(s + "匿名函数")}) // 输出:在func内调用匿名函数
匿名函数有如下简写的情况
// 1 参数的类型可以省略,会根据形参进行自动的推导
func((s) => { println(s + "匿名函数")})
// 2 若只有一个参数,则圆括号可以省略
func(s => { println(s + "匿名函数")})
// 3 函数如果只有一行,则大括号也可以省略
func(s => println(s + "匿名函数"))
// 4 如果参数只出现一次,则参数省略用_代替
func(println( _ ))
函数闭包
将函数中用到的所有变量和函数打包在一起保存起来。这样在将函数作为指返回后,即使原来环境中的局部变量被释放掉,函数仍然可以正常运行。
函数层次化:为了降低函数的耦合度,将把一个参数列表的多个参数拆分为多个参数列表进行传递。如下所示,将参数a、b拆分到了两个列表
def f3(a: Int)(b: Int) = {
a + b
}
println(f3(2)(3))
传名参数
当函数作为变量进行传递时,传递的不是一个结果值,而是一个代码块,每次出现都会执行对应的代码。在代码块作为参数时需要根据返回值指定类型,例如=>Int
代表返回值为Int的代码块。
def f = () => {
println("f...")
10
}
def foo1(a: Int): Unit = {
println(a)
println(a)
}
foo1(f()) // 传递值,f只被调用一次
/* 输出
f...
10
10
*/
def foo2(a: => Int): Unit = {
println(a)
println(a)
}
foo2(f()) // 传递函数,f被调用了两次
/* 输出
f...
10
f...
10
*/
惰性加载当函数返回值被声明为 lazy 时,函数的执行将被推迟,直到我们首次对此取值,该函数才会执行。这种函数我们称之为惰性函数。
4 面向对象
Scala 的面向对象思想和 Java 的面向对象思想和概念是一致的,并在此基础上进行了补充扩展。
4.1 包管理
为了更好地区分类名,控制访问范围,Scala也用包Package对类进行管理。不同的是Scala可以在一个源文件中嵌套声明多个package,这样子包中的类可以直接访问父包中的内容
package com{
package tory{
package scala{
}
}
}
关于包的使用注意以下几点
- 在 Scala 中可以为每个包定义一个同名的包对象,其下所有 class 和 object 可以直接访问该包中的成员。
- 和 Java 一样,可以在顶部使用 import 导入,在这个文件中的所有类都可以使用。
- 局部导入:什么时候使用,什么时候导入。在其作用范围内都可以使用
- 通配符导入多个包:import java.util._
- 给导入的类起名:import java.util.{ArrayList=>AL}
- 屏蔽类:import java.util.{ArrayList =>,}
- 导入一个包的多个类:import java.util.{HashSet, ArrayList}
- 导入包的绝对路径:new root.java.util.HashMap
4.2 类
一个.scala 文件中可以定义多个类。Scala 中没有 public,默认可见性就是公开的。但是其底层实现上是通过get 方法(obj.field())和 set 方法(obj.field_=(value))来实现对属性的获取和赋值的,因此Scala不推荐将属性设置为private并通过getter、setter方法来操作。但有时为了和其他Java框架进行兼容,也可以通过@BeanProperty
注解为属性设置getter和setter方法。
protected 为受保护权限,Scala 中受保护权限比 Java 中更严格,同类、子类可以访问,同包无法访问。
class Person {
var name: String = "bobo" //定义属性
var age: Int = _ // _表示给属性一个默认值,val 修饰的属性不能赋默认值,必须显示指定
//Bean 属性(@BeanProperty)
// 通过注解自动生成规范的 setXxx/getXxx 方法
@BeanProperty
var sex: String = "男"
}
Scala 构造对象也需要调用构造方法,并且可以有任意多个构造方法。Scala 类的构造器包括:主构造器和辅助构造器。
- 主构造器:由于类名和构造函数名是一样的,那么就可以把类名后面加上形参列表,从而当作主构造器。构造器参数列表默认为局部变量,如果用var修饰就是类成员属性。如果主构造器无参数,小括号可省略,构建对象时调用的构造方法的小括号也可以省略。可以在参数列表()前添加关键字private将主构造器变为私有。
- 辅助构造器:函数的名称
this
,可以有多个进行重载。辅助构造器无法直接创建建对象,必须直接或者间接调用主构造方法。
class Person(var name: String) { // 主构造器
var age: Int = _
def this(name: String, age: Int) { // 辅助构造器
this(name) // 调用主构造器
this.age = age
}
def sayHi(): Unit = {
println("我叫" + name + ",年龄" + age)
}
}
object Person {
def main(args: Array[String]): Unit = {
val p1 = new Person("小李", 18)
p1.sayHi()
}
}
scala 是单继承,子类继承父类的属性和方法,子类中调用父类的方法使用 super 关键字。如果重写需要通过关键字override
声明。
多态也是通过动态绑定实现的。需要注意的是Scala 中属性和方法都是动态绑定,而 Java 中只有方法为动态绑定。
如下所示,Teacher类为Person类的子类,通过val teacher1: Person = new Teacher
创建一个Teacher对象,这里指针p的类型是Person,但是它并没有和Person类绑定,而是在运行过程中动态判定其类型,在输出属性name和调用方法hello()时都输出了teacher的信息。
class Person {
val name: String = "person"
def hello(): Unit = {
println("hello person")
}
}
class Teacher extends Person {
override val name: String = "teacher" // 重写父类属性
override def hello(): Unit = { // 重写父类方法
println("hello teacher")
}
}
object Test {
def main(args: Array[String]): Unit = {
val teacher: Teacher = new Teacher()
println(teacher.name)
teacher.hello()
val p: Person = new Teacher
println(p.name) // 输出teacher
p.hello() // 输出hello teacher
}
}
如下所示为Java中多态的实现,同样定义了Person类的指针p,虽然其方法hello()输出hello teacher但是其属性名称却是person,可见其方法实现了动态绑定,而属性name却没有
public class TestDynamic {
public static void main(String[] args) {
Teacher teacher = new Teacher();
Person p = new Teacher();
System.out.println(teacher.name);
teacher.hello();
System.out.println(p.name); // 输出person
p.hello(); // 输出hello teacher
}
}
抽象类:在类属性定义时,如果一个属性没有初始化就是抽象属性,只有在abstract
关键字标记的抽象类中才能定义抽象属性和抽象方法,只声明而没有实现的方法就是抽象方法。如果父类为抽象类,那么子类需要将抽象的属性和方法实现,否则子类也需声明为抽象类。
如下所示为三个类方法
//(1)判断对象是否为某个类型的实例
val bool: Boolean = person.isInstanceOf[Person]
if ( bool ) {
//(2)将对象转换为某个类型的实例
val p1: Person = person.asInstanceOf[Person]
println(p1)
}
//(3)获取类的信息
val pClass: Class[Person] = classOf[Person]
枚举类需要继承Enumeration
,应用类需要继承App
object Color extends Enumeration {
val RED = Value(1, "red")
val YELLOW = Value(2, "yellow")
val BLUE = Value(3, "blue")
}
4.3 单例对象
Scala语言是完全面向对象的语言,所以并没有静态的概念。但是为了能够和Java语言交互,就产生了一种特殊的对象来模拟类对象,该对象为单例对象。若单例对象名与类名一致,则称该单例对象这个类的伴生对象,这个类的所有“静态”内容都可以放置在它的伴生对象中声明。
单例对象采用 object 关键字声明,单例对象对应的类称之为伴生类,伴生对象的名称应该和伴生类名一致。单例对象中的属性和方法都可以通过伴生对象名(类名)直接调用访问。
class Person { // 伴生类
var name: String = "bobo"
}
object Person { // 伴生对象
var country: String = "China"
}
object Test {
def main(args: Array[String]): Unit = {
// 通过伴生对象访问属性
println(Person.country)
}
}
可以通过函数式的风格创建对象,即类名()
,其本质是在调用该对象的 apply()
方法,在apply中调用构造器,进而统一面向对象编程和函数式编程的风格
class Person {
println("调用主构造器")
}
object Person {
def apply(): Person = {
println("调用apply")
new Person()
}
}
object Test {
def main(args: Array[String]): Unit = {
var p = Person() // 用函数式的风格创建对象
/* 输出
调用apply
调用主构造器
*/
}
}
4.4 Trait
Scala 语言中采用特质 trait(特征)来代替接口的概念,即将具有相同特征的多个类抽象出来,声明为trait
trait 中即可以有抽象属性和方法,也可以有具体的属性和方法。
trait PersonTrait {
// 声明属性
var name:String = _
// 声明方法
def eat():Unit={
}
// 抽象属性
var age:Int
// 抽象方法
def say():Unit
}
与Java只能实现一个接口不同,一个类可以混入(mixin)多个特质,这也是对Scala单继承机制的一种补充。如果有多个特质或存在父类,那么需要采用 with
关键字连接。所有的 Java 接口都可以当做 Scala 特质使用。还可以在创建对象时动态混入trait。
// 继承多个trait
class Teacher extends PersonTrait with java.io.Serializable {
override def say(): Unit = {
println("say")
}
override var age: Int = _
}
object TestTrait {
def main(args: Array[String]): Unit = {
// 动态混入:灵活扩展对象
val t2 = new Teacher with SexTrait {
override var sex: String = "男"
}
//调用混入 trait 的属性
println(t2.sex)
}
}
当混入的特质中具有相同的方法造成冲突时:
- 如果两个 trait 之间没有任何关系,则直接在中重写冲突方法
- 两个 trait A、B继承自相同的 trait C,采用特质叠加:将混入的多个 trait 中的冲突方法叠加起来,依次执行A、B、C的部分
4.5 泛型
Scala在传统泛型的基础上还针对集合增加了协变和逆变
- 协变:Son 是 Father 的子类,则 MyList[Son] 也作为 MyList[Father]的“子类”。
- 逆变:Son 是 Father 的子类,则 MyList[Son]作为 MyList[Father]的“父类”。
class MyList[+T]{ //协变
}
class MyList[-T]{ //逆变
}
class MyList[T] //不变
泛型还可以通过上下限对传入的类型进行限定,上限就是指传入的参数
Class PersonList[T <: Person]{ //泛型上限,传入的类型T为Person的子类或自身
}
Class PersonList[T >: Person]{ //泛型下限,传入Person或其父类
}
5 集合
Scala 的集合有三大类:序列 Seq、集 Set、映射 Map,所有的集合都扩展自 Iterable。对于几乎所有的集合类,Scala 都同时提供了可变和不可变的版本
- 不可变集合:scala.collection.immutable。集合对象不可修改,每次修改就会返回一个新对象,类似于 java 中的 String 对象
- 可变集合: scala.collection.mutable。可以直接对原对象进行修改,而不会返回新的对象。类似于 java 中 StringBuilder 对象
如下所示为不可变集合继承图
下图为可变集合
集合常见的共有函数如下所示
//(1)获取集合长度
println(list.length)
//(2)获取集合大小,等同于 length
println(list.size)
//(3)循环遍历
list.foreach(println)
//(4)迭代器
for (elem <- list.itera tor) {
println(elem)
}
//(5)生成字符串
println(list.mkString(","))
//(6)是否包含
println(list.contains(3))
5.1 数组
如下所示为对不可变数组Array
进行定义和访问的常见方法
// 1 数组定义,长度为4的Int型数组
val arr01 = new Array[Int](4)
println(arr01.length) // 4
// 1.2 使用函数式的apply方法创建数组并直接赋初值
val arr1 = Array(1, 2)
//(2)数组赋值
//(2.1)修改某个元素的值
arr01(3) = 10
//(2.2)采用方法的形式给数组赋值
arr01.update(0,1)
//(3)遍历数组
//(3.1)打印数组
println(arr01.mkString(","))
//(3.2)普通遍历
for (i <- arr01) {
println(i)
}
//(3.3)对每个元素使用函数
def printx(elem:Int): Unit = {
println(elem)
}
arr01.foreach(printx)
arr01.foreach(println)
//(4)增加元素(由于创建的是不可变数组,增加元素其实是产生新的数组)
val ints: Array[Int] = arr01 :+ 5
println(ints)
如下所示为可变数组ArrayBuffer
的使用
//(1)创建并初始赋值可变数组
val arr01 = ArrayBuffer[Any](1, 2, 3)
println(arr01.length) // 3
println("arr01.hash=" + arr01.hashCode())
//(2)遍历数组
for (i <- arr01) {
println(i)
}
//(3)增加元素
//(3.1)追加数据
arr01.+=(4)
//(3.2)向数组最后追加数据
arr01.append(5,6)
//(3.3)向指定的位置插入数据
arr01.insert(0,7,8)
println("arr01.hash=" + arr01.hashCode()) // 数组的哈希值不变
//(4)修改元素
arr01(1) = 9 //修改第 2 个元素的值
通过arr1.toBuffer
可以将不可变数组转可变数组,arr2.toArray
将可变数组转不可变数组
如下所示为多维数组的创建和使用
// 创建一个3×4的二维数组
val arr = Array.ofDim[Int](3, 4)
arr(1)(2) = 88
//(2)遍历二维数组
for (a <- arr) { //a 就是一维数组
for (i <- a) {
print(i + " ")
}
println()
}
5.2 List
如下所示为不可变List
的创建和使用
//创建一个 List(数据有顺序,可重复)
val list: List[Int] = List(1,2,3,4,3)
// 访问元素
println(list(0))
// 遍历
list.foreach(println)
// 增加数据
val list2 = list.+:(5)
// 将元素5添加到集合,运算规则从右向左
val list1 = 5::list
// 将集合list拆分成单个元素后逐个添加到list1
val list2 = list ::: list1
如下所示为可变 ListBuffer
的创建和使用
//(1)创建一个可变集合
val buffer = ListBuffer(1,2,3,4)
//(2)向集合中添加数据
buffer.+=(5)
buffer.append(6)
buffer.insert(1,2)
//(3)打印集合数据
buffer.foreach(println)
//(4)修改数据
buffer(1) = 6
buffer.update(1,7)
//(5)删除数据
buffer.-(5)
5.3 Set集合
如下所示为不可变集合Set
的使用
//(1)Set 默认是不可变集合,数据无序
val set = Set(1,2,3,4,5,6)
//(2)数据不可重复
val set1 = Set(1,2,3,4,5,6,3)
//(3)遍历集合
for(x<-set1){
println(x)
}
默认情况下Scala 使用的是不可变集合,如果想使用可变集合需要引用scala.collection.mutable.Set
//(1)创建可变集合
val set = mutable.Set(1,2,3,4,5,6)
//(3)集合添加元素
set += 8
//(4)向集合中添加元素,返回一个新的 Set
val ints = set.+(9)
//(5)删除数据
set-=(5)
5.4 Map集合
如下所示为不可变集合Map
//(1)创建不可变集合 Map
val map = Map( "a"->1, "b"->2, "c"->3 )
//(2)循环打印
map.foreach((kv)=>{println(kv)})
//(3)访问数据
for (elem <- map.keys) {
// 使用 get 访问 map 集合的数据
println(elem + "=" + map.get(elem).get)
}
//(4)如果 key 不存在,返回 0
println(map.get("d").getOrElse(0))
println(map.getOrElse("d", 0))
使用可变集合需要引入mutable.Map
//(1)创建可变集合
val map = mutable.Map( "a"->1, "b"->2, "c"->3 )
//(3)向集合增加数据
map.+=("d"->4)
// 将数值 4 添加到集合,并把集合中原值 1 返回
val maybeInt: Option[Int] = map.put("a", 4)
println(maybeInt.getOrElse(0))
//(4)删除数据
map.-=("b", "c")
//(5)修改数据
map.update("d",5)
map("d") = 5
5.5 集合函数
集合集成了许多常用的操作函数
//(1)获取集合的头
println(list1.head)
//(2)获取集合的尾(不是头的就是尾)
println(list1.tail)
//(3)集合最后一个数据
println(list1.last)
//(4)集合初始数据(不包含最后一个)
println(list1.init)
//(5)反转
println(list1.reverse)
//(6)取前(后)n 个元素
println(list1.take(3))
println(list1.takeRight(3))
//(7)去掉前(后)n 个元素
println(list1.drop(3))
println(list1.dropRight(3))
//(8)并集
println(list1.union(list2))
//(9)交集
println(list1.intersect(list2))
//(10)差集
println(list1.diff(list2))
//(11)拉链 注:如果两个集合的元素个数不相等,那么会将同等数量的数据进行拉链,多余的数据省略不用
println(list1.zip(list2))
//(12)滑窗
list1.sliding(2, 5).foreach(println)
还有一些计算有关的函数
//(1)求和
println(list.sum)
//(2)求乘积
println(list.product)
//(3)最大值
println(list.max)
//(4)最小值
println(list.min)
//(5)排序
// (5.1)按照元素大小排序
println(list.sortBy(x => x))
// (5.2)按照元素的绝对值大小排序
println(list.sortBy(x => x.abs))
// (5.3)按元素大小升序排序
println(list.sortWith((x, y) => x < y))
// (5.4)按元素大小降序排序
println(list.sortWith((x, y) => x > y))
如下所示为对集合元素进行操作的函数
//(1)过滤
println(list.filter(x => x % 2 == 0))
//(2)转化/映射
println(list.map(x => x + 1))
//(3)扁平化
println(nestedList.flatten)
//(4)扁平化+映射 注:flatMap 相当于先进行 map 操作,在进行 flatten操作
println(wordList.flatMap(x => x.split(" ")))
//(5)分组
println(list.groupBy(x => x % 2))
// 6 聚合
val list = List(1,2,3,4)
val i1 = list.reduceLeft((x,y) => x-y)
val i2 = list.reduceRight((x,y) => x-y) // 4-3-2-1=-2
6 异常
Scala的异常处理和Java类似,将可疑代码封装在 try
块中,之后使用了一个 catch
处理程序来捕获异常。如果发生任何异常,catch 处理程序将处理它,程序将不会异常终止。最后无论是否异常都执行finally
子句。
try {
var n= 10 / 0
}catch {
case ex: ArithmeticException=>{
// 发生算术异常
println("发生算术异常")
}
case ex: Exception=>{
// 对异常处理
println("发生了异常 1")
println("发生了异常 2")
}
}finally {
println("finally")
}
但是 Scala 没有“checked(编译期)”异常, 即 Scala 没有编译异常这个概念,异常都是在运行的时候捕获处理。
用 throw 关键字,抛出一个异常对象。所有异常都是 Throwable 的子类型。throw 表达式是有类型的,就是 Nothing,因为 Nothing 是所有类型的子类型,所以 throw 表达式可以用在需要类型的地方
def test():Nothing = {
throw new Exception("异常")
}