Skip to content

项目2:查询手机号码信息——Scala基础 ​

先修基础:Java、Linux、Hadoop、Hive(有Java基础学Scala非常快) 笔记定位:学生复习+实操+面试一体化学习手册,大量对比Java,降低学习门槛


目录 ​


第一部分: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,这张对比表帮你快速建立认知:

对比维度JavaScala
编程范式纯面向对象面向对象 + 函数式编程
代码量啰嗦,样板代码多简洁,同样功能代码量少 50%~90%
静态类型必须显式声明类型有类型推断,大部分情况不用写类型
函数地位函数是类的成员,不能独立存在函数是一等公民,可以当参数、返回值、赋值给变量
变量/常量只有变量,final 模拟常量val(常量)和 var(变量),推荐用 val
空值null,容易空指针Option 类型,更安全
静态成员static 关键字用 object 单例对象实现
集合Java Collection 框架Scala Collection,更强大,函数式操作
LambdaJava 8+ 才有,功能有限原生支持,非常强大
样例类没有(需要Lombok等插件)case class,一行搞定,自带 equals/hashCode/toString
模式匹配switch(很弱)match(超强,可匹配类型、结构、样例类等)
默认访问修饰符default(包级私有)public
分号必须写自动推断,一般不用写

为什么 Spark 选 Scala 而不是 Java? ​

  1. 代码简洁:大数据处理逻辑用 Scala 写,代码量少,可读性高
  2. 函数式编程:RDD 的算子(map、filter、reduceByKey 等)本质就是函数式编程,用 Scala 写非常自然
  3. REPL 交互式:spark-shell 就是 Scala 的 REPL,可以边写边跑,适合数据分析
  4. 类型安全 + 类型推断:既有静态类型的安全,又不用像 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:解压安装包 ​

bash
tar -zxf /opt/apps/scala-2.13.16.tgz -C /usr/local/

解压后目录:/usr/local/scala-2.13.16/

步骤2:配置环境变量 ​

编辑 /etc/profile:

bash
vi /etc/profile

添加:

bash
# SCALA_HOME
export SCALA_HOME=/usr/local/scala-2.13.16
export PATH=$PATH:$SCALA_HOME/bin

使配置生效:

bash
source /etc/profile

步骤3:验证安装 ​

bash
scala -version

看到版本信息就说明安装成功了。


1.5 Scala 的运行方式 ​

方式一:REPL 交互式(最常用,学习首选) ​

REPL = Read-Evaluate-Print-Loop(读取-执行-输出-循环)

bash
# 直接输入 scala 进入 REPL
scala

进入后看到 scala> 提示符,就可以直接写代码了,输入一行执行一行。

常用 REPL 命令:

命令作用
:quit退出 REPL
:paste进入粘贴模式(写多行代码用)
Ctrl + D退出粘贴模式
:help查看帮助
:load 文件名加载外部 Scala 文件

💡 小技巧:写多行代码(比如定义类)的时候,用 :paste 进入粘贴模式,写完后按 Ctrl+D 执行,不然一行一行写容易报错。

方式二:编译运行(开发项目用) ​

bash
# 编译
scalac Hello.scala

# 运行
scala Hello

方式三:IDEA 开发(企业开发用) ​

  • 安装 IntelliJ IDEA + Scala 插件
  • 适合大型项目开发

方式四:在线运行(快速体验) ​


第二部分: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 可以根据赋值自动推断数据类型,大部分时候不用显式写类型:

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) ​

定义:值不可变的量,推荐优先使用。

scala
val name: Type = 初始值

示例:

scala
val a: Int = 10
val b = 20        // 类型推断,省略 Int
val name = "Scala" // 推断为 String

特点:

  • 一旦赋值就不能再改
  • 强行修改会报错:error: reassignment to val
  • 推荐优先用 val,符合函数式编程思想(不可变)

② 变量 var(variable) ​

定义:值可以改变的量。

scala
var name: Type = 初始值

示例:

scala
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。

scala
val 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) ​

