项目2:查询手机号码信息——Scala基础
先修基础:Java、Linux、Hadoop、Hive(有Java基础学Scala非常快) 笔记定位:学生复习+实操+面试一体化学习手册,大量对比Java,降低学习门槛
目录
- 第一部分:Scala 入门概览
- 第二部分:Scala 基本语法
- 第三部分:Scala 控制结构
- 第四部分:Scala 集合类型(重点+难点)
- 第五部分:Scala 面向对象编程
- 第六部分:项目实战——查询手机号码信息
- 第七部分:常见问题与排错指南
- 第八部分:实习 / 面试高频考点
- 附录:习题解析
第一部分:Scala 入门概览
1.1 Scala 是什么
一句话定义:Scala(Scalable Language)是一门运行在 JVM 上的多范式编程语言,同时支持面向对象编程和函数式编程,代码简洁优雅,是 Spark 的官方开发语言。
核心关键词
| 关键词 | 含义 |
|---|---|
| 多范式 | 同时支持面向对象 + 函数式编程,想怎么写就怎么写 |
| 运行在JVM上 | 编译成 Java 字节码,和 Java 完全互通,可以调用所有 Java 类库 |
| 静态类型 | 编译时类型检查,但有类型推断,不用每次都写类型 |
| 可扩展 | 语言本身可以扩展,适合构建大型系统 |
💡 通俗理解:Scala 就像「Java 的升级版 + 精简版」,保留了 Java 的所有优点(JVM生态、跨平台),又去掉了 Java 的啰嗦,还加了函数式编程的超能力。
1.2 Scala vs Java(核心对比,有Java基础必看)
你们已经学过 Java,这张对比表帮你快速建立认知:
| 对比维度 | Java | Scala |
|---|---|---|
| 编程范式 | 纯面向对象 | 面向对象 + 函数式编程 |
| 代码量 | 啰嗦,样板代码多 | 简洁,同样功能代码量少 50%~90% |
| 静态类型 | 必须显式声明类型 | 有类型推断,大部分情况不用写类型 |
| 函数地位 | 函数是类的成员,不能独立存在 | 函数是一等公民,可以当参数、返回值、赋值给变量 |
| 变量/常量 | 只有变量,final 模拟常量 | val(常量)和 var(变量),推荐用 val |
| 空值 | null,容易空指针 | Option 类型,更安全 |
| 静态成员 | static 关键字 | 用 object 单例对象实现 |
| 集合 | Java Collection 框架 | Scala Collection,更强大,函数式操作 |
| Lambda | Java 8+ 才有,功能有限 | 原生支持,非常强大 |
| 样例类 | 没有(需要Lombok等插件) | case class,一行搞定,自带 equals/hashCode/toString |
| 模式匹配 | switch(很弱) | match(超强,可匹配类型、结构、样例类等) |
| 默认访问修饰符 | default(包级私有) | public |
| 分号 | 必须写 | 自动推断,一般不用写 |
为什么 Spark 选 Scala 而不是 Java?
- 代码简洁:大数据处理逻辑用 Scala 写,代码量少,可读性高
- 函数式编程:RDD 的算子(map、filter、reduceByKey 等)本质就是函数式编程,用 Scala 写非常自然
- REPL 交互式:spark-shell 就是 Scala 的 REPL,可以边写边跑,适合数据分析
- 类型安全 + 类型推断:既有静态类型的安全,又不用像 Java 那样啰嗦
⚠️ 重要提醒:Scala 和 Java 可以无缝互调!Scala 可以直接调用 Java 的所有类库,Java 也可以调用 Scala 编译的类。所以你之前学的 Java 知识完全不会浪费!
1.3 Scala 四大核心特性
① 面向对象(Everything is Object)
- Scala 是纯粹的面向对象语言,每个值都是对象
- 连数字、函数都是对象(Java 里 int 是基本类型,不是对象)
- 支持类、继承、多态、封装等所有 OOP 特性
② 函数式编程(Functional Programming)
- 函数是一等公民:函数可以像变量一样赋值、传参、返回
- 不可变优先:推荐用 val(常量),少用 var(变量)
- 表达式优先:if、for 等都是表达式,有返回值
- 高阶函数:函数可以操作其他函数
③ 静态类型 + 类型推断
- 编译时类型检查,安全可靠
- 但有强大的类型推断,不用每次都写类型
- 兼顾了动态语言的简洁和静态语言的安全
④ 可扩展
- 可以自己定义操作符(运算符本质是方法)
- 隐式转换、隐式参数(高级特性,进阶再学)
- DSL 领域特定语言能力强
1.4 Scala 环境搭建
前置要求
- JDK 1.8+(必须,Scala 跑在 JVM 上)
- Linux / Windows / macOS 都可以
Linux 安装步骤
步骤1:解压安装包
tar -zxf /opt/apps/scala-2.13.16.tgz -C /usr/local/解压后目录:/usr/local/scala-2.13.16/
步骤2:配置环境变量
编辑 /etc/profile:
vi /etc/profile添加:
# SCALA_HOME
export SCALA_HOME=/usr/local/scala-2.13.16
export PATH=$PATH:$SCALA_HOME/bin使配置生效:
source /etc/profile步骤3:验证安装
scala -version看到版本信息就说明安装成功了。
1.5 Scala 的运行方式
方式一:REPL 交互式(最常用,学习首选)
REPL = Read-Evaluate-Print-Loop(读取-执行-输出-循环)
# 直接输入 scala 进入 REPL
scala进入后看到 scala> 提示符,就可以直接写代码了,输入一行执行一行。
常用 REPL 命令:
| 命令 | 作用 |
|---|---|
:quit | 退出 REPL |
:paste | 进入粘贴模式(写多行代码用) |
Ctrl + D | 退出粘贴模式 |
:help | 查看帮助 |
:load 文件名 | 加载外部 Scala 文件 |
💡 小技巧:写多行代码(比如定义类)的时候,用
:paste进入粘贴模式,写完后按Ctrl+D执行,不然一行一行写容易报错。
方式二:编译运行(开发项目用)
# 编译
scalac Hello.scala
# 运行
scala Hello方式三:IDEA 开发(企业开发用)
- 安装 IntelliJ IDEA + Scala 插件
- 适合大型项目开发
方式四:在线运行(快速体验)
- Scastie:https://scastie.scala-lang.org/
- 不用安装,浏览器里直接写代码运行
第二部分:Scala 基本语法
2.1 数据类型
Scala 数据类型层次结构
Scala 是纯面向对象的,所有类型都是对象,没有 Java 那样的基本类型和包装类的区别。
Any(所有类型的根)
/ \
/ \
AnyVal AnyRef
/ | \ \
Int String List ...(所有引用类型)
Long Double
Boolean Char
Float Byte Short
Unit常用数据类型一览
| 数据类型 | 含义 | 占用空间 | 取值范围 |
|---|---|---|---|
| Byte | 字节 | 8位 | -128 ~ 127 |
| Short | 短整型 | 16位 | -32768 ~ 32767 |
| Int | 整型 | 32位 | -2^31 ~ 2^31-1 |
| Long | 长整型 | 64位 | -2^63 ~ 2^63-1 |
| Float | 单精度浮点 | 32位 | — |
| Double | 双精度浮点 | 64位 | — |
| Boolean | 布尔 | — | true / false |
| Char | 字符 | 16位 | 单个字符 |
| String | 字符串 | — | 字符序列 |
| Unit | 无返回值 | — | 对应 Java 的 void,只有一个值 () |
| Null | 空引用 | — | 所有引用类型的子类 |
| Nothing | 所有类型的子类 | — | 表示非正常终止 |
类型推断(Type Inference)
Scala 可以根据赋值自动推断数据类型,大部分时候不用显式写类型:
val a = 10 // 自动推断为 Int
val b = 3.14 // 自动推断为 Double
val c = "hello" // 自动推断为 String
val d = true // 自动推断为 Boolean但建议定义函数参数时显式写类型,可读性更好。
⚠️ 和 Java 的区别:
- Java 的 int 是基本类型,Integer 是包装类;Scala 的 Int 就是对象,直接调用方法
- Scala 没有 Java 的自动装箱/拆箱问题,一切都是对象
- Unit 对应 Java 的 void,但 Unit 是一个真正的类型
2.2 常量与变量
① 常量 val(value)
定义:值不可变的量,推荐优先使用。
val name: Type = 初始值示例:
val a: Int = 10
val b = 20 // 类型推断,省略 Int
val name = "Scala" // 推断为 String特点:
- 一旦赋值就不能再改
- 强行修改会报错:
error: reassignment to val - 推荐优先用 val,符合函数式编程思想(不可变)
② 变量 var(variable)
定义:值可以改变的量。
var name: Type = 初始值示例:
var x: Int = 10
x = 20 // 可以重新赋值
var y = "hello"
y = "world" // 同类型可以重新赋值特点:
- 可以重新赋值
- 但类型不能变,只能赋同类型的值
- 赋不同类型会报错:
error: type mismatch
val vs var 对比
| 对比项 | val(常量) | var(变量) |
|---|---|---|
| 能否重新赋值 | ❌ 不能 | ✅ 能 |
| 推荐程度 | ⭐⭐⭐⭐⭐ 优先用 | ⭐⭐ 少用 |
| 函数式编程 | 符合 | 不符合 |
| 线程安全 | 安全 | 不安全 |
💡 编程建议:Scala 编程的黄金法则——能不用 var 就不用 var,优先用 val。不可变数据是函数式编程的核心思想,也是 Spark RDD 的设计理念(RDD 就是不可变的)。
命名规则
- 以字母或下划线开头
- 不能用美元符号
$ - 遵循驼峰命名法
- 常量名建议大写(和 Java 类似)
2.3 运算符
Scala 的运算符和 Java 基本一致,但有一个重要区别:Scala 的运算符本质是方法(函数)。
算术运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
+ | 加 | 1 + 2 等价于 1.+(2) |
- | 减 | 3 - 1 |
* | 乘 | 2 * 3 |
/ | 除 | 6 / 2 |
% | 取余 | 5 % 2 |
关系运算符
| 运算符 | 含义 |
|---|---|
== | 等于(注意:Scala 的 == 比较的是值,不是引用!) |
!= | 不等于 |
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
⚠️ 重要区别:Scala 的
==比较的是值(类似 Java 的 equals),比较引用用eq。scalaval a = new String("abc") val b = new String("abc") a == b // true(值相等) a.eq(b) // false(引用不同)
逻辑运算符
| 运算符 | 含义 |
|---|---|
&& | 与 |
| ` | |
! | 非 |
位运算符
| 运算符 | 含义 |
|---|---|
& | 按位与 |
| ` | ` |
^ | 按位异或 |
~ | 按位取反 |
<< | 左移 |
>> | 右移 |
>>> | 无符号右移 |
赋值运算符
| 运算符 | 含义 |
|---|---|
= | 赋值 |
+= | 加等于 |
-= | 减等于 |
*= | 乘等于 |
/= | 除等于 |
%= | 取余等于 |
💡 Scala 运算符的本质:在 Scala 里,
1 + 2其实是1.+(2)的简写,+就是 Int 类的一个方法。所以 Scala 可以自定义运算符,这也是 Scala 扩展性强的原因之一。
2.4 数组(Array)
数组是存储相同类型元素的固定大小的顺序集合。
定义数组
方式一:先指定长度,后赋值
var z: Array[String] = new Array[String](3)
z(0) = "baidu"
z(1) = "google"
z(2) = "bing"方式二:直接赋值(更常用)
var z: Array[String] = Array("baidu", "google", "bing")
// 或者省略类型,自动推断
var z = Array("baidu", "google", "bing")⚠️ 注意:Scala 数组的索引用圆括号
(),不是方括号[]! Java 是arr[0],Scala 是arr(0)。
常用数组操作
| 操作 | 代码 | 说明 |
|---|---|---|
| 获取长度 | z.length | 返回数组长度 |
| 获取第一个元素 | z.head | 第一个元素 |
| 获取除第一个外的元素 | z.tail | 去掉第一个,剩下的组成新数组 |
| 判断是否为空 | z.isEmpty | 空返回 true |
| 判断是否包含 | z.contains("baidu") | 包含返回 true |
| 获取第n个元素 | z(0) | 索引从0开始 |
连接数组
方式一:用 ++ 操作符
val arr1 = Array(1, 2, 3)
val arr2 = Array(4, 5, 6)
val arr3 = arr1 ++ arr2 // Array(1, 2, 3, 4, 5, 6)方式二:用 concat() 方法
import Array._
val arr4 = concat(arr1, arr2)创建区间数组
import Array._
val arr = range(1, 10, 2) // Array(1, 3, 5, 7, 9)
// range(起始, 结束, 步长),不包含结束值可变数组(ArrayBuffer)
默认的 Array 是不可变的(长度固定)。如果需要可变长度的数组,用 ArrayBuffer:
import scala.collection.mutable.ArrayBuffer
val buf = ArrayBuffer[Int]()
buf += 1 // 添加元素
buf += (2, 3) // 添加多个元素
buf ++= Array(4, 5) // 追加数组
buf.remove(0) // 删除指定位置元素💡 和 Java 的区别:
- Java 的数组是
[],Scala 是()- Scala 的 Array 是对象,有很多方法(head、tail、contains 等)
- Scala 默认是不可变数组,可变的要用 ArrayBuffer
2.5 函数(重点!函数式编程核心)
函数是 Scala 的一等公民,也是函数式编程的核心。
函数定义
语法格式:
def 函数名(参数名: 参数类型, ...): 返回类型 = {
函数体
}示例:两个数相加
简洁写法(推荐)
def add(a: Int, b: Int): Int = { a + b }完整写法
def add(a: Int, b: Int): Int = {
var sum = 0
sum = a + b
return sum
}函数定义的几个关键点
- def 关键字:定义函数必须用 def
- 参数必须写类型:参数类型不能省略(Scala 不会推断参数类型)
- 返回类型可以省略:Scala 可以自动推断返回值类型(但递归函数必须显式写返回类型)
- return 可以省略:函数体最后一行表达式的值就是返回值
- 等号 = 不能少:函数体前必须有
=,没有=的函数返回 Unit
无返回值的函数
如果函数不需要返回值,返回类型是 Unit(对应 Java 的 void):
def sayHello(name: String): Unit = {
println("Hello, " + name)
}
// 也可以省略返回类型和等号(不推荐,容易混淆)
def sayHello(name: String) {
println("Hello, " + name)
}函数调用
// 直接调用
add(3, 5)
// 如果函数在类/对象里
Test.addInt(3, 5)匿名函数(Lambda 表达式)
定义:没有名字的函数,用 => 定义。
语法:
(参数列表) => 函数体示例:
// 两个数相加的匿名函数
(a: Int, b: Int) => a + b
// 可以赋值给变量
val add = (a: Int, b: Int) => a + b
add(3, 5) // 调用,结果 8占位符语法(下划线 _)
如果每个参数在函数体中最多只出现一次,可以用 _ 代替参数:
// 完整写法
val add = (a: Int, b: Int) => a + b
// 占位符写法
val add = (_: Int) + (_: Int)
// 更多例子
val double = (_: Int) * 2 // 乘以2
val isEven = (_: Int) % 2 == 0 // 判断偶数💡 理解占位符:每个
_就是一个坑位,调用时按顺序填参数。两个_就代表两个参数。
高阶函数(Higher-Order Function)
定义:操作其他函数的函数,即函数可以作为参数,也可以作为返回值。
这是函数式编程的核心,也是 Spark RDD 算子的本质(map、filter 等都是高阶函数)。
① 函数作为参数
// 定义高阶函数:f 是一个函数参数,接收两个 Int 返回 Int
def addInt(f: (Int, Int) => Int, a: Int, b: Int): Int = {
f(a, b) // 调用传入的函数
}
// 调用:传入一个匿名函数
val result = addInt((x, y) => x + y, 3, 5)
// result = 8② 函数作为返回值
// 函数返回另一个函数
def perimeter(width: Double): Double => Double = {
(height: Double) => 2 * (width + height)
}
// 调用
val p = perimeter(5.0) // 返回一个函数
p(3.0) // 调用返回的函数,结果 16.0函数柯里化(Currying)
定义:把接收多个参数的函数,变成接收一个参数的函数,返回接收余下参数的新函数。
普通写法:
def addInt(a: Int, b: Int): Int = a + b
addInt(1, 2) // 调用柯里化写法:
def addInt(a: Int)(b: Int): Int = a + b
addInt(1)(2) // 调用原理:addInt(1) 先返回一个函数,这个函数接收 b,然后再调用 (2)。
💡 柯里化的用处:
- 可以分步传参,灵活组合
- Spark 中有很多地方用到柯里化
- 可以让类型推断更精准
第三部分:Scala 控制结构
3.1 if 判断
Scala 的 if 和 Java 类似,但有一个重要区别:Scala 的 if 是表达式,有返回值。
四种形式
① 单 if
if (x > 10) {
println("x 大于 10")
}② if...else
if (x > 10) {
println("x 大于 10")
} else {
println("x 小于等于 10")
}③ if...else if...else
if (x == 10) {
println("x 的值为 10")
} else if (x == 20) {
println("x 的值为 20")
} else {
println("无法判断 x 的值")
}④ if 嵌套
if (x > 10) {
if (x > 20) {
println("x 大于 20")
} else {
println("x 大于 10 但小于等于 20")
}
}if 表达式有返回值(重点!)
Scala 的 if 不是语句,是表达式,可以返回值并赋值给变量:
val x = 20
val result = if (x > 10) "大于10" else "小于等于10"
// result = "大于10"⚠️ 和 Java 的区别:Java 的 if 是语句,没有返回值;Scala 的 if 是表达式,有返回值。 这是函数式编程的特点——一切都是表达式,都有值。
3.2 for 循环(重点!Scala 特色)
Scala 的 for 循环比 Java 强大得多,叫 for 推导式(for comprehension)。
基本用法
范围循环(to 和 until)
// to:包含结束值(1到10,共10次)
for (i <- 1 to 10) {
print(i + " ")
}
// 输出:1 2 3 4 5 6 7 8 9 10
// until:不包含结束值(1到9,共9次)
for (i <- 1 until 10) {
print(i + " ")
}
// 输出:1 2 3 4 5 6 7 8 9💡 记忆技巧:
to是「到」,包含终点;until是「直到」,到终点前停下。
遍历集合
val arr = Array("a", "b", "c")
for (elem <- arr) {
println(elem)
}多重循环(嵌套循环)
Scala 的 for 可以写多个生成器,用分号或花括号隔开:
for (i <- 1 to 2; j <- 1 to 2) {
println("(" + i + "," + j + ")")
}输出:
(1,1)
(1,2)
(2,1)
(2,2)等价于 Java 的两层嵌套 for 循环,但 Scala 写起来更简洁。
守卫(if 过滤)
for 循环里可以加 if 条件过滤元素,多个条件用分号隔开:
// 输出 1~10 中大于 6 的偶数
for (i <- 1 to 10; if i % 2 == 0; if i > 6) {
println(i)
}
// 输出:8 10💡 守卫:就是 for 循环里的 if 条件,用来过滤不需要的元素。 这和 Spark RDD 的 filter 算子思想是一致的。
for...yield(推导式,重点!)
作用:把 for 循环的每次结果收集起来,组成一个新的集合返回。
语法:
val result = for {
变量 <- 集合
if 条件
} yield 表达式示例:收集 1~10 的所有偶数
val even = for {
i <- 1 to 10
if i % 2 == 0
} yield i
// even = Vector(2, 4, 6, 8, 10)⚠️ 重点理解:
yield会把每次循环的结果保存下来- 循环结束后,所有结果组成一个新集合返回
- 这就是「推导式」——从一个集合推导出另一个集合
- Spark RDD 的 map 算子就是这个思想!
字符串插值
Scala 2.13+ 推荐用字符串插值,不用 + 拼接:
val name = "Tom"
val age = 20
println(s"姓名:$name,年龄:$age")
// 输出:姓名:Tom,年龄:20注意:字符串前面加
s,变量前面加$。
while 循环和 do...while 循环(了解)
Scala 也有 while 和 do...while,但用得不多(因为函数式编程倾向用不可变数据,而 while 需要 var)。
// while 循环
var i = 0
while (i < 10) {
println(i)
i += 1
}
// do...while 循环
var j = 0
do {
println(j)
j += 1
} while (j < 10)💡 建议:Scala 编程优先用 for 推导式和递归,少用 while。
第四部分:Scala 集合类型(重点+难点)
Scala 的集合体系非常强大,是 Spark 编程的基础。RDD 的很多算子和集合的函数组合器思想完全一致。
4.1 Scala 集合总览
不可变集合 vs 可变集合
Scala 集合分为两大类:
| 类型 | 包 | 特点 | 推荐程度 |
|---|---|---|---|
| 不可变集合 | scala.collection.immutable | 创建后不能改,修改返回新集合 | ⭐⭐⭐⭐⭐ 优先用 |
| 可变集合 | scala.collection.mutable | 可以修改原集合 | ⭐⭐ 少用 |
💡 为什么优先用不可变集合?
- 线程安全,不用担心并发修改
- 函数式编程风格,代码更简洁
- Spark RDD 就是不可变的,思想一致
- 不可变集合的修改操作其实是高效的(不是全复制,而是共享大部分结构)
常用集合类型
| 集合类型 | 特点 | 类似 Java 的 |
|---|---|---|
| List | 有序、不可变、元素可重复 | LinkedList(但不可变) |
| Set | 无序、元素不重复 | Set |
| Map | 键值对、键唯一 | Map |
| Tuple(元组) | 元素类型可以不同 | 没有对应(Java 没有) |
| Array | 有序、可变长度固定 | 数组 |
4.2 List(列表)—— 最常用
List 是不可变的链表,元素有序、可重复。
定义 List
方式一:直接赋值(常用)
// 定义 String 类型的列表
val fruit: List[String] = List("apple", "pears", "oranges")
// 定义 Int 类型的列表
val nums: List[Int] = List(1, 2, 3, 4, 5)
// 自动推断类型
val fruit2 = List("apple", "pears", "oranges")
// 空列表
val empty: List[Nothing] = List()方式二:用 :: 和 Nil 构造(经典写法)
// "apple"::"pears"::"oranges"::Nil
// 读作:apple 加到 pears 加到 oranges 加到 空列表
val fruit: List[String] = "apple" :: "pears" :: "oranges" :: Nil
val nums: List[Int] = 1 :: 2 :: 3 :: 4 :: 5 :: Nil
val empty: List[Nothing] = Nil💡 理解
::和Nil:
Nil表示空列表::是中缀操作符,表示在列表前面加元素::是右结合的,所以1::2::3::Nil等价于1::(2::(3::Nil))
List 常用操作
查看元素
| 操作 | 代码 | 说明 |
|---|---|---|
| 第一个元素 | list.head | 返回第一个元素 |
| 除第一个外的元素 | list.tail | 返回去掉第一个元素的新列表 |
| 除最后一个外的元素 | list.init | 返回去掉最后一个元素的新列表 |
| 最后一个元素 | list.last | 返回最后一个元素 |
| 前n个元素 | list.take(n) | 返回前 n 个元素组成的新列表 |
| 第n个元素 | list(n) | 获取索引为 n 的元素(从0开始) |
判断操作
| 操作 | 代码 | 说明 |
|---|---|---|
| 是否为空 | list.isEmpty | 空返回 true |
| 是否包含 | list.contains(元素) | 包含返回 true |
| 长度 | list.length / list.size | 元素个数 |
合并列表
val list1 = List(1, 2, 3)
val list2 = List(4, 5, 6)
// 方式一:::: 操作符
val list3 = list1 ::: list2 // List(1, 2, 3, 4, 5, 6)
// 方式二:concat 方法
val list4 = List.concat(list1, list2)⚠️ 注意:List 是不可变的,以上所有「修改」操作都是返回新的 List,原 List 不变。
4.3 Set(集合)
Set 中的元素不重复,默认无序。
定义 Set
val set = Set(1, 2, 3, 3, 4)
// set = Set(1, 2, 3, 4) 重复的 3 自动去掉了常用操作
| 操作 | 代码 | 说明 |
|---|---|---|
| 添加元素 | set + 5 | 返回新 Set,添加了 5 |
| 删除元素 | set - 1 | 返回新 Set,删除了 1 |
| 是否包含 | set.contains(2) | 包含返回 true |
| 合并集合 | set1 ++ set2 | 合并两个 Set |
| 交集 | set1 & set2 | 两个 Set 都有的元素 |
| 大小 | set.size | 元素个数 |
4.4 Map(映射)
Map 是键值对结构,键唯一。
定义 Map
// 不可变 Map
val person = Map("name" -> "Tom", "age" -> 20, "gender" -> "male")常用操作
| 操作 | 代码 | 说明 |
|---|---|---|
| 根据键取值 | person("name") | 返回 "Tom" |
| 获取所有键 | person.keys | 返回所有 key 的集合 |
| 获取所有值 | person.values | 返回所有 value 的集合 |
| 是否包含键 | person.contains("name") | 包含返回 true |
| 添加键值对 | person + ("email" -> "tom@qq.com") | 返回新 Map |
⚠️ 注意:默认的 Map 是不可变的,添加/删除都是返回新 Map。
4.5 Tuple(元组)—— Scala 特色
元组可以包含不同类型的元素,这是和 List/Array 最大的区别。
定义元组
// 方式一:直接写
val t = (1, 3.14, "hello")
// 类型是 (Int, Double, String)
// 方式二:Tuple3 写法
val t2 = Tuple3(1, 3.14, "hello")访问元组元素
t._1 // 第一个元素,1
t._2 // 第二个元素,3.14
t._3 // 第三个元素,"hello"⚠️ 注意:
- 元组的索引从 1 开始,不是 0!(
_1、_2、_3...)- Scala 元组最多支持 22 个元素(Tuple1 ~ Tuple22)
- 元组是不可变的
元组的用处
- 函数返回多个值(Java 只能返回一个值,Scala 用元组可以返回多个)
- 键值对其实就是 Tuple2(
"key" -> "value"本质是("key", "value")的语法糖)
💡 和数组/列表的区别:
- 数组/列表:元素类型必须相同
- 元组:元素类型可以不同
- 数组/列表:索引从 0 开始
- 元组:索引从 1 开始(_1, _2...)
4.6 函数组合器(重点中的重点!Spark 算子的基础)
函数组合器就是集合的高阶函数,接收一个函数作为参数,对集合中的每个元素应用这个函数。
这是 Spark RDD 编程的核心思想,必须掌握!
① map —— 一一映射
作用:对集合中的每个元素应用一个函数,返回新集合(元素个数不变)。
val nums = List(1, 2, 3, 4, 5)
// 每个元素乘以 2
val doubled = nums.map(x => x * 2)
// doubled = List(2, 4, 6, 8, 10)
// 用占位符简写
val doubled2 = nums.map(_ * 2)💡 理解 map:一个进,一个出,每个元素都被「转换」一次,集合大小不变。 Spark RDD 的 map 算子就是这个逻辑!
② foreach —— 遍历(无返回值)
作用:对每个元素执行操作,没有返回值(返回 Unit),一般用于打印等副作用操作。
val nums = List(1, 2, 3)
nums.foreach(x => println(x))
// 输出:
// 1
// 2
// 3
// 简写
nums.foreach(println)⚠️ map vs foreach 的区别:
- map:有返回值,返回新集合
- foreach:无返回值(Unit),只做操作
- 纯函数式编程尽量少用 foreach(因为有副作用)
③ filter —— 过滤
作用:过滤出满足条件的元素,组成新集合。
val nums = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 过滤出偶数
val evens = nums.filter(x => x % 2 == 0)
// evens = List(2, 4, 6, 8, 10)
// 简写
val evens2 = nums.filter(_ % 2 == 0)💡 理解 filter:满足条件的留下,不满足的去掉。 Spark RDD 的 filter 算子就是这个逻辑!
④ flatten —— 扁平化
作用:把嵌套的集合(集合的集合)拍平成一层。
val nested = List(List(1, 2), List(3, 4), List(5, 6))
val flat = nested.flatten
// flat = List(1, 2, 3, 4, 5, 6)💡 理解 flatten:把二维的变成一维的,把「列表的列表」变成「一个列表」。
⑤ flatMap —— 先 map 再 flatten
作用:先对每个元素做 map(返回集合),再 flatten 拍平。 等于 map + flatten 的组合。
val words = List("hello world", "scala spark")
// 先 map 切分,得到 List(Array("hello","world"), Array("scala","spark"))
// 再 flatten 拍平
val result = words.flatMap(line => line.split(" "))
// result = List("hello", "world", "scala", "spark")💡 理解 flatMap:一对多的转换,每个输入元素产生多个输出元素。 Spark RDD 的 flatMap 算子就是这个逻辑!WordCount 第一步切分单词就用它!
⑥ groupBy —— 分组
作用:按指定的 key 对集合元素分组,返回 Map。
val phone = List(
"70999,1371001,广东,广州,中国移动,020,510000",
"71000,1371002,广东,广州,中国移动,020,510000",
"71348,1371350,广东,深圳,中国移动,0755,518000",
"71349,1371351,广东,深圳,中国移动,0755,518000"
)
// 按归属地(第4个字段,索引3)分组
val groupByPhone = phone.groupBy(x => x.split(",")(3))
// 结果是一个 Map:
// Map(
// "广州" -> List("70999,...", "71000,..."),
// "深圳" -> List("71348,...", "71349,...")
// )💡 理解 groupBy:按某个规则把数据分成几组,每组一个 key。 Spark RDD 的 groupByKey 类似,但 groupByKey 是 KV 结构的。
函数组合器总结表
| 组合器 | 作用 | 元素数量变化 | Spark 对应算子 |
|---|---|---|---|
| map | 一一转换 | 不变 | map |
| foreach | 遍历操作 | 不变(无返回) | foreach |
| filter | 过滤 | 减少 | filter |
| flatten | 扁平化 | 增加(拍平) | — |
| flatMap | 先map再flatten | 增加 | flatMap |
| groupBy | 分组 | 减少(分组) | groupBy / groupByKey |
⚠️ 重要结论:Scala 集合的函数组合器和 Spark RDD 的算子思想完全一致!学好集合,Spark 编程就成功了一半。
第五部分:Scala 面向对象编程
5.1 类(Class)
定义类
class 类名(参数列表) {
// 字段
// 方法
}示例:二维坐标点类
class Point(xc: Int, yc: Int) {
var x: Int = xc
var y: Int = yc
def move(dx: Int, dy: Int) = {
x = x + dx
y = y + dy
println("x轴的坐标为:" + x)
println("y轴的坐标为:" + y)
}
}创建对象(实例化)
val p = new Point(1, 2)
p.move(2, 3)
// 输出:
// x轴的坐标为:3
// y轴的坐标为:5和 Java 的区别
| 对比项 | Java | Scala |
|---|---|---|
| 类修饰符 | 默认 public(或写 public) | 不用写,默认就是 public |
| 构造函数 | 构造方法名和类名相同 | 类名后面的参数就是主构造函数 |
| 一个文件 | 只能有一个 public 类 | 可以有多个类 |
| new 关键字 | 必须用 | 一般用(样例类可以不用) |
💡 Scala 类的主构造函数: 类名后面括号里的参数就是主构造函数的参数,整个类体就是主构造函数的执行体。 这比 Java 简洁太多了!
5.2 继承(Extends)
Scala 用 extends 关键字继承类,和 Java 类似。
示例
// 父类:二维坐标点
class Point(xc: Int, yc: Int) {
var x: Int = xc
var y: Int = yc
def move(dx: Int, dy: Int) = {
x = x + dx
y = y + dy
println("x轴坐标: " + x)
println("y轴坐标: " + y)
}
}
// 子类:三维坐标点,继承 Point
class Location(xc: Int, yc: Int, zc: Int) extends Point(xc, yc) {
var z: Int = zc
// 重载 move 方法(参数不同)
def move(dx: Int, dy: Int, dz: Int) = {
x = x + dx
y = y + dy
z = z + dz
println("x轴坐标: " + x)
println("y轴坐标: " + y)
println("z轴坐标: " + z)
}
}override 关键字
Scala 重写父类的方法时,必须用 override 关键字(Java 是可选的注解)。
abstract class Father {
def fun1 = 1 // 已实现的方法
def fun2: Int // 抽象方法(没有方法体)
}
class Child extends Father {
override def fun1 = 2 // 重写已实现的方法,必须加 override
def fun2 = 1 // 实现抽象方法,不用加 override
}⚠️ 规则总结:
- 重写父类已实现的方法 → 必须加
override- 实现父类抽象的方法 → 不用加
override- Scala 只支持单继承(和 Java 一样)
5.3 单例对象(object)—— Scala 特色
Scala 没有 static 关键字,用 object 关键字实现单例模式。
定义单例对象
object Person {
val age = 10
def getAge = age
}
// 调用
Person.getAge // 10特点
- 单例对象在整个程序中只有一个实例
- 不用 new,直接用
对象名.方法/属性调用 - 单例对象不能带构造参数(这是和类的重要区别)
💡 类比 Java:object 里的成员就像 Java 的 static 成员,但 Scala 没有 static 概念,用单例对象实现。
程序入口 main 方法
Scala 程序的入口是单例对象中的 main 方法:
object Hello {
def main(args: Array[String]): Unit = {
println("Hello, Scala!")
}
}和 Java 的 main 方法类似,但必须写在 object 里。
5.4 伴生类和伴生对象(重点理解)
什么是伴生?
当一个 类(class) 和一个 单例对象(object) 同名,并且在同一个文件里时:
- 这个类叫伴生类
- 这个对象叫伴生对象
特点
- 伴生类和伴生对象可以互相访问对方的私有成员
- 必须在同一个源文件里
示例
// 伴生类
class Person private (val name: String) {
// 私有方法
private def getSkill() = name + "'s skill is:" + Person.skill
}
// 伴生对象
object Person {
private val skill = "basketball"
private val person = new Person("Tracy")
def printSkill = println(person.getSkill())
def main(args: Array[String]): Unit = {
Person.printSkill
}
}💡 理解伴生:
- 伴生类访问了伴生对象的私有属性
skill- 伴生对象访问了伴生类的私有方法
getSkill()- 伴生对象常用作「工厂」,用来创建伴生类的实例(比如 apply 方法)
5.5 模式匹配(match)—— Scala 杀手锏
Scala 的模式匹配比 Java 的 switch 强大得多,可以匹配值、类型、结构、样例类等。
基本语法
选择器 match {
case 模式1 => 表达式1
case 模式2 => 表达式2
case _ => 默认表达式
}匹配值
def matchTest(x: Int) = x match {
case 1 => println("one")
case 2 => println("two")
case _ => println("many") // _ 是通配符,匹配所有
}
matchTest(1) // one
matchTest(2) // two
matchTest(3) // many匹配列表结构
def matchTest(x: List[Int]) = x match {
case List(0, _, _) => println("列表有3个元素,第一个是0")
case List(1, _*) => println("列表第一个元素是1,后面任意个")
case List(_, 1, _*) => println("列表第二个元素是1")
case _ => println("其他情况")
}💡 模式匹配中的符号:
_匹配任意单个元素_*匹配任意多个元素(0个或多个)- 模式匹配是按顺序匹配的,匹配到第一个就执行
5.6 样例类(case class)—— Spark 常用
样例类是一种特殊的类,用 case 关键字定义,经过优化,特别适合模式匹配。
定义样例类
case class Person(name: String, age: Int)样例类的好处(编译器自动帮你做的事)
- 不用 new:伴生对象自动生成 apply 方法,可以直接
Person("Alice", 25)创建对象 - 参数自动是 val:构造参数默认是 val,不用手动写
- 自动生成方法:自动生成 toString、equals、hashCode、copy 等方法
- 支持模式匹配:可以直接用在 match 表达式中
示例
// 定义样例类
case class Person(name: String, age: Int)
// 创建对象(不用 new!)
val alice = Person("Alice", 25)
val bob = Person("Bob", 22)
val mike = Person("Mike", 24)
// 模式匹配
for (person <- List(alice, bob, mike)) {
person match {
case Person("Alice", 25) => println("Hi, Alice!")
case Person("Bob", 22) => println("Hi, Bob!")
case Person(name, age) => println("name:" + name + "\t" + "age:" + age)
}
}💡 样例类在 Spark 中的应用:
- DataSet 的数据经常用样例类来定义结构
- 结构化数据建模非常方便
- 比 Java 的 POJO 简洁太多了(一行顶几十行)
5.7 读写文件
写文件
Scala 没有专门的文件写入类,直接用 Java 的 PrintWriter:
import java.io._
val pw = new PrintWriter(new File("/opt/data/test.txt"))
pw.println("I am learning Scala")
pw.write("I am learning Scala") // 也可以用 write 方法
pw.close()读文件
Scala 提供了 Source 类来读取文件:
import scala.io.Source
// 读取文件并打印每个字符
Source.fromFile("/opt/data/test.txt").foreach(print)
// 按行读取
val lines = Source.fromFile("/opt/data/test.txt").getLines()
for (line <- lines) {
println(line)
}读取用户输入
import scala.io.StdIn
val line = StdIn.readLine() // 读取用户输入的一行
println("你输入的是:" + line)第六部分:项目实战——查询手机号码信息
本章核心项目:基于手机号段数据,实现号码类型识别、数量统计、归属地分组、归属地查询四个功能 数据文件:
2024phonelocation.txt数据格式:编号,号码段,省份,市,号码类型,区号,邮编 示例数据:1,1300000,山东,济南,中国联通,0531,250000
任务一:识别手机号码类型
需求
输入手机号码段,判断是中国移动、中国联通还是中国电信。
实现思路
- 用三个数组分别存储三大运营商的号码段
- 定义函数,接收号码段参数,判断属于哪个数组
完整代码
// 1. 创建数组存储三大运营商的号码段
val yidong = Array(1340, 1341, 1342, 1343, 1344, 1345, 1346, 1347, 1348,
135, 136, 137, 138, 139, 150, 151, 152, 157, 158, 159,
182, 183, 184, 187, 188, 178, 147, 1705)
val liantong = Array(130, 131, 132, 155, 156, 185, 186, 176, 145, 1709)
val dianxin = Array(133, 1349, 153, 180, 181, 189, 1700, 177)
// 2. 定义识别函数
def identify(x: Int) = {
if (yidong.contains(x)) {
println("这个号码是属于中国移动的")
} else if (liantong.contains(x)) {
println("这个号码是属于中国联通的")
} else if (dianxin.contains(x)) {
println("这个号码是属于中国电信的")
} else {
println("这个号码不属于我国号码")
}
}
// 3. 调用测试
identify(133) // 这个号码是属于中国电信的
identify(138) // 这个号码是属于中国移动的
identify(999) // 这个号码不属于我国号码知识点
- 数组定义与使用
contains()方法- if...else if...else 多分支判断
- 函数定义与调用
任务二:统计广州号码段数量
需求
统计某个城市的号码段数量。
实现思路
- 定义数组存储号码段数据(简化版,4条数据演示)
- 遍历数组,判断每条数据是否包含目标城市
- 计数并返回
完整代码
def count(area: String): Int = {
// 模拟数据(实际项目从文件读取)
val arr = Array(
"115036,1477799,广东,广州,中国移动,020,510000",
"115038,1477801,广东,东莞,中国移动,0769,511700",
"115033,1477796,广东,广州,中国移动,020,510000",
"115032,1477795,广东,广州,中国移动,020,510000"
)
var sum = 0
// for 循环 + if 守卫
for (a <- arr if a.contains(area)) {
sum += 1
}
println(sum)
sum
}
// 调用测试
count("广州") // 输出 3知识点
- for 循环 + if 守卫(过滤)
contains()字符串包含判断- 函数返回值
任务三:根据归属地对号码段分组
需求
按城市把号码段数据分组。
实现思路
- 数据存放在 List 中
- 使用
groupBy()函数组合器按城市字段分组
完整代码
// 号码段数据
val phone: List[String] = List(
"70999,1371001,广东,广州,中国移动,020,510000",
"71000,1371002,广东,广州,中国移动,020,510000",
"71348,1371350,广东,深圳,中国移动,0755,518000",
"71349,1371351,广东,深圳,中国移动,0755,518000"
)
// 根据归属地(第4个字段,索引3)分组
val groupByPhone = phone.groupBy(x => x.split(",")(3))
// 查看结果
groupByPhone.foreach(println)
// 输出:
// (广州,List(70999,1371001,..., 71000,1371002,...))
// (深圳,List(71348,1371350,..., 71349,1371351,...))知识点
- List 集合
split()字符串切分groupBy()分组函数(高阶函数)- 函数式编程思想
任务四:手机号码归属地查询程序(综合)
需求
用户输入手机号码段(前3~7位),程序查询并输出所有匹配的号码段信息。
实现思路
- 读取
2024phonelocation.txt文件数据 - 转为 List 存储
- 接收用户输入的号码段
- 遍历列表,筛选出以输入号码段开头的数据并输出
完整代码
import scala.io.StdIn
import scala.io.Source
object Phone {
def checkPhone() = {
// 1. 读取文件,每行一个号码段信息
val phone = for {
line <- Source.fromFile("/opt/data/2024phonelocation.txt").getLines()
} yield line
// 2. 转为 List(方便多次遍历)
val phoneList: List[String] = phone.toList
// 3. 接收用户输入
var num: String = StdIn.readLine()
// 4. 遍历列表,筛选匹配的号码段
for (line <- phoneList if line.split(",")(1).startsWith(num)) {
println(line)
}
}
}
// 调用
Phone.checkPhone()运行效果
输入:147779
输出:
115036,1477799,广东,广州,中国移动,020,510000
115033,1477796,广东,广州,中国移动,020,510000
115032,1477795,广东,广州,中国移动,020,510000
...
输入:1477799
输出:
115036,1477799,广东,广州,中国移动,020,510000知识点
- 文件读取(Source.fromFile)
- 用户输入(StdIn.readLine)
- 阻塞等待,从控制台读取用户输入的一行字符串,回车结束
- for 推导式
for-yield推导式遍历集合并生成新集合,类似 Java stream 的 map
startsWith()字符串前缀匹配- 单例对象(object)
- 相当于 Java 静态工具类,直接调用方法
- 综合运用前面所有知识点
- toList:转成 List 列表
- 把迭代器
Iterator转成不可变列表List - 为什么要转?
Iterator只能遍历一次,查完一次就不能再查了List可以重复遍历多次,支持多次查询- 这是 Scala IO 操作的经典最佳实践:读取 → 转集合 → 业务计算
- 把迭代器
第七部分:常见问题与排错指南
7.1 环境类问题
问题1:输入 scala 命令找不到
原因:环境变量没配置或没生效 解决:
- 检查
/etc/profile中SCALA_HOME和PATH是否正确 - 执行
source /etc/profile使配置生效 - 检查安装路径是否正确
问题2:报错 NoClassDefFoundError
原因:JDK 版本不兼容或环境变量问题 解决:
- 确保 JDK 1.8+ 已安装且配置正确
java -version检查 JDK 版本- Scala 版本和 JDK 版本要匹配
7.2 语法类问题
问题3:报错 error: not found: value xxx
原因:变量/函数名写错了,或者还没定义就用了 解决:检查拼写,确保变量/函数已经定义
问题4:报错 error: reassignment to val
原因:试图给 val(常量)重新赋值 解决:如果需要改变值,把 val 改成 var
问题5:报错 error: type mismatch
原因:类型不匹配,比如把 String 赋给 Int 变量 解决:确保赋值的类型和变量类型一致
问题6:数组/列表索引报错
原因:
- 用了方括号
[]而不是圆括号() - 索引越界 解决:
- Scala 用
arr(0),不是arr[0] - 检查索引是否在合法范围内
问题7:元组取值报错
原因:
- 索引用了 0 开头(元组从 1 开始)
- 用了方括号 解决:元组用
tuple._1、tuple._2... 不是tuple(0)
7.3 集合类问题
问题8:List 不能修改元素
原因:List 是不可变的 解决:
- 如果需要修改,用可变的 ListBuffer
- 或者用函数组合器生成新的 List
问题9:函数组合器返回的类型不对
原因:对函数组合器的返回值理解有误 解决:
- map 返回和原集合大小相同的新集合
- filter 返回更小的集合
- flatMap 返回更大的集合(一对多)
- groupBy 返回 Map
7.4 面向对象类问题
问题10:object 不能传参数
原因:单例对象不能带构造参数 解决:如果需要传参数创建对象,用 class(类),或者用伴生对象的 apply 方法
问题11:重写方法报错
原因:没有加 override 关键字 解决:重写父类已实现的方法必须加 override
问题12:模式匹配匹配不到
原因:模式写错了,或者顺序不对 解决:
- 检查模式的结构是否正确
- 注意匹配顺序(先具体后通用,
case _放最后)
7.5 文件操作类问题
问题13:文件找不到
原因:路径写错了,或者文件不存在 解决:
- 检查文件路径是否正确
- 用绝对路径试试
- 确认文件存在且有读取权限
问题14:中文乱码
原因:文件编码和读取编码不一致 解决:指定编码读取:
Source.fromFile("文件路径", "UTF-8")第八部分:实习 / 面试高频考点
针对面试,整理本章最常考的 Scala 知识点,按频率排序
8.1 概念类(高频)
Q1:Scala 和 Java 的区别?
参考 1.2 节对比表,重点说: ① Scala 同时支持面向对象和函数式编程 ② 代码更简洁(类型推断、样例类、模式匹配等) ③ 函数是一等公民(高阶函数、匿名函数) ④ 运行在 JVM 上,和 Java 完全互通
Q2:val 和 var 的区别?
val 是常量,不可重新赋值;var 是变量,可以重新赋值。 Scala 推荐优先用 val,符合函数式编程不可变的思想。
Q3:什么是函数式编程?
核心特点: ① 函数是一等公民(可以当参数、返回值、赋值给变量) ② 不可变数据(优先用 val,不用 var) ③ 表达式优先(if、for 都是表达式,有返回值) ④ 纯函数(相同输入总是相同输出,没有副作用)
Q4:Scala 的集合有哪些?
常用:List、Set、Map、Tuple、Array 分不可变(immutable)和可变(mutable),默认是不可变的。
Q5:Array 和 List 的区别?
- Array:长度固定,元素可变(可以改元素值),访问快
- List:不可变链表,元素不能改,添加/删除头部快
- 都是有序、元素类型相同
Q6:什么是元组?和列表有什么区别?
元组可以包含不同类型的元素,列表元素类型必须相同。 元组索引从 1 开始(_1, _2...),列表从 0 开始。 元组最多 22 个元素。
Q7:什么是高阶函数?举个例子
高阶函数就是操作其他函数的函数:函数可以作为参数,也可以作为返回值。 比如集合的 map、filter 都是高阶函数,接收一个函数参数。
Q8:什么是柯里化?
把接收多个参数的函数变成接收一个参数的函数,返回接收余下参数的新函数。
def add(a:Int)(b:Int) = a + b就是柯里化的写法。
8.2 集合类(高频)
Q9:map 和 flatMap 的区别?
- map:一一映射,每个输入元素产生一个输出元素,集合大小不变
- flatMap:先 map 再 flatten,每个输入元素产生多个输出元素,集合变大 举例:WordCount 切分单词就用 flatMap,一行输入变成多个单词输出。
Q10:map 和 foreach 的区别?
- map:有返回值,返回新集合,纯函数
- foreach:无返回值(Unit),只做操作(如打印),有副作用
Q11:filter 是干什么的?
过滤集合元素,满足条件的留下,不满足的去掉。返回新集合。
Q12:groupBy 是干什么的?
按指定的 key 对集合元素分组,返回一个 Map,key 是分组键,value 是该组的元素列表。
8.3 面向对象类(中频)
Q13:Scala 有没有 static?怎么实现静态成员?
Scala 没有 static 关键字。用 object(单例对象)实现静态成员的效果。 单例对象里的成员就类似 Java 的 static 成员。
Q14:什么是伴生类和伴生对象?
当一个 class 和一个 object 同名且在同一个文件里时:
- class 是伴生类,object 是伴生对象
- 它们可以互相访问对方的私有成员
Q15:样例类(case class)是什么?有什么好处?
用 case 关键字定义的特殊类,编译器自动优化: ① 不用 new 就能创建对象(自动生成 apply 方法) ② 构造参数默认是 val ③ 自动生成 toString、equals、hashCode、copy 方法 ④ 支持模式匹配 Spark 中常用于定义 DataSet 的数据结构。
Q16:Scala 的模式匹配和 Java 的 switch 有什么区别?
Scala 的 match 比 Java 的 switch 强大得多: ① 可以匹配值、类型、结构、样例类等 ② 不需要 break,匹配到第一个就停 ③ 有返回值 ④ 功能更强大,是 Scala 的杀手锏特性之一
Q17:Scala 是单继承还是多继承?
单继承(和 Java 一样),一个类只能继承一个父类。 但可以通过特质(trait)实现类似多继承的效果(本章没讲,进阶再学)。
8.4 实操类(中频)
Q18:怎么用 Scala 实现 WordCount?
参考任务一的思路:
scalaval lines = List("hello world", "hello scala") val words = lines.flatMap(_.split(" ")) val pairs = words.map((_, 1)) // 分组聚合(实际用 Spark 的 reduceByKey)核心步骤:切分 → 映射 → 分组聚合
Q19:怎么读取文件?怎么写文件?
读文件:用
scala.io.Source.fromFile("路径")写文件:用 Java 的java.io.PrintWriter(Scala 没有专门的写入类)
Q20:REPL 是什么?
Read-Evaluate-Print-Loop,读取-执行-输出-循环,就是交互式命令行。 spark-shell 就是基于 Scala REPL 的。
附录:习题解析
一、选择题解析
1、答案:C 解析:REPL 全称为 Read-Evaluate-Print-Loop,即读取-执行-输出-循环。
2、答案:A 解析:Scala 是一种纯粹的面向对象语言,每个值都是对象,并不是面向过程的程序设计语言。Scala 同时支持面向对象和函数式编程。
3、答案:D 解析:val 关键字用于定义常量,常量一旦定义后无法对常量进行重新赋值。var 才是定义变量的。
4、答案:D 解析:
Array[Int](0, 0)→ [0, 0]Array(0, 0)→ [0, 0](类型推断)new Array[Int](2)→ [0, 0](默认值)Array[Int](1, 1)→ [1, 1](不同)
5、答案:D 解析:目前 Scala 元组最多能包含 22 个元素(Tuple1 ~ Tuple22)。
6、答案:C 解析:
- 外层循环 i 取 1, 2, 3
- 内层循环 j 取 1, 2, 3
- 条件
i != j过滤掉 i=j 的情况 - 结果:12, 13, 21, 23, 31, 32
7、答案:C 解析:List[String]('a', 'b', 'c') 中试图将 Char 类型(单引号)放入 String 类型列表中,类型不匹配。字符串应该用双引号 "a"。
8、答案:B 解析:
- 第1次:k=1, v="Chinese" → k>1 为 false → 跳过
- 第2次:k=2, v="Math" → k>1 为 true → yield "Math"
- 第3次:k=3, v="English" → k>1 为 true → yield "English"
- 结果:List("Math", "English")
9、答案:B 解析:单例对象与类的区别在于单例对象不能带构造参数。
10、答案:C 解析:
- A 错:主构造函数参数默认私有,外部不能直接访问
- B 错:val 定义的成员是只读的,不能重新赋值
- D 错:val 不能重新赋值
- C 对:var 定义的成员可以重新赋值
二、操作题解析:学生成绩统计
题目
读取 primary_midsemester.txt 学生成绩数据,统计每门课程的平均分、最低分、最高分。
数据格式
ID gender Chinese English Math
301610 male 80 64 78
301611 female 65 87 58
...完整代码与逐行解析
// 1. 读取文件
val inputFile = scala.io.Source.fromFile("/opt/data/primary_midsemester.txt")
// 2. 按空白字符分割每行数据,转为 List[Array[String]]
val originalData = inputFile.getLines().map{x => x.split("\\s+")}.toList
// 3. 获取第一行的课程名(去掉前两列:ID 和 gender)
val courseNames = originalData.head.drop(2)
// courseNames = Array("Chinese", "English", "Math")
// 4. 去掉表头,剩下的是所有学生数据
val allStudents = originalData.tail
// 5. 统计函数:输入学生数据列表,返回每门课的(平均分, 最低分, 最高分)
def statistc(lines: List[Array[String]]) = {
// for 推导式:遍历每门课的列索引(从第2列开始,即第3列是语文)
(for(i <- 2 to courseNames.length + 1) yield {
// 取出该列的所有成绩,转为 Double
val temp = lines map { elem => elem(i).toDouble }
// 返回三元组:(总分, 最低分, 最高分)
(temp.sum, temp.min, temp.max)
}) map {
// 把总分除以人数得到平均分
case (total, min, max) => (total / lines.length, min, max)
}
}
// 6. 输出结果函数
def printResult(theresult: Seq[(Double, Double, Double)]): Unit = {
// zip 把课程名和统计结果配对,然后遍历输出
(courseNames zip theresult) foreach {
case (course, result) =>
// f 字符串插值:格式化输出(左对齐、保留小数)
println(f"${course + ":"}%-10s${result._1}%5.2f${result._2}%8.2f${result._3}%8.2f")
}
}
// 7. 调用函数,输出全体学生成绩统计
val allResult = statistc(allStudents)
printResult(allResult)
知识点总结
- 文件读取:
Source.fromFile- 打开指定路径的文本文件
- 默认按系统编码读取,中文环境容易乱码,生产环境建议指定编码:
Source.fromFile(path, "UTF-8")
- 字符串分割:
split("\\s+")按空白字符分割"\\s+"是正则表达式:匹配 1 个或多个空格、制表符\t- 对每一行做转换:把一行字符串按任意空白字符切割成字符串数组
- 比
split(" ")更鲁棒:不管字段之间是空格还是 Tab、几个空格,都能正确分割 - 转换后:
Iterator[Array[String]],每个元素是一行的所有字段
- .getLines()
- 按行读取文件,返回一个
Iterator[String](迭代器) - 迭代器的特点:只能遍历一次,遍历完就耗尽了,不能重复用
- 按行读取文件,返回一个
head/tail:取第一个/除第一个外的drop(n):去掉前 n 个元素- for 推导式 + yield
map、sum、min、max等高阶函数zip:两个集合配对- 格式化字符串:
f"..." - 函数式编程风格
笔记版本:V1.0 对应教材:《Spark大数据技术与应用(第3版)》人民邮电出版社 对应项目:项目2 查询手机号码信息——Scala基础 最后更新:2026年8月