Scala第十六章节
章节目标
- 掌握泛型方法, 类, 特质的用法
- 了解泛型上下界相关内容
- 了解协变, 逆变, 非变的用法
- 掌握列表去重排序案例
1. 泛型
泛型的意思是泛指某种具体的数据类型
, 在Scala中, 泛型用[数据类型]
表示. 在实际开发中, 泛型一般是结合数组或者集合来使用的, 除此之外, 泛型的常见用法还有以下三种:
- 泛型方法
- 泛型类
- 泛型特质
1.1 泛型方法
泛型方法指的是把泛型定义到方法声明上, 即:该方法的参数类型是由泛型来决定的
. 在调用方法时, 明确具体的数据类型.
格式
def 方法名[泛型名称](..) = {
//...
}
需求
定义方法getMiddleElement(), 用来获取任意类型数组的中间元素.
- 思路一: 不考虑泛型直接实现(基于Array[Int]实现)
- 思路二: 加入泛型支持.
参考代码
//案例: 泛型方法演示.
//细节: 泛型方法在调用方法的时候 明确具体的数据类型.
object ClassDemo01 {
//需求: 用一个方法来获取任意类型数组的中间的元素
//思路一:不考虑泛型直接实现(基于Array[Int]实现)
//def getMiddleElement(arr: Array[Int]) = arr(arr.length / 2)
//思路二: 加入泛型支持
def getMiddleElement[T](arr: Array[T]) = arr(arr.length / 2)
def main(args: Array[String]): Unit = {
//调用方法
println(getMiddleElement(Array(1, 2, 3, 4, 5)))
println(getMiddleElement(Array("a", "b", "c")))
}
}
1.2 泛型类
泛型类指的是把泛型定义到类的声明上, 即:该类中的成员的参数类型是由泛型来决定的
. 在创建对象时, 明确具体的数据类型.
格式
class 类[T](val 变量名: T)
需求
- 定义一个Pair泛型类, 该类包含两个字段,且两个字段的类型不固定.
- 创建不同类型的Pair泛型类对象,并打印.
参考代码
//案例: 泛型-演示泛型类的使用.
//泛型类: 在创建对象的时候, 明确具体的数据类型.
object ClassDemo02 {
//1. 实现一个Pair泛型类
//2. Pair类包含两个字段,而且两个字段的类型不固定
class Pair[T](var a:T, var b:T)
def main(args: Array[String]): Unit = {
//3. 创建不同类型泛型类对象,并打印
var p1 = new Pair[Int](10, 20)
println(p1.a, p1.b)
var p2 = new Pair[String]("abc", "bcd")
println(p2.a, p2.b)
}
}
1.3 泛型特质
泛型特质指的是把泛型定义到特质的声明上, 即:该特质中的成员的参数类型是由泛型来决定的
. 在定义泛型特质的子类或者子单例对象时, 明确具体的数据类型.
格式
trait 特质A[T] {
//特质中的成员
}
class 类B extends 特质A[指定具体的数据类型] {
//类中的成员
}
需求
- 定义泛型特质Logger, 该类有一个变量a和show()方法, 它们都是用Logger特质的泛型.
- 定义单例对象ConsoleLogger, 继承Logger特质.
- 打印单例对象ConsoleLogger中的成员.
参考代码
//案例: 演示泛型特质.
object ClassDemo03 {
//1. 定义泛型特质Logger, 该类有一个a变量和show()方法, 都是用Logger特质的泛型.
trait Logger[T] {
//定义变量
val a:T
//定义方法.
def show(b:T) = println(b)
}
//2. 定义单例对象ConsoleLogger, 继承Logger特质.
object ConsoleLogger extends Logger[String]{
override val a: String = "张三"
}
//main方法, 作为程序的主入口.
def main(args: Array[String]): Unit = {
//3. 打印单例对象ConsoleLogger中的成员.
println(ConsoleLogger.a)
ConsoleLogger.show("10")
}
}
2. 上下界
我们在使用泛型(方法, 类, 特质)时,如果要限定该泛型必须从哪个类继承、或者必须是哪个类的父类。此时,就需要使用到泛型的上下界
。
2.1 上界
使用T <: 类型名
表示给类型添加一个上界,表示泛型参数必须要从该类(或本身)继承.
格式
[T <: 类型]
例如: [T <: Person]的意思是, 泛型T的数据类型必须是Person类型或者Person的子类型
需求
- 定义一个Person类
- 定义一个Student类,继承Person类
- 定义一个泛型方法demo(),该方法接收一个Array参数.
- 限定demo方法的Array元素类型只能是Person或者Person的子类
- 测试调用demo()方法,传入不同元素类型的Array
参考代码
//案例: 演示泛型的上下界之 上界.
object ClassDemo04 {
//1. 定义一个Person类
class Person
//2. 定义一个Student类,继承Person类
class Student extends Person
//3. 定义一个demo泛型方法,该方法接收一个Array参数,
//限定demo方法的Array元素类型只能是Person或者Person的子类
def demo[T <: Person](arr: Array[T]) = println(arr)
def main(args: Array[String]): Unit = {
//4. 测试调用demo,传入不同元素类型的Array
//demo(Array(1, 2, 3)) //这个会报错, 因为只能传入Person或者它的子类型.
demo(Array(new Person()))
demo(Array(new Student()))
}
}
2.2 下界
使用T >: 数据类型
表示给类型添加一个下界,表示泛型参数必须是从该类型本身或该类型的父类型.
格式
[T >: 类型]
注意:
- 例如: [T >: Person]的意思是, 泛型T的数据类型必须是Person类型或者Person的父类型
- 如果泛型既有上界、又有下界。下界写在前面,上界写在后面. 即: [T >: 类型1 <: 类型2]
需求
- 定义一个Person类
- 定义一个Policeman类,继承Person类
- 定义一个Superman类,继承Policeman类
- 定义一个demo泛型方法,该方法接收一个Array参数,
- 限定demo方法的Array元素类型只能是Person、Policeman
- 测试调用demo,传入不同元素类型的Array
参考代码
//案例: 演示泛型的上下界之 下界.
//如果你在设定泛型的时候, 涉及到既有上界, 又有下界, 一定是: 下界在前, 上界在后.
object ClassDemo05 {
//1. 定义一个Person类
class Person
//2. 定义一个Policeman类,继承Person类
class Policeman extends Person
//3. 定义一个Superman类,继承Policeman类
class Superman extends Policeman
//4. 定义一个demo泛型方法,该方法接收一个Array参数,
//限定demo方法的Array元素类型只能是Person、Policeman
// 下界 上界
def demo[T >: Policeman <: Policeman](arr: Array[T]) = println(arr)
def main(args: Array[String]): Unit = {
//5. 测试调用demo,传入不同元素类型的Array
//demo(Array(new Person))
demo(Array(new Policeman))
//demo(Array(new Superman)) //会报错, 因为只能传入: Policeman类获取它的父类型, 而Superman是Policeman的子类型, 所以不行.
}
}
3. 协变、逆变、非变
在Spark的源代码中大量使用到了协变、逆变、非变,学习该知识点对我们将来阅读spark源代码很有帮助。
- 非变: 类A和类B之间是父子类关系, 但是Pair[A]和Pair[B]之间没有
任何关系
. - 协变: 类A和类B之间是父子类关系, Pair[A]和Pair[B]之间也有
父子类
关系. - 逆变: 类A和类B之间是父子类关系, 但是Pair[A]和Pair[B]之间是
子父类
关系.
如下图:
3.1 非变
语法格式
class Pair[T]{
}
- 默认泛型类是
非变的
- 即: 类型B是A的子类型,Pair[A]和Pair[B]没有任何从属关系
3.2 协变
语法格式
class Pair[+T]
- 类型B是A的子类型,Pair[B]可以认为是Pair[A]的子类型
- 参数化类型的方向和类型的方向是一致的。
3.3 逆变
语法格式
class Pair[-T]
- 类型B是A的子类型,Pair[A]反过来可以认为是Pair[B]的子类型
- 参数化类型的方向和类型的方向是相反的
3.4 示例
需求
- 定义一个Super类、以及一个Sub类继承自Super类
- 使用协变、逆变、非变分别定义三个泛型类
- 分别创建泛型类对象来演示协变、逆变、非变
参考代码
//案例: 演示非变, 协变, 逆变.
object ClassDemo06 {
//1. 定义一个Super类、以及一个Sub类继承自Super类
class Super //父类
class Sub extends Super //子类
//2. 使用协变、逆变、非变分别定义三个泛型类
class Temp1[T] //非变
class Temp2[+T] //协变
class Temp3[-T] //逆变.
def main(args: Array[String]): Unit = {
//3. 分别创建泛型类来演示协变、逆变、非变
//演示非变.
val t1:Temp1[Sub] = new Temp1[Sub]
//val t2:Temp1[Super] = t1 //编译报错, 因为非变是: Super和Sub有父子类关系, 但是Temp1[Super] 和 Temp1[Sub]之间没有关系.
//演示协变
val t3:Temp2[Sub] = new Temp2[Sub]
val t4:Temp2[Super] = t3 //不报错, 因为协变是: Super和Sub有父子类关系, 所以Temp2[Super] 和 Temp2[Sub]之间也有父子关系.
//Temp2[Super]是父类型, Temp2[Sub]是子类型.
//演示逆变
val t5:Temp3[Super] = new Temp3[Super]
val t6:Temp3[Sub] = t5 //不报错, 因为逆变是: Super和Sub有父子类关系, 所以Temp3[Super] 和 Temp3[Sub]之间也有子父关系.
//Temp3[Super]是子类型, Temp3[Sub]是父类型.
}
}
4. 案例: 列表去重排序
4.1 需求
-
已知当前项目下的data文件夹中有一个1.txt文本文件, 文件内容如下:
11 6 5 3 22 9 3 11 5 1 2
-
对上述数据去重排序后, 重新写入到data文件夹下的2.txt文本文件中, 即内容如下:
1 2 3 5 6 9 11 22
4.2 目的
考察泛型, 列表, 流
相关的内容.
4.3 参考代码
import java.io.{
BufferedWriter, FileWriter}
import scala.io.Source
//案例: 列表去重排序, 并写入文件.
object ClassDemo07 {
def main(args: Array[String]): Unit = {
//1. 定义数据源对象.
val source = Source.fromFile("./data/1.txt")
//2. 从指定文件中读取所有的数据(字符串形式)
val list1:List[String] = source.mkString.split("\\s+").toList
//3. 把List[String]列表转换成List[Int]
val list2:List[Int] = list1.map(_.toInt)
//4. 把List[Int]转换成Set[Int], 对列表元素去重.
val set:Set[Int] = list2.toSet
//5. 把Set[Int]转成List[Int], 然后升序排列
val list3:List[Int] = set.toList.sorted
//println(list3)
//6. 把数据重新写入到data文件夹下的2.txt文件中.
val bw = new BufferedWriter(new FileWriter("./data/2.txt"))
for(i <- list3) {
bw.write(i.toString)
bw.newLine() //别忘记加换行
}
//7. 释放资源
bw.close()
}
}
Scala第十七章节
章节目标
- 了解集合的相关概念
- 掌握Traversable集合的用法
- 掌握随机学生序列案例
1. 集合
1.1 概述
但凡了解过编程的人都知道程序 = 算法 + 数据结构
这句话, 它是由著名的瑞士计算机科学家尼古拉斯·沃斯
提出来的, 而他也是1984年图灵奖的获得者. 算法指的是计算的一系列有效, 通用的步骤. 算法和数据结构是程序设计中相辅相成的两个方面, 因此数据结构也是编程中很重要的一个方面. 很多编程语言都提供了数据结构的对应编程库,
并称之为集合库(Collection Library). Scala中也有集合库, 它的优点如下:
-
易于使用
使用集合库提供的20到50个左右的方法, 灵活的组合运用, 就可以解决大部分的集合问题.
-
简洁
简单的一个单词(例如: foreach), 就可以实现一个或者多个循环操作.
-
安全
绝大部分错误都可以在编译期被发现.
-
快速
集合类型的方法实现时, 都进行了调优, 用户可以根据需求选择合适的集合.
-
统一
Scala的集合有非常严谨的继承体系, 相似类型的集合拥有同样的一组方法, 以及属于自己独有的方法.
1.2 分类
Scala同时支持不可变集合和可变集合
, 因为不可变集合可以安全的并发访问, 所以它也是默认使用的集合类库. 在Scala中, 对于几乎所有的集合类, 都提供了可变和不可变两个版本, 具体如下:
-
不可变集合: 指的是
集合内的元素一旦初始化完成就不可再进行更改, 任何对集合的改变都将生成一个新的集合.
都在
scala.collection.immutable
这个包下, 使用时无需手动导包. -
可变集合: 指的是
这个集合本身可以动态变化, 且可变集合提供了改变集合内元素的方法
都在
scala.collection.mutable
这个包下, 使用是需要手动导包.
如下图:
小技巧:
可变集合比不可变集合更加丰富.
例如: 在Seq集合中, 增加了Buffer集合, 我们常用的有: ArrayBuffer和ListBuffer.
当我们接触一个新的继承体系是, 建议采用
学顶层, 用底层
的方式.
顶层定义的是整个继承体系所共有的内容.
而底层才是具体的体现, 实现.
2. Traversable
2.1 概述
Traversable是一个特质(trait), 它是其他集合的父特质, 它的子特质immutable.Traversable和mutable.Traversable分别是不可变集合和可变集合的父特质, 集合中大部分通用的方法都是在这个特质中定义的. 因此了解它的功能对学习其他集合类至关重要.
2.2 格式
-
格式一: 创建空的Traversable对象.
//方式一: 通过empty方法实现. val t1 = Traversable.empty[Int] //方式二: 通过小括号方式实现 val t2 = Traversable[Int]() //方式三: 通过Nil实现. val t3 = Nil
-
格式二: 创建带参数的Traversable对象.
//方式一: 通过toTraversable()方法实现 val t1 = List(1, 2, 3).toTraversable //方式二: 通过Traversable的伴生对象的apply()方法实现. val t1 = Traversable(1, 2, 3)
2.3 示例一: 创建Traversable对象
- 创建空的, 用来存储Int类型数据的Traversable对象.
- 创建Traversable集合对象, 存储数字1, 2, 3, 并将结果打印到控制台上.
参考代码
//案例: 演示创建Traversable对象.
object ClassDemo01 {
def main(args: Array[String]): Unit = {
//1. 创建空的, 用来存储Int类型数据的Traversable对象.
//1.1 创建对象.
val t1: Traversable[Int] = Traversable.empty[Int]
val t2: Traversable[Int] = Traversable[Int]()
val t3: Traversable[Int] = Nil
//1.2 比较它们是否相等.
println(t1 == t2) //==比较的是集合中的数据.
println(t1 == t3)
println(t2 == t3)
println(t1 eq t2) //eq比较的是集合的地址值.
println(t1 eq t3)
println(t2 eq t3)
println("-" * 15)
//2. 创建Traversable集合对象, 存储数字1, 2, 3, 并将结果打印到控制台上.
//2.1 通过toTraversable()方法实现.
val t4: Traversable[Int] = List(1, 2, 3).toTraversable
val t5: Traversable[Int] = Set(1, 2, 3).toTraversable
//2. 通过Traversable的伴生对象的apply()方法实现.
val t6:Traversable[Int] = Traversable(11, 22, 33, 44, 55)
//3. 打印结果(因为Traversable是特质, 所以底层还是通过它的具体子类来实现的).
println(s"t4: ${t4}")
println(s"t5: ${t5}")
println(s"t6: ${t6}")
}
}
2.4 案例二: 转置Traversable集合
了解过线性代数的同学都知道, 矩阵有一个转置的操作, 在Scala中, 可以通过transpose()方法
来实现类似的操作.
如下图:
注意:
进行转置操作时, 程序会自动检测每个集合中的元素个数是否一致, 如果一致, 则转置成功. 如果不一致, 则报错.
需求
- 定义一个Traversable集合t1, 它有三个元素, 每个元素都是Traversable集合, 并分别存储如下数据:
- 第一个元素存储(1, 4, 7), 第二个元素存储(2, 5, 8), 第三个元素存储(3, 6, 9).
- 通过transpose方法, 对集合t1进行转置操作.
- 打印结果.
参考代码
//案例: 演示转置集合.
object ClassDemo02 {
def main(args: Array[String]): Unit = {
//1. 定义一个Traversable集合t1, 它有三个元素, 每个元素都是Traversable集合, 并分别存储如下数据:
//2. 第一个元素存储(1, 4, 7), 第二个元素存储(2, 5, 8), 第三个元素存储(3, 6, 9).
val t1 = Traversable(Traversable(1, 4, 7), Traversable(2, 5, 8), Traversable(3, 6, 9))
//3. 通过transpose方法, 对集合t1进行转置操作.
val t2 = t1.transpose
//4. 打印结果.
println(t2)
}
}
2.5 案例三: 拼接集合
在实际开发中, 数据是从多渠道获取到的, 所以我们经常需要拼接一些数据, 在Scala中, 我们可以通过++
来拼接数据, 但是这种方式会创建大量的临时集合(即: 每++一次, 就会创建一个新的临时集合), 针对这种情况, 我们可以通过concat()方法
来实现. 该方法会预先计算出所需的集合的大小, 然后生成一个集合, 减少了中间无用的临时集合, 所以它更加有效.
需求
- 已知有三个Traversable集合, 分别存储(11, 22, 33), (44, 55), (66, 77, 88, 99)元素.
- 通过concat()方法拼接上述的三个集合.
- 将拼接后的结果打印到控制台上.
参考代码
//案例: 演示concat()方法, 拼接集合.
object ClassDemo03 {
def main(args: Array[String]): Unit = {
//1. 已知有三个Traversable集合, 分别存储(11, 22, 33), (44, 55), (66, 77, 88, 99)元素.
val t1 = Traversable(11, 22, 33)
val t2 = Traversable(44, 55)
val t3 = Traversable(66, 77, 88, 99)
//2. 通过concat()方法拼接上述的三个集合.
val t4 = Traversable.concat(t1, t2, t3)
//3. 将拼接后的结果打印到控制台上.
println(s"拼接后结果为: ${t4}")
}
}
2.6 案例四: 利用偏函数筛选元素
在Scala中, 我们还可以通过collect()方法
实现偏函数结合集合
来使用, 从而来从集合中筛选指定的数据.
格式
def collect[B](pf: PartialFunction[A, B]): Traversable[B]
解释:
[B]
表示通过偏函数处理后, 返回值的数据类型.pf: PartialFunction[A, B]
表示collect()方法需要传入一个偏函数对象.Traversable[B]
表示返回的具体数据的集合.
需求
- 已知有一个Traversable集合, 存储元素为: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.
- 通过collect方法筛选出集合中所有的偶数.
参考代码
//案例: 通过偏函数筛选出集合中所有的偶数.
object ClassDemo04 {
def main(args: Array[String]): Unit = {
//1. 已知有一个Traversable集合, 存储元素为: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.
val t1 = (1 to 10).toTraversable
//2. 通过collect方法筛选出集合中所有的偶数.
val t2 = t1.collect( {
case x if x % 2 == 0 => x
})
//3. 打印结果.
println(t2)
}
}
2.7 案例五: 计算集合元素的阶乘
假设一个Traversable[Int]集合中包含(1, 2, 3, 4, 5)五个数字, 如果让我们计算每个元素的阶乘, 并放到一个新的Traversable[Int]集合中, 我们可以通过递归来实现, 但是这种实现方式有弊端, 每次计算都是从头开始计算的, 例如: 获取5的阶乘, 是通过5 * 4 * 3 * 2 * 1
计算出来的, 并没有运用之前计算出来的4的阶乘的结果. 此时, 我们就可以通过scan()方法
来优化此需求了, 它不仅将中间的计算结果放入新的集合中, 并且还会把中间结果传递给下一次的函数调用.
格式
def scan[B](z: B)(op: (B, B) => B)
解释:
[B]
表示返回值的数据类型.(z: B)
表示初始化值.(op: (B, B) => B)
表示一个具体的运算函数.- scan()方法等价于scanLeft()方法, 还有一个跟它相反的方法scanRight().
需求
- 定义Traversable集合t1, 存储1, 2, 3, 4, 5这五个数字.
- 假设初始值为1, 通过scan()方法, 分别获取t1集合中各个元素的阶乘值.
- 打印结果.
参考代码
//案例: 通过scan()方法, 获取集合中元素的阶乘值.
object ClassDemo05 {
def main(args: Array[String]): Unit = {
//1. 定义Traversable集合t1, 存储1, 2, 3, 4, 5这五个数字.
val t1 = Traversable(1, 2, 3, 4, 5)
//2. 假设初始值为1, 通过scan()方法, 分别获取t1集合中各个元素的阶乘值.
val t2 = t1.scan(1)(_ * _)
//3. 打印结果.
println(t2)
}
}
2.8 案例六: 获取集合的指定元素
集合是用来存储数据的, 既然能存储, 那肯定也可以从集合中获取我们想要的数据, 可以通过如下的方法实现:
-
head: 获取集合的第一个元素, 如果元素不存在, 则抛出
NoSuchElementException异常
. -
last: 获取集合的最后一个元素, 如果元素不存在, 则抛出
NoSuchElementException异常
. -
headOption: 获取集合的第一个元素, 返回值类型是Option.
-
lastOption: 获取集合的最后一个元素, 返回值类型是Option.
-
find: 查找集合中第一个满足指定条件的元素.
-
slice: 截取集合中的一部分元素.
def slice(from:Int, until: Int): Traversable[A]
注意:
截取从from(起始索引)开始, 至until索引(结束索引)结束的元素, 包含from索引, 但是不包含until索引.
需求
- 定义一个Traversable集合, 包含1, 2, 3, 4, 5, 6这六个元素.
- 分别通过head, headOption, last, lastOption获取集合中的首尾第一个元素, 并打印.
- 通过find方法获取集合中第一个偶数, 并打印.
- 通过slice()方法获取3, 4, 5这三个元素, 并将它们放到一个新的Traversable集合中, 然后打印结果.
参考代码
//案例: 获取Traversable对象的特定的元素
object ClassDemo06 {
def main(args: Array[String]): Unit = {
//1. 定义一个Traversable集合, 包含1, 2, 3, 4, 5, 6这六个元素.
val t1 = (1 to 6).toTraversable
//2. 分别通过head, headOption, last, lastOption获取集合中的首尾第一个元素, 并打印.
println(t1.head)
println(t1.last)
println(t1.headOption)
println(t1.lastOption)
//3. 通过find方法获取集合中第一个偶数, 并打印.
println(t1.find(_ % 2 == 0))
//4. 通过slice()方法获取3, 4, 5这三个元素, 然后打印结果.
val t2 = t1.slice(2, 5)
println(t2)
}
}
2.9 案例七: 判断元素是否合法
如果我们遇到判断集合中所有的元素是否都满足指定的条件, 或者任意元素满足指定的条件
这种需求时, 就可以考虑使用forall()方法和exists()方法
来实现了.
-
forall(): 如果集合中所有元素都满足指定的条件则返回true, 否则返回false.