数组是存储相同类型元素的固定大小的顺序集合。

定义数组 ​

方式一:先指定长度,后赋值 ​

scala
var z: Array[String] = new Array[String](3)
z(0) = "baidu"
z(1) = "google"
z(2) = "bing"

方式二:直接赋值(更常用) ​

scala
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开始

连接数组 ​

方式一:用 ++ 操作符 ​

scala
val arr1 = Array(1, 2, 3)
val arr2 = Array(4, 5, 6)
val arr3 = arr1 ++ arr2   // Array(1, 2, 3, 4, 5, 6)

方式二:用 concat() 方法 ​

scala
import Array._
val arr4 = concat(arr1, arr2)

创建区间数组 ​

scala
import Array._
val arr = range(1, 10, 2)  // Array(1, 3, 5, 7, 9)
// range(起始, 结束, 步长),不包含结束值

可变数组(ArrayBuffer) ​

默认的 Array 是不可变的(长度固定)。如果需要可变长度的数组,用 ArrayBuffer:

scala
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 的一等公民,也是函数式编程的核心。

函数定义 ​

语法格式:

scala
def 函数名(参数名: 参数类型, ...): 返回类型 = {
  函数体
}

示例:两个数相加 ​

简洁写法(推荐) ​

scala
def add(a: Int, b: Int): Int = { a + b }

完整写法 ​

scala
def add(a: Int, b: Int): Int = {
  var sum = 0
  sum = a + b
  return sum
}

函数定义的几个关键点 ​

  1. def 关键字:定义函数必须用 def
  2. 参数必须写类型:参数类型不能省略(Scala 不会推断参数类型)
  3. 返回类型可以省略:Scala 可以自动推断返回值类型(但递归函数必须显式写返回类型)
  4. return 可以省略:函数体最后一行表达式的值就是返回值
  5. 等号 = 不能少:函数体前必须有 =,没有 = 的函数返回 Unit

无返回值的函数 ​

如果函数不需要返回值,返回类型是 Unit(对应 Java 的 void):

scala
def sayHello(name: String): Unit = {
  println("Hello, " + name)
}

// 也可以省略返回类型和等号(不推荐,容易混淆)
def sayHello(name: String) {
  println("Hello, " + name)
}

函数调用 ​

scala
// 直接调用
add(3, 5)

// 如果函数在类/对象里
Test.addInt(3, 5)

匿名函数(Lambda 表达式) ​

定义:没有名字的函数,用 => 定义。

语法:

scala
(参数列表) => 函数体

示例:

scala
// 两个数相加的匿名函数
(a: Int, b: Int) => a + b

// 可以赋值给变量
val add = (a: Int, b: Int) => a + b
add(3, 5)  // 调用,结果 8

占位符语法(下划线 _) ​

如果每个参数在函数体中最多只出现一次,可以用 _ 代替参数:

scala
// 完整写法
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 等都是高阶函数)。

① 函数作为参数 ​

scala
// 定义高阶函数: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

② 函数作为返回值 ​

scala
// 函数返回另一个函数
def perimeter(width: Double): Double => Double = {
  (height: Double) => 2 * (width + height)
}

// 调用
val p = perimeter(5.0)  // 返回一个函数
p(3.0)  // 调用返回的函数,结果 16.0

函数柯里化(Currying) ​

定义:把接收多个参数的函数,变成接收一个参数的函数,返回接收余下参数的新函数。

普通写法:

scala
def addInt(a: Int, b: Int): Int = a + b
addInt(1, 2)  // 调用

柯里化写法:

scala
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 ​

scala
if (x > 10) {
  println("x 大于 10")
}

② if...else ​

scala
if (x > 10) {
  println("x 大于 10")
} else {
  println("x 小于等于 10")
}

③ if...else if...else ​

scala
if (x == 10) {
  println("x 的值为 10")
} else if (x == 20) {
  println("x 的值为 20")
} else {
  println("无法判断 x 的值")
}

④ if 嵌套 ​

