SCALA
Scala 是一种运行在 JVM上的函数式的面向对象语言。
- Scala 是兼容的:兼容 Java,可以访问庞大的 Java 类库;
- Scala 是精简的:Scala 表达能力强,一行代码抵得上多行 Java 代码,开发速度快。可以让程序短小精悍,看起来更简洁,更优雅;
- Scala 是静态类型的:Scala 拥有非常先进的静态类型系统,支持类型推断和模式匹配等;
- Scala 可以开发大数据应用程序:例如 Spark、Flink 等。
基础语法*
打印输出
正常情况下 Scala 语句以不断行结尾(不需要分号),为了节省带宽/存储等,Scala 也支持将多行代码写在同一行,此时语句需要使用分号来结束,最后一条代码的分号可以省略。
scala
// 一条语句独占一行
println("Hello World!")
// 多条语句在一行
println("Hello World!"); println("Hello Scala!")
变量和常量
Scala 可以根据变量的值自动推断变量的类型
官方推荐使用 val:
- 更安全
- 代码可读性更高
- 资源回收更快,方法执行完,val 所定义的常量就会被回收
插值表达式
Scala 中可以使用插值表达式来定义字符串,能有效避免大量字符串的拼接。
scala
val|var 变量名 = s"${变量|表达式}字符串"
惰性赋值
首次使用该值时,才会被加载
scala
lazy val 变量名 = 表达式
lazy 只支持 val,不支持 var。
数据类型
- Scala 所有类型都使用大写字母开头
- 整型使用 Int 而不是 Integer
- Scala 中定义变量可以不写类型,让 Scala 编译器自动推断
- Scala 中默认的整型是 Int,默认的浮点型是 Double
类型转换
数值类型自动类型转换从小到大为:Byte,Short,Char,Int,Long,Float,Double。
强制类型转换
scala
val|var 变量名 = 值.toXxx // 例如强转为 Int 则为 toInt
键盘录入
导包: import scala.io.StdIn
调用方法:通过 StdIn.readXxx() 接收用户键盘录入的数据
语句块
Scala 中使用 {} 表示一个语句块,语句块是有返回值的(最后一个逻辑行)。
三元表达式
if 语句在使用时,要注意的事项有以下三点:
- 和 Java 一样,在 Scala 中,如果大括号 {} 内的逻辑代码只有一行,则大括号可以省略
- 在 Scala 中,条件表达式也是有返回值的
- 在 Scala 中,没有三元表达式,可以使用 if 表达式替代三元表达式
scala
// 定义变量,表示年龄
var age = 18;
// 定义常量,接收 if 语句的返回值
val result = if (age >= 18) "已成年" else "未成年"
println("age:" + age + "," + result)
循环结构
for 循环
SCALA
for (i <- 表达式|数组|集合) {
// 语句块(也叫循环体)
}
守卫
for 表达式中,可以添加 if 判断语句,这个 if 判断就称之为守卫。通过守卫可以让 for 表达式更加简洁。
SCALA
for (i <- 表达式|数组|集合 if 表达式) {
// 语句块
}
yield 生成器
yield 是一个类似 return 的关键字,但是 yield 不会结束函数,而 return 会结束函数。如果在循环结构中使用了 yield,相当于迭代一次遇到 yield 时就将 yield 后面(右边)的值放入一个集合,最后整个循环结束时将集合返回。我们把使用了 yield 的 for 表达式称之为推导式。yield不仅可以使用于 for 循环中,还可以使用于某个函数的参数,只要这个函数的参数允许被迭代.
SCALA
// 将 1~10 的偶数返回
val result = for (i <- 1 to 10 if i % 2 == 0) yield i
println(result)
// 生成 10,20,...,90,100
val result = for (i <- 1 to 10) yield i * 10
println(result)
注意:下一次迭代时,从上一次迭代遇到的 yield 后面的代码(下一行)开始执行。
break 和 continue
在 Scala 中,类似 Java 的 break 和 continue 关键字被移除了,如果一定要使用 break/continue,需要使用 scala.util.control 包下的 Breaks 类的 breakable 块和 break 方法。
- 实现 break 是用 breakable 将整个 for 表达式包起来
- 实现 continue 是用 breakable 将 for 表达式的循环体包起来
方法
在 Scala 中,方法传参默认是 val 类型,即不可变,这意味着你在方法体内部不能改变传入的参数。这和 Scala 的设计理念有关,Scala 遵循函数式编程理念,强调方法不应该有副作用。
语法格式
scala
def 方法名(参数名:参数类型, 参数名:参数类型, ...): [返回值类型] = {
// 语句块(方法体)
}
- 方法参数列表的参数类型不能省略。
- 方法返回值类型可以省略,由 Scala 编译器自动推断,但是定义递归方法,不能省略。
- 方法返回值可以不写 return,默认是 {} 块的最后一个逻辑行。
- 在 Scala 中,方法传参默认是 val 类型,即不可变,这意味着你在方法体内部不能改变传入的参数。
类型推断
在 Scala 中,定义方法时返回值类型可以省略,由 Scala 编译器自动推断。但是定义递归方法时,不能省略。否则会报错: error:recursive method accumulation needs result type 。
惰性方法
当记录方法返回值的变量被声明为 lazy 时,方法的执行将会被推迟,直到我们首次使用该值时,方法才会执行。这样的方法叫做惰性方法。
注意:lazy 只支持 val,不支持 var。
方法参数
Scala 中的方法参数比较灵活,支持以下几种类型:
- 默认参数:定义方法时可以定义参数的默认值
- 带名参数:调用方法时,可以通过参数名称来指定某个值所对应的参数
- 变长参数:如果方法的参数是不固定的,可以将该方法的参数定义为变长参数
在参数类型后面加一个 * 号,表示参数可以是 0 个或者多个
一个方法有且只能有一个变长参数,并且变长参数要放到参数列表的最后边
方法调用
后缀调用法
中缀调用法
花括号调用法
无括号调用法
方法只有一个参数时,才能使用花括号调用法。
如果方法没有参数,可以省略方法名后面的括号。
在 Scala 中,如果方法的返回值类型为 Unit 类型,这样的方法称之为过程(Procedure)。
函数
Scala 混合了面向对象特性和函数式的特性。在函数式编程中,函数是一等公民,主要体现在:
- 函数可以存储在变量中
- 函数可以作为参数
- 函数可以作为返回值
函数式编程(Functional Programming,FP)FP 是编程范式之一,常见的编程范式还有面向过程编程、面向对象编程。所谓的函数式编程指的是方法的参数列表可以接收函数对象,函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系。
定义函数
scala
// 因为函数是对象,所以函数有类型:(函数参数类型1, 函数参数类型2,...) => 函数返回值类型
val 函数名: (函数参数类型1, 函数参数类型2,...) => 函数返回值类型 = (参数名:参数类型, 参数名:参数类型, ...) => {
函数体
}
- 函数的本质就是引用类型,相当于 Java 中 new 出来的实例,所以函数是在堆内存中开辟空间;
- 函数的定义不需要使用 def 关键字,但定义的函数一定要有输入和返回值,没有返回值相当于返回的是 Unit;
- 函数不建议写 return 关键字,Scala 会使用函数体的最后一行代码作为返回值;
- 因为函数是对象,所以函数有类型,但函数类型可以省略,Scala 编译期可以自动推断类型。
方法和函数的区别
- 方法是隶属于类或者对象的,在运行时,它会被加载到 JVM 的方法区中。
- 函数是一个对象,继承自 FunctionN,函数对象有 apply,curried,toString,tupled 这些方法,方法则没有。
结论:在 Scala 中,函数是对象,而方法是属于对象的,所以可以理解为:方法归属于函数。
方法转换为函数
scala
val|var 变量名 = 方法名 _
Option
在 Scala 中,我们返回某些数据时,可以返回一个 Option 类型的对象来封装具体的数据,从而有效的避免空指针异常。
语法格式
Some(x) :表示实际的值
None :表示没有值
使用 getOrElse 方法,当值为 None 时可以指定一个默认值
面向对象
类和对象
创建类和对象
如果类是空的,没有任何成员,可以省略 {}
如果构造器的参数为空,可以省略 ()
初始化成员属性
Scala 提供了一个简洁的初始化成员变量的方式,使用 _
可以让代码看起来更加简洁、优雅。
访问权限
Scala没有 public 关键字,任何没有被标为 private 或 protected 的成员都是公共的。
Scala 中的权限修饰符只有:private、private[this]、protected、默认这四种。
构造器
当创建对象的时候,会自动调用类的构造器。
主构造器
scala
class 类名(val|var 参数名:类型 = 默认值, val|var 参数名:类型 = 默认值, ...) {
// 语句块(构造代码块)
}
- 主构造器的参数列表直接定义在类名后面,可以通过主构造器直接定义成员属性
- 构造器参数列表可以指定默认值
- 创建实例时,可以指定参数属性进行初始化
- 整个 class 中除了属性定义和方法定义的代码都是构造代码
辅助构造器
在 Scala 中,除了定义主构造器外,还可以根据需要来定义辅助构造器。我们把除了主构造器之外的构造器称为辅助构造器。
scala
def this(参数名:类型, 参数名:类型) {
// 第一行需要调用主构造器或者其他构造器
// 辅助构造器代码
}
辅助构造器的第一行代码,必须调用主构造器或者其他辅助构造器。
单例对象
cala 中是没有 static 关键字的,要想定义类似于 Java 中的 static 变量、static 方法,就要使用 Scala 中的单例对象,也就是 object。单例对象除了没有构造器外,可以拥有类的所有特性。
main 方法必须在一个单例对象中
定义单例对象
单例对象表示全局仅有一个对象,也叫孤立对象。定义单例对象和定义类很像,就是把 class 换成 object。
scala
object 单例对象名{}
在 object 中定义的成员属性类似于 Java 的静态属性,在内存中都只有一个
在单例对象中,可以直接使用 单列对象名. 的形式调用成员
伴生对象
一个 class 和 一个 object 具有相同的名字时,这个 object 就被称为伴生对象,这个 class 被称为伴生类。
- 伴生对象和伴生类必须是相同的名字;
- 伴生对象和伴生类在同一个 scala 源文件中;
- 伴生对象和伴生类可以互相访问 private 私有属性。
private[this] 访问权限
如果某个成员的权限设置为 private[this] ,则表示只能在当前类中访问,即使是伴生对象也无法访问。
apply 方法
在 Scala 中,创建对象的时候可以省去 new 关键字,这种写法可以让代码看起来更加简洁、优雅。但是,必须通过伴生对象的 apply 方法来实现。
scala
object 伴生对象名 {
def apply(参数名:参数类型, 参数名:参数类型, ...) = new 类(参数1, 参数2, ...)
}
scala
val 对象名 = 伴生对象名(参数1, 参数2, ...)
unapply 方法
unapply 方法相当于 apply 方法的逆过程,对对象进行解构,快速提取对象属性。主要作用是和 match 模式匹配一起使用。
继承
语法格式
- Scala 使用 extends 关键字来实现继承
- 可以在子类中定义父类中没有的属性和方法,或者重写父类的方法
- 类和单例对象都可以有父类
scala
class|object A类 extends B类 {
}
方法重写
- 子类要重写父类的某个方法,该方法必须要使用 override 关键字来修饰
- 父类用 var 修饰的变量,子类不能重写,子类只能重写 val 定义的属性
- 使用 super 关键字来访问父类的成员方法
类型判断
scala
// 判断对象是否为指定类型
val trueOrFalse:Boolean = 对象.isInstanceOf[类]
// 将对象转换为指定类型
val 变量 = 对象.asInstanceOf[类型]
sInstanceOf 只能判断对象是否为指定类型的继承链上的对象,而不能精确的判断出是哪个对象。如果要求精确的判断出对象的类型,只能使用 getClass 和 classOf 来实现。
抽象类
scala
// 定义抽象类
abstract class 抽象类名 {
// 定义抽象属性
val|var 抽象属性名:类型
// 定义抽象方法
def 方法名(参数:参数类型, 参数:参数类型, ...): 返回类型
}
匿名类
scala
new 类名() {
// 重写父类中所有的抽象内容
}
注意:上述格式中,如果类的主构造器参数列表为空,则小括号可以省略不写。
特质
在不影响当前继承体系的情况下,对某些类或者某些对象的功能进行增强。
特点
- 特质可以提高代码的复用性
- 特质可以提高代码的扩展性和可维护性
- 类与特质之间是继承关系,只不过类与类之间只支持单继承,但是类与特质之间可以多继承
- 特质中可以有具体的属性、抽象属性、具体的方法以及抽象方法
注意:只有抽象内容的特质被称为瘦接口,既有抽象内容又有具体内容的特质被称为富接口。
语法格式
定义特质
scala
trait 特质名称 {
// 具体属性
// 抽象属性
// 具体方法
// 抽象方法
}
继承特质
scala
class 类 extends 特质1 with 特质2 {
// 重写抽象属性
// 重写抽象方法
}
Scala 中不管是类还是特质,都使用继承 extends 关系,特质支持多继承
如果要继承多个特质,则特质之间使用 with 关键字隔开
对象混入 trait
通过使用特质的对象混入功能,在不改变类继承体系的情况下,对对象的功能进行临时增强或者扩展。
所谓的对象混入指的是:在 Scala 中,类和特质之间无任何的继承关系,但是通过特定的关键字,却可以让该类的对象具有指定特质中的成员。
scala
val|var 对象名 = new 类 with 特质
构造机制
- 每个特质只有一个无参数的构造器
- 遇到一个类继承另一个类、以及继承多个 trait 的情况,当创建1该类实例时,构造器执行顺序如下:
- 先执行父类的构造器
- 按照继承从左到右的顺序,依次执行 trait 的构造器
- 如果 trait 有父 trait,则先执行父 trait 的构造器
- 如果多个 trait 有同样的父 trait,则父 trait 的构造器只初始化一次
- 执行子类构造器
特质继承类
在 Scala 中,特质也可以继承类,继承后特质会将类中的非私有成员都继承下来。
scala
class 类A {
// 成员属性
// 成员方法
}
trait B extends A {
}
包
格式一:文件顶部标记法,合并版。
scala
package com.yjxxt.packages
// 这里写类,特质等
格式二:文件顶部标记法,分解版。
scala
package com.yjxxt
package packages
// 这里写类,特质等
格式三:串联式包语句。
scala
package com.yjxxt {
package packages {
// 这里写类,特质等
}
}
作用域
- 下层可以直接访问上层的内容
- 上层访问下层内容时,可以通过导包或者写全包名的形式访问
- 如果上下层有相同的类,使用时将采用就近原则访问。即:上下层的类重名时,优先使用下层的类,如果明确需要访问上层的类,可通过上层路径 + 类名的形式实现。
包对象
包中可以定义子包,也可以定义类或者特质,但是 Scala 中不允许直接在包中定义变量或者方法,这是因为 JVM 的局限性导致的,要想解决此问题,就需要使用包对象。
在 Scala 中,每个包都有一个包对象,包对象的名称和包名必须一致,且它们之间是平级关系,不能嵌套定义。
scala
package 包名1 {
package 包名2 {
}
// 包名 2 的包对象
package object 包名2 {
}
访问权限
在 Scala 中,我们也是可以通过访问权限修饰符来限定包中一些成员的访问权限。
scala
访问权限修饰符[包名] // 例如:private[com] var name = "张三"
导入包
在 Scala 中,导入包也是通过关键字 import 来实现,但是 Scala 中的 import 功能更加强大,更加灵活,它不再局限于编写到 Scala 文件的顶部,而是可以编写到 Scala 文件中任何你需要的地方。且 Scala 默认引入了 java.lang 包,scala 包,以及 Predef 包。
样例类
在 Scala 中,样例类是一种特殊的类,一般用于保存数据(类似 Java 的 POJO 类)
scala
case class 样例类名(val|var 成员属性1:类型1, 成员属性2:类型2, ...) {}
注意:val|var 可以不写,不写时默认为 val。
默认方法
apply() :
- 可以让我们快速使用类名来创建对象,省去了 new 关键字。
- W例如: val p = Person() 。
toString() :
- 可以让我们通过输出语句打印对象各个属性的值。
- 例如: println§ 打印的是对象的各个属性值,并非它的地址值。
equals() :
- 可以让我们直接使用 == 来比较两个样例类对象的所有成员属性值是否相等。
- 例如: p1 == p2 比较的是两个对象各个属性的值是否相等,而不是比较地址值。
hashCode() :
- 用来获取对象的哈希值。即:同一个对象(样例类属性值相同即为相同对象)的哈希值肯定是相同的。
copy() :
- 可以用来快速创建一个属性值相同的实例对象,还可以使用带名参数的形式给指定的成员重新赋值。
- 例如: val p1 = Person("张三", 23); val p2 = p1.copy(age = 24)
unapply() :
- 提取器,快速提取对象属性。W
除此之外,样例类默认还继承了 scala.Serializable 特质。
在 Scala 中,用 case 修饰的单例对象叫做样例对象。样例对象没有主构造器,它主要用在两个地方:
枚举类:为了统一项目的某些参数的值而定义的类。
没有任何参数的消息传递:目前先了解即可,后续 Akka 并发编程时再详细讲解。
scala
case object 样例对象名 {}
样例对象
在 Scala 中,用 case 修饰的单例对象叫做样例对象。样例对象没有主构造器,它主要用在两个地方:
枚举类:为了统一项目的某些参数的值而定义的类。
没有任何参数的消息传递:目前先了解即可,后续 Akka 并发编程时再详细讲解。
scala
case object 样例对象名 {}
高阶函数
Scala 混合了面向对象和函数式的特性,在函数式编程中,函数是一等公民,主要体现在:
- 函数可以存储在变量中
- 函数可以作为参数
- 函数可以作为返回值
常用的高阶函数包含:
- 函数作为参数
- 函数作为返回值
- 匿名函数(Anonymous Function)
- 回调函数(Callback Function)
- 偏应用函数(Partial Applied Function)
- 柯里化函数(Currying Function)
- 闭包函数(Closure Function)
- 递归函数(Recursive Function)
基本使用
- 函数的本质就是引用类型,相当于 Java 中 new 出来的实例,所以函数是在堆内存中开辟空间;
- 函数的定义不需要使用 def 关键字,但定义的函数一定要有输入和返回值,没有返回值相当于返回的是 Unit;
- 函数不建议写 return 关键字,Scala 会使用函数体的最后一行代码作为返回值;
- 因为函数是对象,所以函数有类型,但函数类型可以省略,Scala 编译期可以自动推断类型;
- 调用函数其实是调用函数里面的 apply 方法来执行逻辑。
至简原则*
- 方法和函数不建议写 return 关键字,Scala 会使用函数体的最后一行代码作为返回值;
- 方法的返回值类型如果能够推断出来,那么可以省略,如果有 return 则不能省略返回值类型,必须指定;
- 因为函数是对象,所以函数有类型,但函数类型可以省略,Scala 编译期可以自动推断类型;
- 如果方法明确声明了返回值为 Unit,那么即使方法体中有 return 关键字也不起作用;
- 如果方法的返回值类型为 Unit,可以省略等号 = ;
- 如果函数的参数类型如果能够推断出来,那么可以省略;
- 如果方法体或函数体只有一行代码,可以省略花括号 {} ;
- 如果方法无参,但是定义时声明了 () ,调用时小括号 () 可省可不省;
- 如果方法无参,但是定义时没有声明 () ,调用时必须省略小括号 () ;
- 如果不关心名称,只关心逻辑处理,那么函数名可以省略。也就是所谓的匿名函数;
- 如果匿名函数只有一个参数,小括号 () 和参数类型都可以省略,没有参数或参数超过一个的情况下不能省略 () ;
- 如果参数只出现一次,且方法体或函数体没有嵌套使用参数,则参数可以用下划线 _ 来替代。
函数作为参数
在 Scala 中,函数和 Int、String、Class 等其他类型处于同等的地方,可以像其他类型的变量一样被传递和操作。也就是说,一个函数的参数列表可以接收函数对象。
函数作为返回值
在 Scala 中,函数和 Int、String、Class 等其他类型处于同等的地方,可以像其他类型的变量一样被传递和操作。所以函数也可以作为返回值。
匿名函数
当函数只用一次,无需特意声明再调用,直接通过匿名函数就可以实现。
scala
// 无参匿名函数
() => {}
// 有参匿名函数
(参数1:参数类型, 参数2:参数类型, ...) => {}
x => {}
(x: Int) => {}
(x: Int, y: String) => {}
回调函数
回调函数(Callback Function)就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
偏应用函数
偏应用函数(Partial Applied Function)也叫部分应用函数
scala
val 函数名 = (参数1:参数类型, 参数2:参数类型, 参数3:参数类型, ...) => {
函数体
}
// 参数 1 已知
val 偏应用函数名 = (2, 参数2, 参数3, ...) {}
// 参数 1 和 2 已知
val 偏应用函数名 = (2, 5, 参数3, ...) {}
柯里化函数
柯里化(Currying)函数,是指将原来接受多个参数的方法转换为多个只有一个参数的参数列表的函数。即:有多个参数列表,或者说多个小括号括起来的参数列表的函数就是柯里化函数。
scala
func(x, y, z) -> func(x)(y, z) // 对第一个参数柯里化
func(x, y, z) -> func(x, y)(z) // 对最后一个参数柯里化
func(x, y, z) -> func(x)(y)(z) // 全柯里化
柯里化不会调用函数,它只是对函数进行转换
实现柯里化的方式有两种:
- 方式一:直接在参数列表上进行柯里化
- 方式二:新增参数列表(接收函数)完成柯里化
柯里化让我们能够更容易地获取偏应用函数。普通函数在被柯里化之后,当我们调用它的时候传入一个参数或两个参数时,它会返回偏应用函数。
闭包函数
闭包(Closure)是一个存在内部函数的引用关系,该引用指向的是外部函数的局部变量(前提是内部函数使用了外部函数的局部变量)。简单的理解闭包,可以分为以下几点:
- 函数嵌套函数
- 返回内部函数
- 内部函数使用外部函数环境(数据),为了保证数据不丢失,内部函数包裹了外部环境(数据)
- 有状态的函数
函数状态
无状态函数
通常我们执行一个函数,它都是无状态的
有状态函数
闭包定义
闭包即一个函数对象,即使函数对象的调用在它原始作用域之外,依然能够访问在它词法作用域内的变量。
定义阶段:我们定义了 Function 作为闭包,但是却没有真正执行它。由于我们只是定义了闭包,而没有执行,所以 num 对象是不存在的。
创建阶段:这时候,我们真正执行了 outer 闭包的内容,并返回执行结果,num 被创建出来。这时候,只要 func 不被 GC,那么
num 也会一直存在。
访问阶段:然后我们可以通过某种方式访问 outer 闭包中的内容(本例中间接访问了 num)。
集合&泛型
集合 Collection
分类
- 不可变集合:集合内的元素、长度一旦初始化完成就不可再进行更改,任何对集合的改变都将生成一个新的集合。不可变集合都在 scala.collection.immutable 这个包下,使用时无需手动导包。
- 可变集合:指的是这个集合本身可以动态改变,且可变集合提供了改变集合内元素的方法。可变集合都在scala.collection.mutable 这个包下,使用时需要手动导包。
继承树
所有集合的抽象特质:
特质不可变的实现类(蓝色为抽象类,黑色为具体实现类):
集合库中所有类的图示(蓝色为接口,黑色为具体实现类):
可变集合比不可变集合功能更加丰富,例如:Seq 集合中增加了 Buffer(ArrayBuffer、ListBuffer) 可变集合
当我们接触一个新的继承体系时,建议采用学顶层,用底层的方式:
- 顶层定义了整个继承体系所有共性的内容。相似类型的集合拥有同样的一组方法。
- 底层是具体实现(编码)以及实现类独有的方法(非共性)。
上层接口
Traversable
Traversable 是一个特质(trait),它是其他集合的父特质,它的子特质 immutable.Traversable 和 mutable.Traversable 分别是不可变集可变集合的父特质,集合中大部分通用的方法都是在这个特质中定义的。
语法格式
格式一:创建空的 Traversable 对象
scala
// 方式一:通过 empty 方法实现
val t1 = Traversable.empty[Int]
// 方式二:通过小括号方式实现
val t2 = Traversable[Int]()
// 方式三:通过 Nil 实现
val t3 = Nil
格式二:创建带元素的 Traversable 对象
scala
// 方式一:通过 toTraversable() 方式实现
val t4 = List(1, 2, 3).toTraversable
val t5 = Array(1, 2, 3).toTraversable
val t6 = Set(1, 2, 3).toTraversable
// 方式二:通过 Traversable 的伴生对象的 apply() 方法实现
val t7 = Traversable(1, 2, 3)
转置集合:transpose()
方法
拼接集合:每 ++
一次,就会创建一个新的临时集合,concat()
计算阶乘:scan()
获取元素:
判断元素:
forall()
:如果集合中所有元素都满足指定的条件则返回 true,否则返回 falseexists()
:只要集合中任意一个元素满足指定的条件就返回 true,否则返回 false
聚合操作:
Iterable
Iterable 代表一个可以迭代的集合,它继承了 Traversable 特质,同时也是其他集合的父特质。最重要的是,它定义了获取迭代器 iterator 的方法: def iterator: Iterator[A] ,这是一个抽象方法,它的实现类需要实现这个方法,从而实现迭代集合并返回集合中的元素。
Traversable 提供了两种遍历数据的方式:
- 通过 iterator() 方法实现,迭代访问元素。这种方式属于主动迭代,可以通过 hasNext() 检查是否还有元素,并且可以主动的调用 next() 方法获取元素。即:我们可以自主控制迭代过程。
- 通过 foreach() 方法实现,遍历访问元素。这种方式属于被动迭代,只需要提供一个函数,并不能控制遍历的过程。即:迭代过程是由集合本身控制的。
分组遍历
如果遇到将 Iterable 对象中的元素分成固定大小的组,然后再遍历的需求,可以通过 grouped() 方法来实现。
scala
def grouped(size: Int): Iterator[Ierable[A]]
按索引生成元组
Iterable 集合中存储的每个元素都是有索引的,如果想按照 元素 -> 索引 这种格式生成一个新的集合,可以使用 zipWithIndex() 方法。
判断集合是否相同
sameElements() 方法:判断两个集合中的元素是否全部相同,而且还要求这两个集合元素的迭代顺序保持一致
Seq
Seq(Sequence)特质代表按照一定顺序排列的元素序列,序列是一种特别的可迭代集合,它的元素特点是有序(元素存取顺序一致),可重复,有索引
IndexedSeq 和 LinearSeq 是 Seq 的子特质
IndexedSeq
IndexedSeq 代表索引序列,相对于 Seq 来说,它并没有增加额外的方法,对于随机访问元素,它更加有效,常用的子集合有:NumericRange、Range、Vector、String 等。
- NumericRange :通用的等差队列,可以生成 Int、BigInteger、Short、Byte 等类型的序列。
- Range :有序的整数队列,每个元素之间的间隔相同,例如奇数队列:1, 3, 5, 7, 9。
- Vector :通用的不可变的数据结构,相对来讲它获取数据的时间会稍长一些,但是随机更新数据要比数组和链表快许多。
LinearSeq
LinearSeq 代表线性序列,它通过链表的方式实现,因此它的 head、tail、isEmpty 执行起来相对更高效,常用的子集合有:List、Stack、Queue 等。
-
Stack :表示栈数据结构,元素特点是先进后出。由于历史原因,Scala 当前的库中还包含一个。
immutable.Stack ,但当前已经被标记为弃用,因为它的设计不怎么优雅,而且性能也不太好,因为栈会涉及到大量元素的进出,所以不可变栈的应用场景还是比较少,最常用的还是可变栈。
- mutable.Stack :通过 List 链表的方式实现,增删快,查询慢。
- mutable.ArrayStack :通过 Array 数组的方式实现,查询块,增删慢。
-
Queue :表示队列数据结构,元素特点是先进先出。
获取元素索引
上述方法都是查找到指定数据后则返回对应的索引,如果找不到则返回 -1。
判断集合是否包含指定元素
修改元素
updated
:修改指定索引位置的元素
patch
:修改指定区间的元素
Set
Set 集合是没有重复元素的集合,所有的元素都是唯一的。
- HashSet:HashSet 是基于 HashMap 实现的,对 HashMap 做了一层简单的封装,而且只使用了 HashMap 的 Key 来实现各种特性。元素特点唯一、无序。
- ListSet:元素特点唯一、有序(元素添加的顺序)。
- TreeSet:元素特点唯一、排序(按自然顺序排序)。
Map
Map 表示映射,是以键值对的方式存储数据(key-value),键不能重复,但是值可以有重复。根据键得到值,对 Map集合遍历时先得到键的 Set 集合,再对 Set 集合进行遍历,得到相应的值。
集合遍历时先得到键的 Set 集合,再对 Set 集合进行遍历,得到相应的值。常用子类有以下几种:
- HashMap:元素特点 Key 唯一、无序。
- ListMap:元素特点 Key 唯一、有序(元素添加的顺序)。
- TreeMap:元素特点 Key 唯一、排序(按自然顺序排序)。
下层实现
数组 Array
数组就是用来存储多个元素(类型可以不同,使用时一般存储相同类型的数据)的容器,每个元素都有编号,被称为下标或者索引,编号从 0 开始。
不可变数组
数组的长度不允许改变,数组的内容可以改变。
格式一:通过指定长度定义数组。
SCALA
val|var 数组名 = new Array[元素类型](数组长度)
格式二:通过指定元素定义数组。
SCALA
val|var 数组名 = Array(元素1, 元素2, 元素3, ...)`
- 在 Scala 中,数组的泛型使用 [] 来指定。
- 使用 数组名(索引) 来获取数组中的元素。
- 数组元素拥有默认值,例如:Int:0,Double:0.0,String:null
- 通过 数组名.length 或者 数组名.size 来获取数组的长度
可变数组
数组的长度和内容都是可变的,可以往数组中添加、删除元素。
创建变长数组,需要先导入 import scala.collection.mutable.ArrayBuffer
。
格式一:创建空的 ArrayBuffer 变长数组。
SCALA
val|var 变量名 = ArrayBuffer[元素类型]()
格式二:创建带有初始元素的 ArrayBuffer 变长数组
val|var 变量名 = ArrayBuffer(元素1, 元素2, 元素3, ...)
增删改
- += :添加单个元素
- -= :删除单个元素
- ++= :追加多个元素到变长数组中
- --= :移除变长数组中的多个元素
- 数组名(索引) = 新值 或者 数组名.update(索引, 新值) :修改元素
遍历数组
- 使用索引遍历数组中的元素
- 使用 for 表达式直接遍历数组中的元素
常用方法
- sum() :求和。
- max() :求最大。
- min() :求最小。
- sorted() :排序(正序),返回一个新的数组。倒序可以先排序再反转。
- reverse() :反转,返回一个新的数组。
元组 Tuple
元组一般用来存储多个不同类型的值,例如同时存储姓名、年龄、性别、出生年月等数据,就要用到元组来存储了。元组的长度和元素都是不可变的。
语法格式
格式一:通过小括号实现。
SCALA
val|var 元组名 = (元素1, 元素2, 元素3, ...)
格式二:通过箭头来实现
SCALA
val|var 元组名 = 元素1 -> 元素2
格式二只适用于元组中只有两个元素的情况。
访问元素
在 Scala 中可以通过 元组名._编号
的形式来访问元组中的元素, _1
表示访问第一个元素,依次类推。也可以通过元组名.productIterator
的方式来获取该元素的迭代器,从而实现遍历元组。
格式一:访问元组中的单个元素。
scala
元组名._1 // 第一个元素
元组名._2 // 第二个元素
元组名._N // 第N个元素
格式二:遍历元组。
scala
val tuple = (元素1, 元素2, 元素3, ...)
val iterator = tuple.productIterator
for (elem <- iterator) println(elem)
列表 List
列表 List 是 Scala 中最重要也是最常用的一种数据结构。它的特点是:有序,可重复
有序的意思并不是会自动排序,而且值元素的插入顺序和取出顺序是一致的。可重复表示列表中可以添加相同的元素多次
不可变列表
不可变列表指的是:列表的元素、长度都是不可变的。
语法格式
格式一:通过 List() 直接初始化。
scala
val|var 列表名 = List(元素1, 元素2, 元素3, ...)
格式二:通过 Nil 创建一个空列表。
scala
val|var 列表名 = Nil
格式三:通过 :: 创建列表。
scala
val|var 列表名 = 元素1 :: 元素2 :: 元素n :: Nil
使用 :: 方式创建的列表,必须在最后添加一个 Nil 。
可变列表
可变列表指的是列表的元素、长度都是可变的。
语法格式
创建可变列表,需要先导入 import scala.collection.mutable.ListBuffer
格式一:创建空的可变列表。
scala
val|var 列表名 = ListBuffer[数据类型]()
格式二:通过 ListBuffer() 直接初始化。
scala
val|var 列表名 = ListBuffer(元素1, 元素2, 元素3, ...)
可变列表的常见操作
基本上容器都有 toXxx 方法,方便容器之间进行转换。元组需要先获取到迭代器再转换。
列表常见操作
扁平化
扁平化是指将嵌套列表中的每个元素单独放到一个新的列表中。
如果某个列表中的所有元素都是列表,那么这样的列表就被称为:嵌套列表。
拉链与拉开
- 拉链:
将两个列表合并成一个列表,列表的元素为元组。无法组成拉链的单个元素会被丢弃。
例如:将 List("张三", "李四") , List(18, 19) 合并成列表 List((张三, 18), ("李四", 19)) 。 - 拉开:
将一个列表拆分成两个列表,两个列表被元组包含
例如:将 List(("张三", 18), ("李四", 19)) 拆分成元组 (List("张三", "李四")), List(18, 19)) 。
转换字符串
列表转换成其对应的字符串形式,可以通过 toString 方法或者 mkString 方法实现
- toString :返回 List 中的所有元素的字符串
- mkString :可以将元素以指定的分隔符拼接起来并返回,默认没有分隔符
并集/交集/差集
-
union :
表示对两个列表进行并集(合并)操作,且不去重
例如: list1.union(list2) ,表示获取 list1 和 list2 中所有的元素(元素不去重)
-
intersect :
表示对两个列表取交集(相同)
例如: list1.intersect(list2) ,表示获取 list1,list2 中都有的元素
-
diff :
表示对两个列表取差集(不同)
例如: list1.diff(list2) ,表示获取 list1 中有,但是 list2 中没有的元素
集 Set
集 Set 也是 Scala 中常用的一种数据结构。它的特别是:唯一,自动去重。
无序的意思是插入顺序和取出顺序是不一致的。唯一表示集中的元素具有唯一性不可重复。
不可变集
不可变集指的是:集的元素、长度都是不可变的。
语法格式
格式一:创建一个空的不可变集。
scala
val|var 集名 = Set[类型]()
格式二:给定元素来创建一个不可变集。
scala
val|var 集名 = Set(元素1, 元素2, 元素3, ...)
可变集
可变集指的是集的元素、长度都是可变的。
语法格式
创建方式与不可变集一致,只不过创建可变集,需要先导入 import scala.collection.mutable.Set
。
格式一:创建一个空的不可变集。
scala
val|var 集名 = mutable.Set[类型]()
格式二:给定元素来创建一个不可变集。
scala
val|var 集名 = mutable.Set(元素1, 元素2, 元素3, ...)
常见操作
映射 Map
映射指的就是 Map,它是由键值对(Key Value)组成的集合。特点是:键具有唯一性,值可以重复。
如果添加相同键元素,则后者的值会覆盖前者的值。
不可变 Map
不可变 Map 指的是:Map 的元素、长度都是不可变的。
语法格式
格式一:通过箭头的方式实现。也就意味着 Map 内部是一个个的二元组。
scala
val|var map = Map(键1 -> 值1, 键2 -> 值2, 键3 -> 值3, ...)
格式二:通过小括号(元组)的方式实现。
scala
val|var 集名 = Map((键1, 值1), (键2, 值2), (键3, 值3), ...)
可变 Map
可变 Map 指的是集的元素、长度都是可变的。
语法格式
创建方式与不可变 Map 一致,只不过创建可变 Map,需要先导入 import scala.collection.mutable.Map
。
格式一:通过箭头的方式实现。
scala
val|var map = mutable.Map(键1 -> 值1, 键2 -> 值2, 键3 -> 值3, ...)
格式一:通过箭头的方式实现。
scala
val|var 集名 = mutable.Map((键1, 值1), (键2, 值2), (键3, 值3), ...)
常见操作
迭代器 Iterator
Scala 针对每一类集合都提供了一个迭代器,用来迭代访问集合。
使用 iterator 方法可以从集合获取一个该集合的迭代器,迭代器中有两个方法:
- hasNext :查询容器中是否有下一个元素
- next :返回迭代器的下一个元素,如果没有则抛出 java.util.NoSuchElementException: next on empty iterator 异常
范围 Range
Range 属于区间类型,本质上就是一种特殊的 Array。
to:左闭右闭(左右都包含)
until:左闭右开(左边包含右边不包含),以下两行代码效果一致
by:设置步长,让一个数值在每次运算中加上某个数(此即步长)重复执行此项运算
栈 Stack
不可变栈的应用场景还是比较少,最常用的还是可变栈。
- mutable.Stack :通过 List 链表的方式实现,增删快,查询慢。
- mutable.ArrayStack :通过 Array 数组的方式实现,查询块,增删慢。
常用方法
队列 Queue
Queue 表示队列数据结构,元素特点是先进先出
常用方法
函数式编程
集合常用函数如下:
简化函数定义
- 方式一:通过 类型推断 来简化函数定义。因为集合中每个元素类型都是已经确定的,所以可以让 Scala 程序来自动推断集合每个元素参数的类型。即:创建函数时,可以省略其参数列表的类型。
- 方式二:通过下划线 _ 来简化函数定义。当函数参数只在函数体中出现一次时,且函数体没有嵌套调用,可以使用_ 下划线来简化函数的定义。
foreach
scala
def foreach(f:(A) => Unit): Unit
// 简写形式
def foreach(函数)
去重 distinct
除了使用 Set 集对元素进行去重外,还可以使用 distinct 对元素进行去重。将数据集中重复的数据去重。其实distinct 底层就是用了 HashSet。
映射 map
scala
def map[B](f: (A) => B): TraversableOnce[B]
// 简写形式
def map(函数对象)
扁平化映射 flatMap
扁平化映射可以理解为先 map,然后再 flatten。
首先将函数作用于集合中的每个元素,然后将结果展平,返回新的集合。
scala
def flatMap[B](f: (A) => GenTraversableOnce[B]): TraversableOnce[B]
// 简写形式
def flatMap(f: (A) => 待转换的集合的处理代码)
过滤 filter
scala
def filter(f: (A) => Boolean): TraversableOnce[A]
排序
-
sorted :按默认规则(升序)排序集合,如果需要降序,升序后 reverse 反转即可。
-
sortBy :按指定属性排序集合,对集合元素根据传入的函数转换后,再进行排序。
scaladef sortBy[B](f: (A) => B): List[A] // 简写形式 def sortBy(函数对象)
-
sortWith :按自定义规则排序集合,根据一个自定义的函数(规则)来进行排序。
scaladef sortWith(f: (A, A) => Boolean): List[A] // 简写形式 def sortWith(函数对象,表示自定义的比较规则)
分组 groupBy
scala
def groupBy[K](f: (A) => K): Map[K, List[A]]
// 简写形式
def groupBy(f: (A) => 具体的分组代码)
聚合
聚合操作是指将一个集合中的数据合并为一个值
-
reduce :对集合元素进行聚合计算(将集合传入一个函数进行聚合计算),reduce 又分为:
reduce:从左到右计算
reduceLeft:从左到右计算
reduceRight:从右到左计算
scaladef reduce[A1 >: A](op: (A1, A1) => A1): A1 // 简写形式 def reduce(op:(A1, A1) => A1)
-
fold :对集合元素进行折叠计算,fold 又分为:
fold:从左往右计算
foldLeft:从左往右计算
foldRight:从右到左计算
scaladef fold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1 // 简写形式 def fold(初始值)(op:(A1, A1) => A1)
泛型 Generics
泛型的意思是泛指某种具体的数据类型,在 Scala 中,泛型用 [数据类型]
表示。
- 泛型集合:定义集合指定泛型,只能存储指定类型数据
- 泛型方法:把泛型定义到方法声明上
- 泛型类:把泛型定义到类的声明上
- 泛型特质:把泛型定义到特质的声明上
泛型 T 可以是任意字母,一般使用 T 来定义,表示 type 类型的意思。
上界
使用 T <: 类型
表示给类型添加一个上界,表示泛型参数必须是继承自该类型或是其类型本身。
例如: [T <: Animal]
表示泛型 T 的数据类型必须是 Animal 类型或者 Animal 的子类型。
下界
使用 T >: 类型
表示给类型添加一个下界,表示泛型参数必须是该类型的祖先类或是其类型本身。
例如: [T >: Person]
表示泛型 T 的数据类型必须是 Person 类型或者 Person 的祖先类型。
非变、协变、逆变
- 非变:A 类和 B 类之间存在父子关系,泛型非变操作后两个泛型类之间不会产生任何关系。
- 协变:A 类和 B 类之间存在父子关系,泛型协变操作后两个泛型类之间也属于父子关系。
- 逆变:A 类和 B 类之间存在父子关系,泛型逆变操作后两个泛型类之间关系进行颠倒。
- 非变: Temp[T]{} 。泛型默认即是非变的。
- 协变: Temp[+T]{} 。
- 逆变: Temp[-T]{} 。
模式匹配
简单模式匹配
一个模式匹配包含了一系列备选项,每个备选项都开始于关键字 case
。且每个备选项都包含了一个模式及一到多个表达式。箭头符号 =>
隔开了模式和表达式。
scala
变量 match {
case "常量1" => 表达式1
case "常量2" => 表达式2
case "常量n" => 表达式n
case _ => 表达式n+1
}
匹配类型
match 表达式还可以进行类型匹配。如果我们要根据不同的数据类型,来执行不同的逻辑,也可以使用 match 表达式来实现。
scala
对象名 match {
case 变量名1:类型1 => 表达式1
case 变量名2:类型2 => 表达式2
case 变量名n:类型n => 表达式n
case _ => 表达式n+1
}
如果 case 表达式中不会使用匹配时的变量,可以使用下划线来代替。
守卫
所谓的守卫是指在 case 语句中添加 if 条件判断
scala
变量 match {
case 变量名 if条件 => 表达式
...
case _ -> 表达式
}
匹配样例类
Scala 中还可以使用模式匹配来匹配样例类,从而实现快速获取样例类中的成员。
scala
对象名 match {
case 样例类型1(属性1, 属性2, 属性n) => 表达式1
case 样例类型2(属性1, 属性2, 属性n) => 表达式2
case 样例类型n(属性1, 属性2, 属性n) => 表达式n
case _ => 表达式n+1
}
- case 样例类后的小括号中的字段个数,要和具体的样例类的字段个数保持一致。
- 通过 match 进行模式匹配的时候,要匹配的对象必须声明为 Any 类型。
匹配集合*
Scala 的模式匹配还可以用来匹配数组、列表、元组、集、映射等。
match 中不支持直接匹配集,可以通过转为数组或列表的方式进行匹配。
由于 Map 中的元素就是一个一个的二元组,所以在遍历时,可以使用元组匹配。