scala
if (x > 10) {
  if (x > 20) {
    println("x 大于 20")
  } else {
    println("x 大于 10 但小于等于 20")
  }
}

if 表达式有返回值(重点!) ​

Scala 的 if 不是语句,是表达式,可以返回值并赋值给变量:

scala
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) ​

scala
// 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 是「直到」,到终点前停下。

遍历集合 ​

scala
val arr = Array("a", "b", "c")
for (elem <- arr) {
  println(elem)
}

多重循环(嵌套循环) ​

Scala 的 for 可以写多个生成器,用分号或花括号隔开:

scala
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 条件过滤元素,多个条件用分号隔开:

scala
// 输出 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 循环的每次结果收集起来,组成一个新的集合返回。

语法:

scala
val result = for {
  变量 <- 集合
  if 条件
} yield 表达式

示例:收集 1~10 的所有偶数

scala
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+ 推荐用字符串插值,不用 + 拼接:

scala
val name = "Tom"
val age = 20
println(s"姓名:$name,年龄:$age")
// 输出:姓名:Tom,年龄:20

注意:字符串前面加 s,变量前面加 $。


while 循环和 do...while 循环(了解) ​

Scala 也有 while 和 do...while,但用得不多(因为函数式编程倾向用不可变数据,而 while 需要 var)。

scala
// 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 ​

方式一:直接赋值(常用) ​

scala
// 定义 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 构造(经典写法) ​

scala
// "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元素个数

合并列表 ​

scala
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 ​

scala
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 ​

scala
// 不可变 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 最大的区别。

定义元组 ​

scala
// 方式一:直接写
val t = (1, 3.14, "hello")
// 类型是 (Int, Double, String)

// 方式二:Tuple3 写法
val t2 = Tuple3(1, 3.14, "hello")

访问元组元素 ​

scala
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 —— 一一映射 ​

作用:对集合中的每个元素应用一个函数,返回新集合(元素个数不变)。

scala
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),一般用于打印等副作用操作。

scala
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 —— 过滤 ​

作用:过滤出满足条件的元素,组成新集合。

scala
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 —— 扁平化 ​

作用:把嵌套的集合(集合的集合)拍平成一层。

scala
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 的组合。

scala
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。

scala
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) ​

定义类 ​

scala
class 类名(参数列表) {
  // 字段
  // 方法
}

示例:二维坐标点类 ​

scala
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)
  }
}

创建对象(实例化) ​

scala
val p = new Point(1, 2)
p.move(2, 3)
// 输出:
// x轴的坐标为:3
// y轴的坐标为:5

和 Java 的区别 ​

对比项JavaScala
类修饰符默认 public(或写 public)不用写,默认就是 public
构造函数构造方法名和类名相同类名后面的参数就是主构造函数
一个文件只能有一个 public 类可以有多个类
new 关键字必须用一般用(样例类可以不用)

💡 Scala 类的主构造函数: 类名后面括号里的参数就是主构造函数的参数,整个类体就是主构造函数的执行体。 这比 Java 简洁太多了!


5.2 继承(Extends) ​

Scala 用 extends 关键字继承类,和 Java 类似。

示例 ​

scala
// 父类:二维坐标点
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 是可选的注解)。

scala
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 关键字实现单例模式。

定义单例对象 ​

scala
object Person {
  val age = 10
  def getAge = age
}

// 调用
Person.getAge  // 10

特点 ​

  • 单例对象在整个程序中只有一个实例
  • 不用 new,直接用 对象名.方法/属性 调用
  • 单例对象不能带构造参数(这是和类的重要区别)

💡 类比 Java:object 里的成员就像 Java 的 static 成员,但 Scala 没有 static 概念,用单例对象实现。

程序入口 main 方法 ​

Scala 程序的入口是单例对象中的 main 方法:

scala
object Hello {
  def main(args: Array[String]): Unit = {
    println("Hello, Scala!")
  }
}

和 Java 的 main 方法类似,但必须写在 object 里。


5.4 伴生类和伴生对象(重点理解) ​

什么是伴生? ​

当一个 类(class) 和一个 单例对象(object) 同名,并且在同一个文件里时:

  • 这个类叫伴生类
  • 这个对象叫伴生对象

特点 ​

  • 伴生类和伴生对象可以互相访问对方的私有成员
  • 必须在同一个源文件里

示例 ​

scala
// 伴生类
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 强大得多,可以匹配值、类型、结构、样例类等。

基本语法 ​

scala
选择器 match {
  case 模式1 => 表达式1
  case 模式2 => 表达式2
  case _ => 默认表达式
}

匹配值 ​

scala
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

匹配列表结构 ​

scala
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 关键字定义,经过优化,特别适合模式匹配。

定义样例类 ​

scala
case class Person(name: String, age: Int)

样例类的好处(编译器自动帮你做的事) ​

  1. 不用 new:伴生对象自动生成 apply 方法,可以直接 Person("Alice", 25) 创建对象
  2. 参数自动是 val:构造参数默认是 val,不用手动写
  3. 自动生成方法:自动生成 toString、equals、hashCode、copy 等方法
  4. 支持模式匹配:可以直接用在 match 表达式中

示例 ​

scala
// 定义样例类
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:

scala
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 类来读取文件:

scala
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)
}

读取用户输入 ​

scala
import scala.io.StdIn

val line = StdIn.readLine()  // 读取用户输入的一行
println("你输入的是:" + line)

第六部分:项目实战——查询手机号码信息 ​

本章核心项目:基于手机号段数据,实现号码类型识别、数量统计、归属地分组、归属地查询四个功能 数据文件:2024phonelocation.txt 数据格式:编号,号码段,省份,市,号码类型,区号,邮编 示例数据:1,1300000,山东,济南,中国联通,0531,250000


任务一:识别手机号码类型 ​

需求 ​

输入手机号码段,判断是中国移动、中国联通还是中国电信。

实现思路 ​

  1. 用三个数组分别存储三大运营商的号码段
  2. 定义函数,接收号码段参数,判断属于哪个数组

完整代码 ​

scala
// 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 多分支判断
  • 函数定义与调用

任务二:统计广州号码段数量 ​

需求 ​

统计某个城市的号码段数量。

实现思路 ​

  1. 定义数组存储号码段数据(简化版,4条数据演示)
  2. 遍历数组,判断每条数据是否包含目标城市
  3. 计数并返回

完整代码 ​

scala
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() 字符串包含判断
  • 函数返回值

任务三:根据归属地对号码段分组 ​

需求 ​

按城市把号码段数据分组。

实现思路 ​

  1. 数据存放在 List 中
  2. 使用 groupBy() 函数组合器按城市字段分组

完整代码 ​

scala
// 号码段数据
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位),程序查询并输出所有匹配的号码段信息。

实现思路 ​

  1. 读取 2024phonelocation.txt 文件数据
  2. 转为 List 存储
  3. 接收用户输入的号码段
  4. 遍历列表,筛选出以输入号码段开头的数据并输出

完整代码 ​

scala
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 命令找不到 ​

原因:环境变量没配置或没生效 解决:

  1. 检查 /etc/profile 中 SCALA_HOME 和 PATH 是否正确
  2. 执行 source /etc/profile 使配置生效
  3. 检查安装路径是否正确

问题2:报错 NoClassDefFoundError ​

原因:JDK 版本不兼容或环境变量问题 解决:

  1. 确保 JDK 1.8+ 已安装且配置正确
  2. java -version 检查 JDK 版本
  3. 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:文件找不到 ​

原因:路径写错了,或者文件不存在 解决:

  1. 检查文件路径是否正确
  2. 用绝对路径试试
  3. 确认文件存在且有读取权限

问题14:中文乱码 ​

原因:文件编码和读取编码不一致 解决:指定编码读取:

scala
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? ​

参考任务一的思路:

scala
val 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
...

完整代码与逐行解析 ​

scala
// 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)

image-20260921025616872

知识点总结 ​

  • 文件读取: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月

基于 Vite 强力驱动 | 纯静态轻量托管