4 自适应类型与函数
笔记衔接前序《Scala 内建控制、类与对象》,严格匹配课程体系,针对有 Java 基础的大数据专业大学生设计,全程Java 语法对比 + 课程配套源码拆解 + Spark 大数据场景应用 + 避坑指南 + 作业全解析。Scala 函数式编程是 Spark/Flink 大数据开发的核心基础,Spark 的所有核心算子均基于高阶函数实现,本节内容必须重点掌握。
前置知识:Scala 面向对象基础、类与伴生对象、Java 基础语法
开发环境:JDK1.8+、Scala2.12.x、IDEA + Scala 插件
核心学习目标:
- 掌握 Scala 特殊类型(Nothing、Option [T]、Tuple)的用法与解决的核心问题
- 熟练掌握 Scala 函数的定义、规则、参数特性,明确与 Java 方法的核心差异
- 深入理解函数字面量、闭包、柯里化、部分应用函数等函数式编程核心特性
- 熟练使用 Scala 常用高阶函数,掌握 Spark 大数据开发的核心算子基础
- 独立完成课程配套作业,彻底掌握函数式编程的基础应用
第一章 Scala 自适应特殊类型
Scala 的自适应类型指编译器可自动进行类型推算,无需冗余的类型声明,其中 3 个特殊类型是 Scala 区别于 Java 的核心特性,解决了 Java 开发中的高频痛点。
1.1 Nothing 类型
1.1.1 核心特性
- Nothing 是 Scala 中所有类型的子类,处于 Scala 类层级的最底端,可赋值给任意类型;
- Nothing 是一个抽象类,没有任何实例值,仅用于类型推断与异常场景;
- Java 中无对应类型,这是 Scala 特有的类型设计。
1.1.2 核心作用
帮助 Scala 编译器进行类型推断,统一异常抛出场景的返回值类型。当方法抛出异常时,返回值类型为 Nothing,而 Nothing 是所有类型的子类,可与方法声明的返回值类型兼容,无需额外的类型处理。
1.1.3 课程案例实战
对应配套源码RumNothing.scala,实现整数除法,除数为 0 时抛出异常:
object RumNothing {
def main(args: Array[String]): Unit = {
val result1 = divide(4, 2)
println(s"result1=$result1") // 输出:result1=2
val result2 = divide(4, 0) // 抛出异常,异常返回值为Nothing,与Int兼容
println(s"result2=$result2")
}
// 异常抛出方法,返回值为Nothing
def error(msg: String) = {
throw new RuntimeException(msg)
}
// 除法方法,正常返回Int,异常返回Nothing(Int的子类)
def divide(x: Int, y: Int) = {
if (y != 0) {
x / y // Int类型
} else {
error("can not divide by zero") // Nothing类型,与Int兼容
}
}
}1.2 Option [T] 类型
1.2.1 核心特性
Option [T] 是 Scala 为解决 Java 空指针异常(NullPointerException)设计的特殊类型,是一个泛型抽象类,仅有两个子类:
Some(T):表示取值非空,包装了实际的值 T;None:表示取值为空,无实际值。
大数据场景应用:Spark/Flink 的 ETL 数据清洗中,经常遇到空值字段,使用 Option [T] 可避免频繁的 null 判断,防止空指针异常导致任务崩溃。
1.2.2 核心常用方法
表格
| 方法 | 作用 |
|---|---|
get | 获取 Some (T) 中包装的实际值,若为 None 则抛出异常 |
getOrElse(default: T) | 获取实际值,若为 None 则返回指定的默认值(最常用) |
isEmpty() | 判断是否为 None,返回 Boolean |
1.2.3 课程案例实战
对应配套源码RunOption.scala,通过 Map 获取值,避免空指针异常:
object RunOption {
def main(args: Array[String]): Unit = {
OptionTest()
}
def OptionTest(): Unit = {
// 定义Map集合
val map: Map[String, String] = Map("1" -> "java", "2" -> "scala")
// Map的get方法返回Option[String]类型,而非直接返回值
val v1 = map.get("1") // 存在key,返回Some("java")
val v2 = map.get("3") // 不存在key,返回None
println(s"v1=$v1,v2=$v2") // 输出:v1=Some(java),v2=None
// 1. isEmpty判断是否为空
if (v2.isEmpty) {
println(s"v2不存在,值为$v2")
} else {
println(s"v2的值为${v2.get}")
}
// 2. getOrElse:安全获取值,空值返回默认值(最常用)
println(s"v1=${v1.getOrElse("默认值")},v2=${v2.getOrElse("v2_default")}")
// 输出:v1=java,v2=v2_default
}
}1.3 元组 Tuple 类型
1.3.1 核心特性
元组是 Scala 中不同类型值的集合,可同时存放 Int、String、Boolean 等不同类型的元素,是 Scala 特有的类型。
- 元组最多支持22 个元素,对应 Tuple1~Tuple22;
- 元组是不可变的,一旦创建无法修改元素值;
- Java 中无原生元组类型,需通过自定义类、集合等方式实现多值返回,Scala 元组极大简化了代码。
大数据场景应用:Spark 中最核心的键值对 RDD(PairRDD),本质就是 Tuple2 类型的集合,是 WordCount、分组聚合等场景的基础。
1.3.2 核心用法
表格
| 操作 | 语法 | 说明 |
|---|---|---|
| 元组创建 | (元素1, 元素2, ...) / new TupleN(元素1,...) | 两种创建方式,前者更简洁 |
| 元素访问 | 元组名._索引 | 索引从 1 开始,而非 0 |
| 元组遍历 | 元组名.productIterator | 返回迭代器,遍历所有元素 |
| 元组交换 | 元组名.swap | 仅 Tuple2 支持,交换两个元素的位置 |
1.3.3 课程案例实战
对应配套源码RunTuple.scala,实现元组的创建、访问、遍历、求和、交换:
object RunTuple {
def main(args: Array[String]): Unit = {
// 1. 元组创建:两种方式
val t1 = ("java", 4) // 简洁写法,Tuple2[String, Int]
val t2 = new Tuple2[String, Int](sslocal://flow/file_open?url=%22python%22%2C+6&flow_extra=eyJsaW5rX3R5cGUiOiJjb2RlX2ludGVycHJldGVyIn0=) // 完整写法
// 2. 元组元素访问:索引从1开始
println(s"t1._1=${t1._1},t1._2=${t1._2}") // 输出:t1._1=java,t1._2=4
println(s"t2._1=${t2._1},t2._2=${t2._2}") // 输出:t2._1=python,t2._2=6
// 3. 元组遍历:productIterator
val t4 = (1, 2, 3, 4)
println("=== 元组遍历 ===")
t4.productIterator.foreach(i => println(s"i =${i.asInstanceOf[Int] + 1}"))
// 4. 元组所有元素求和
var sum = 0
t4.productIterator.foreach(i => sum = sum + i.asInstanceOf[Int])
println(s"sum=$sum") // 输出:sum=10
// 5. 二元组交换:swap方法
println(t1.swap) // 输出:(4,java)
}
}第二章 Scala 函数基础
Scala 既是面向对象编程语言,也是面向函数编程语言,函数是 Scala 的 “一等公民”,可像变量一样赋值、传递、返回。Java 中仅能定义类的成员方法,无独立的函数概念,这是两者的核心差异。
2.1 函数的定义与核心规则
2.1.1 基础语法
def 函数名(参数名: 参数类型, ...): 返回值类型 = {
函数体
[return 返回值]
}2.1.2 核心规则(与 Java 核心差异)
函数体只有一行代码时,可省略花括号
{};函数的返回值默认是函数体最后一行表达式的值,无需写 return 关键字;
编译器可自动推断返回值类型,大多数场景可省略返回值类型声明;
类型推导的两个强制限制(必须显式声明返回值类型):
- 函数体中使用
return关键字返回值时; - 函数存在递归调用时。
- 函数体中使用
2.1.3 课程案例实战
对应配套源码RunFunction.scala,实现基础函数与递归函数:
object RunFunction {
def main(args: Array[String]): Unit = {
val a = sum1(4, 5)
println(s"a=$a") // 输出:a=9
val a2 = sum2(4, 5)
println(s"a2=$a2") // 输出:a2=9
println(s"gcd=${gcd(0, 4)}") // 输出:gcd=4
}
// 1. 标准函数定义,带return,必须显式声明返回值类型
def sum1(x: Int, y: Int): Int = {
return x + y
}
// 2. 极简写法,省略花括号、return、返回值类型(编译器自动推断)
def sum2(x: Int, y: Int) = x + y
// 3. 递归函数:求最大公约数,必须显式声明返回值类型
def gcd(x: Int, y: Int): Int = {
if (x == 0) y
else gcd(y % x, x)
}
}2.2 函数的参数特性
2.2.1 参数默认值
Scala 支持给函数参数设置默认值,调用函数时可省略带默认值的参数,无需像 Java 一样通过方法重载实现。
- 调用时可通过参数名指定赋值,无需按参数顺序传递;
- 大数据场景应用:Spark 连接数据库的函数,可给超时时间、连接池大小设置默认值,简化调用。
课程案例实战
对应配套源码RunConnection.scala,实现带默认参数的连接创建函数:
class Connection {
/**
* 创建连接的方法
* @param timeout 超时时间,默认5000ms
* @param protocol 协议,默认http
* @return 超时时间和协议的元组
*/
def createConn(timeout: Long = 5000, protocol: String = "http") = {
println(s"timeout=${timeout},protocol=$protocol")
(timeout, protocol) // 元组返回多个值
}
}
object RunConnection {
def main(args: Array[String]): Unit = {
var c = new Connection()
// 1. 使用所有默认值
c.createConn() // 输出:timeout=5000,protocol=http
// 2. 仅给第一个参数赋值,第二个用默认值
c.createConn(700) // 输出:timeout=700,protocol=http
// 3. 按参数名指定赋值,跳过默认参数
c.createConn(protocol = "https") // 输出:timeout=5000,protocol=https
// 4. 按参数名乱序赋值
c.createConn(protocol = "https", timeout = 100) // 输出:timeout=100,protocol=https
// 5. 接收函数返回的多个值(元组解构)
val (a, b) = c.createConn(protocol = "ftp", timeout = 200)
println(s"a=${a},b=$b") // 输出:a=200,b=ftp
}
}2.2.2 可变参数(变参)
Scala 支持可变参数,函数可接受任意数量的同类型参数,语法为参数类型*,与 Java 的参数类型...功能类似,但用法更灵活。
- 可通过
_*将集合 / 数组的所有元素传递给可变参数; - 可变参数必须放在参数列表的最后一位。
课程案例实战
对应配套源码Utils.scala,实现可变参数函数:
class Utils {
// 可变参数:接受任意数量的String
def printNames(names: String*): Unit = {
names.foreach(x => println(x))
}
// 固定参数+可变参数
def printNamesandClass(i: String, j: String, names: String*): Unit = {
println(s"i=$i,j=$j")
names.foreach(x => println(x))
}
}
object RunVarible {
def main(args: Array[String]): Unit = {
val u = new Utils()
// 1. 传递单个参数
u.printNames("java")
// 2. 传递多个参数
u.printNames("java", "scala", "python")
// 3. 通过_*传递集合的所有元素
val fruits = List("apple", "banana", "cherry")
u.printNames(fruits: _*)
// 4. 无参数调用
u.printNames()
}
}2.3 链式风格编程
链式风格是一种代码设计模式,通过方法调用后返回当前对象,实现连续的方法调用,代码可读性更高。
- Scala 中通过返回
this.type实现链式调用,完美支持子类继承的链式调用; - Java 中通过返回
this实现链式调用,但子类继承时会丢失类型,无法继续调用子类方法。
课程案例实战
对应配套源码RunChainMode.scala,实现支持继承的链式风格:
class Person {
var name = ""
var age = 0
var city = "beijing"
// 返回this.type,支持子类继承的链式调用
def setName(name: String): this.type = {
this.name = name
this
}
def setAge(age: Int): this.type = {
this.age = age
this
}
def setCity(city: String): this.type = {
this.city = city
this
}
override def toString = s"Person($name, $age, $city)"
}
// 子类继承,仍支持链式调用
class Student extends Person {
var sex = ""
def setSex(sex: String): this.type = {
this.sex = sex
this
}
override def toString = s"Student($name, $age, $city, $sex)"
}
object RunChainMode {
def main(args: Array[String]): Unit = {
// 父类链式调用
val person = new Person().setName("张三").setAge(20).setCity("北京").toString
println(s"person=$person")
// 子类链式调用,可连续调用父类+子类方法
val student = new Student().setName("李四").setAge(18).setCity("上海").setSex("男").toString
println(s"student=$student")
}
}第三章 Scala 函数进阶
本节是 Scala 函数式编程的核心,也是理解 Spark 高阶算子的基础,重点掌握函数字面量、闭包、柯里化、部分应用函数四大特性。
3.1 函数字面量(匿名函数)
函数字面量即没有函数名的函数,也叫匿名函数,是 Scala 函数式编程的基础,Spark 算子中传入的函数几乎都是函数字面量。
3.1.1 基础语法
(参数名: 参数类型, ...) => { 函数体 }=>是函数字面量的核心符号,左边是参数列表,右边是函数体;- 函数体只有一行时,可省略花括号;
- 可通过
_占位符简化函数字面量,编译器可自动推断类型。
3.1.2 课程案例实战
对应配套源码RunLiteralFuntion.scala,实现函数字面量的多种写法与简化:
object RunLiteralFuntion {
def main(args: Array[String]): Unit = {
// 1. 标准函数字面量,赋值给变量
val sum1 = (x: Int, y: Int) => { x + y }
println(s"sum1=${sum1(3, 4)}") // 输出:sum1=7
// 2. 简化写法:省略花括号
val sum2 = (x: Int, y: Int) => x + y
println(s"sum2=${sum2(1, 4)}") // 输出:sum2=5
// 3. 占位符_简化:每个_对应一个参数,按顺序匹配
val sum3 = (_: Int) + (_: Int)
println(s"sum3=${sum3(7, 4)}") // 输出:sum3=11
// 4. 最简写法:先声明函数类型,再用_简化
val sum4: (Int, Int) => Int = _ + _
println(s"sum4=${sum4(6, 4)}") // 输出:sum4=10
// 5. 懒加载lazy:只有第一次使用时才会初始化
lazy val sum5 = (x: Int, y: Int) => { x + y }
println(sum5(8, 2)) // 输出:10
}
}3.2 函数作为参数
Scala 中函数是 “一等公民”,可作为参数传递给另一个函数,这是高阶函数的核心基础。接受函数作为参数的函数,称为高阶函数。
3.2.1 函数类型的声明
语法:(参数类型1, 参数类型2, ...) => 返回值类型
- 例如:
() => Unit表示无参数、无返回值的函数类型; Int => Int表示接受一个 Int 参数,返回 Int 类型的函数类型;(Int, Int) => Int表示接受两个 Int 参数,返回 Int 类型的函数类型。
3.2.2 课程案例实战
对应配套源码RunWithParFuntion.scala,实现接受函数作为参数的高阶函数:
object RunWithParFuntion {
def main(args: Array[String]): Unit = {
// 1. 无参数无返回值的函数作为参数
val sayhello = () => { println("hello scala function") }
exeFuctionWithOutPar(sayhello) // 输出:hello scala function
// 2. 带一个参数的函数作为参数
val plusTen = (i: Int) => { i + 10 }
val result = exeAdd(plusTen)
println(s"r=$result") // 输出:r=18
// 3. 带两个参数的函数作为参数
val sum = (x: Int, y: Int) => x + y
exeAndPrint(sum, 2, 3) // 输出:callback=5
val multi = (x: Int, y: Int) => x * y
exeAndPrint(multi, 2, 3) // 输出:callback=6
}
/**
* 高阶函数:接受一个无参数无返回值的函数作为参数
* @param callback 函数类型:() => Unit
*/
def exeFuctionWithOutPar(callback: () => Unit): Unit = {
callback() // 执行传入的函数
}
/**
* 高阶函数:接受一个Int=>Int的函数作为参数
*/
def exeAdd(callback: Int => Int): Int = {
callback(8) // 给函数传参8,执行后返回结果
}
/**
* 高阶函数:接受(Int,Int)=>Int的函数和两个Int参数
*/
def exeAndPrint(callback: (Int, Int) => Int, x: Int, y: Int): Unit = {
val result = callback(x, y)
println(s"callback=$result")
}
}3.3 闭包
3.3.1 闭包的定义
闭包是一个函数,连同该函数引用的非局部自由变量的引用环境,共同组成的整体。简单来说:
- 函数内部引用了函数外部定义的变量(自由变量);
- 该函数可作为值被传递、执行,即使脱离了原变量的作用域,仍能访问到该变量;
- 闭包捕获的是变量的引用,而非变量的值,外部变量修改后,闭包内的取值也会同步变化。
与 Java 的核心差异:Java 的匿名内部类只能访问 final 修饰的外部变量,而 Scala 闭包可直接访问非 final 的外部变量,无需额外限制。
3.3.2 课程案例实战
对应配套源码RunClosure.scala,实现闭包的基础用法:
object RunClosure {
def main(args: Array[String]): Unit = {
// 普通函数:仅使用函数内部的参数,无自由变量
val isage1 = (age: Int) => age > 18
println(isage1(10), isage1(20)) // 输出:(false,true)
// 闭包:引用了函数外部的自由变量voteage
var voteage = 18
val isage2 = (age: Int) => age > voteage
println(isage2(10), isage2(20)) // 输出:(false,true)
// 闭包作为参数传递,仍能访问voteage变量
new Clouse().printResult(isage2, 20) // 输出:true
// 外部变量修改,闭包内的取值同步变化(捕获的是引用)
voteage = 21
new Clouse().printResult(isage2, 20) // 输出:false
}
}
class Clouse {
// 接受函数作为参数,执行闭包
def printResult(f: Int => Boolean, x: Int): Unit = {
println(f(x))
}
}3.4 函数的柯里化
3.4.1 柯里化的定义
柯里化指将一个接受多个参数的函数,拆分成多个接受单个参数的函数列表,最终返回结果的过程。
- 基础语法:
def 函数名(参数列表1)(参数列表2)...(参数列表n): 返回值类型 = { 函数体 } - 核心本质:每调用一个参数列表,就返回一个接受下一个参数列表的函数,最终完成函数调用。
3.4.2 课程案例实战
对应配套源码RunK.scala,实现柯里化函数:
class RunK {
// 柯里化函数:两个参数列表,每个列表一个参数
def multi(x: Int)(y: Int): Int = {
x * y
}
}
object RunK {
def main(args: Array[String]): Unit = {
// 完整调用:传递所有参数列表
val r = new RunK().multi(10)(3)
println(r) // 输出:30
// 分步调用:先传第一个参数,返回一个函数
val multi10 = new RunK().multi(10) _
// 再传第二个参数,完成调用
val r2 = multi10(5)
println(r2) // 输出:50
}
}3.4.3 柯里化的核心作用
- 简化函数固定参数的重复调用:将固定的参数提前传入,返回一个专用函数,后续调用无需重复传参;
- 支持隐式参数:Scala 的隐式参数必须放在单独的柯里化参数列表中,是 Spark 隐式转换的基础;
- 辅助编译器类型推断:分步传参可帮助编译器更精准地推断类型。
3.5 部分应用函数
3.5.1 部分应用函数的定义
如果一个函数有多个参数,调用时只传递部分参数,剩余参数用_占位,会返回一个新的函数,这个新函数就是部分应用函数。
- 核心作用:固定函数的部分参数,生成一个参数更少的新函数,简化重复调用;
- 课程难点:与柯里化的区别 —— 柯里化是拆分参数列表,部分应用函数是固定部分参数、返回新函数。
3.5.2 课程案例实战
对应配套源码RunPartialFunction.scala,实现部分应用函数:
class RunPartialFunction {
// 给HTML内容添加前后缀
def warpHTMl(pref: String, context: String, suffix: String): String = {
pref + context + suffix
}
// 三个数相乘
def mutlti(x: Int, y: Int, z: Int) = x * y * z
}
object RunPartialFunction {
def main(args: Array[String]): Unit = {
val p = new RunPartialFunction()
// 1. 固定前后缀参数,仅保留内容参数,生成新的部分应用函数
val htmlwithp = p.warpHTMl("<p>", _: String, "</p>")
println("p= " + htmlwithp("i am p")) // 输出:p= <p>i am p</p>
val htmlwithdiv = p.warpHTMl("<div>", _: String, "</div>")
println("div= " + htmlwithdiv("i am div")) // 输出:div= <div>i am div</div>
// 2. 固定第二个参数为2,生成新函数
val f1 = p.mutlti(_: Int, 2, _: Int)
println(f1(4, 5)) // 4*2*5=40,输出:40
// 3. 固定后两个参数,仅保留第一个参数
val f2 = p.mutlti(_: Int, 2, 3)
println(f2(5)) // 5*2*3=30,输出:30
}
}第四章 Scala 常用高阶函数
高阶函数指接受函数作为参数、或返回函数作为结果的函数,是 Scala 集合操作的核心,也是 Spark RDD/DataFrame 算子的基础。Spark 中的map、flatMap、filter、reduce等核心算子,本质就是 Scala 高阶函数的分布式实现。
4.1 map 与 foreach
4.1.1 核心区别
表格
| 函数 | 作用 | 返回值 | 应用场景 |
|---|---|---|---|
map(f: T => B) | 对集合中的每个元素应用函数 f,返回一个新的集合 | 与原集合同长度的新集合 | 数据转换、元素处理 |
foreach(f: T => U) | 对集合中的每个元素应用函数 f,仅执行副作用 | 无返回值(Unit) | 遍历打印、数据写入等无返回值的操作 |
4.1.2 课程案例实战
对应配套源码RunHighFun.scala,实现 map 打印星号图形:
object RunHighFun {
def main(args: Array[String]): Unit = {
val array = Array(1, 2, 3, 4, 5)
// map:每个数字转换为对应数量的*,返回新数组
val s = array.map(x => "*" * x)
// 遍历打印星号图形
s.foreach(x => println(x))
// 极简写法:_占位符简化
array.map("*" * _).foreach(println(_))
}
}运行结果:
*
**
***
****
*****4.2 filter
4.2.1 核心作用
对集合中的每个元素应用断言函数(返回 Boolean 的函数),仅保留返回 true 的元素,返回一个新的集合。
- 语法:
filter(p: T => Boolean) - 大数据场景应用:ETL 数据过滤、脏数据清洗,Spark 的 filter 算子完全对应此功能。
4.2.2 课程案例实战
object FilterDemo {
def main(args: Array[String]): Unit = {
// 1. 过滤1-10中的奇数
val array1 = Array(1, 2, 3, 4, 5)
array1.filter(x => x % 2 != 0).foreach(e => println(e)) // 输出:1 3 5
// 2. 字符串数组转大写,过滤掉以S开头的字符串
val s2 = Array("java", "scala", "go")
s2.map(_.toUpperCase)
.filter(s => !s.startsWith("S"))
.foreach(e => println(e)) // 输出:JAVA GO
}
}4.3 flatten 与 flatMap
4.3.1 核心作用
flatten:将嵌套的集合(二维数组、集合的集合)展平为一维集合;flatMap:先对集合中的每个元素应用 map 函数,再对结果执行 flatten 展平,等价于map + flatten,是 Spark WordCount 的核心算子。
4.3.2 课程案例实战
对应配套源码RunHighFun.scala,实现字符串拆分求和:
object FlatMapDemo {
def main(args: Array[String]): Unit = {
val sumarray = Array("1,2", "3,4")
// 1. flatMap:先按逗号拆分字符串,再展平为一维数组
// 2. map:将字符串转为Int
// 3. reduce:求和
val sum = sumarray.flatMap(x => x.split(",")).map(_.toInt).reduce(_ + _)
println(s"sum=$sum") // 输出:sum=10
}
}4.4 reduce 与 reduceLeft
4.4.1 核心作用
对集合中的元素,按照指定的二元函数进行两两聚合,最终返回一个聚合结果。
reduce(op: (T, T) => T):对集合元素进行两两聚合,无固定顺序(有序集合按从左到右);reduceLeft(op: (B, T) => B):严格按照从左到右的顺序进行两两聚合,第一个参数为上一次的聚合结果,第二个参数为当前元素;- 大数据场景应用:数据聚合、求和、求最大值 / 最小值,Spark 的 reduce 算子完全对应此功能。
4.4.2 课程案例实战
object ReduceDemo {
def main(args: Array[String]): Unit = {
val array = Array(1, 2, 3, 4, 5)
// 1. reduce求数组所有元素的乘积
val r2 = array.reduce((x, y) => x * y)
println(s"r2=$r2") // 输出:r2=120
// 极简写法:_占位符
val r3 = array.reduce(_ * _)
println(s"r3=$r3") // 输出:r3=120
// 2. reduceLeft求数组中的最大值
val maxarray = Array(1, 20, 38, 400, 666, 0, 999)
val max = maxarray.reduceLeft { (x, y) =>
if (x > y) x else y
}
println(s"max=$max") // 输出:max=999
}
}4.5 fold
4.5.1 核心作用
与 reduce 功能类似,但支持指定初始值,聚合从初始值开始,而非集合的第一个元素。
- 语法:
fold(初始值)(op: (T, T) => T) - 优势:可处理空集合的场景,避免 reduce 空集合抛出异常,同时可指定聚合的初始基准值。
4.5.2 案例实战
object FoldDemo {
def main(args: Array[String]): Unit = {
val array = Array(1, 2, 3, 4, 5)
// 从初始值10开始,累加所有元素
val sum = array.fold(10)(_ + _)
println(s"sum=$sum") // 输出:sum=25
// 空集合安全,返回初始值
val emptyArray = Array.empty[Int]
val emptyResult = emptyArray.fold(0)(_ + _)
println(s"emptyResult=$emptyResult") // 输出:emptyResult=0
}
}第五章 课后作业全解析
对应课程配套作业文档,包含简答题与编程题全解析,所有代码可直接复制运行。
一、简答题
(1)怎么理解 Scala 的闭包?
答:闭包是 Scala 函数式编程的核心特性,是一个函数与其引用的非局部自由变量的引用环境共同组成的整体,核心要点如下:
- 核心构成:闭包包含两部分,一个函数,以及该函数引用的、定义在函数外部作用域的自由变量;
- 核心特性:闭包捕获的是自由变量的引用,而非变量的值,即使外部变量发生修改,闭包内访问到的也是变量的最新值;
- 生命周期:即使闭包函数脱离了自由变量的原作用域,被传递到其他地方执行,仍能正常访问该自由变量,延长了变量的生命周期;
- 与 Java 的差异:Java 的匿名内部类只能访问 final 修饰的外部变量,而 Scala 闭包可直接访问、修改非 final 的外部变量,无需额外限制;
- 应用场景:闭包常用于高阶函数的参数传递、上下文信息携带、Spark 算子中的函数传参等场景。
(2)Scala 与 Java 函数的相同点和不同点?
答:Scala 与 Java 的函数 / 方法既有共通之处,也有本质差异,核心对比如下:
表格
| 维度 | 相同点 | 不同点 |
|---|---|---|
| 核心定位 | 均用于封装可复用的业务逻辑,支持参数传递、返回值、重载 | Java 只有类的成员方法,无独立的函数概念;Scala 中函数是 “一等公民”,可独立于类存在,可像变量一样赋值、传递、返回 |
| 定义语法 | 均使用 def/function 关键字定义,支持参数列表、返回值类型声明 | 1. Scala 函数体最后一行表达式为默认返回值,可省略 return;Java 必须显式写 return(void 方法除外)2. Scala 支持自动类型推断,大多数场景可省略返回值类型;Java 必须显式声明返回值类型3. Scala 支持函数柯里化、多参数列表;Java 仅支持单参数列表 |
| 参数特性 | 均支持方法重载、可变参数 | 1. Scala 支持参数默认值、命名参数,无需重载即可简化调用;Java 必须通过方法重载实现默认参数2. Scala 可变参数通过_*传递集合,比 Java 更灵活 |
| 高级特性 | 均支持匿名内部类 / 匿名函数 | 1. Scala 支持函数字面量(匿名函数),可作为参数传递给高阶函数;Java8 之前无 Lambda 表达式,匿名函数能力受限2. Scala 支持闭包,可自由访问外部非 final 变量;Java 匿名内部类只能访问 final 变量3. Scala 支持高阶函数(函数作为参数 / 返回值);Java 仅能通过接口实现类似能力 |
| 递归特性 | 均支持递归调用 | Scala 递归函数必须显式声明返回值类型;Java 递归方法无此强制限制 |
(3)Nothing 的作用?
答:Nothing 是 Scala 中所有类型的子类,处于类层级的最底端,是无实例的抽象类,核心作用有两点:
- 辅助编译器进行类型推断:当方法抛出异常时,返回值类型为 Nothing,而 Nothing 是所有类型的子类,可与方法声明的任意返回值类型兼容,让编译器能正确推断方法的返回值类型,无需额外的类型处理。例如除法方法中,正常返回 Int,异常抛出的 Nothing 可与 Int 兼容,不会出现类型不匹配的问题。
- 标记非正常终止的程序流程:Nothing 用于表示永远不会有正常返回值的场景(如抛出异常、程序无限循环),明确标记该流程不会产生有效返回值,提升代码的可读性与类型安全性。
二、编程题
(1)编写函数 values (fun:(Int)=>Int,low:Int,high:Int),该函数输出一个集合,对应给定区间内给定函数的输入和输出。比如,values (x=>x*x,-5,5) 应该产出一个对偶的集合 (-5,25),(-4,16),(-3,9),…,(5,25)。
object Homework1 {
/**
* 生成区间内函数的输入输出对偶集合
* @param fun 输入Int输出Int的函数
* @param low 区间下限
* @param high 区间上限
* @return 对偶元组的集合
*/
def values(fun: Int => Int, low: Int, high: Int): Seq[(Int, Int)] = {
// 遍历low到high的区间,每个元素生成(输入, 函数输出)的对偶
(low to high).map(x => (x, fun(x)))
}
def main(args: Array[String]): Unit = {
// 测试:x的平方函数,区间-5到5
val result = values(x => x * x, -5, 5)
// 打印结果
result.foreach(println)
}
}(2)编写函数 largest (fun:(Int)=>Int,inputs:Seq [Int]),输出在给定输入序列中给定函数的最大值。举例来说,largest (x=>10x-xx,1 to 10) 应该返回 25。不得使用循环或递归。
object Homework2 {
/**
* 求函数在输入序列中的最大输出值
* @param fun 输入Int输出Int的函数
* @param inputs 输入序列
* @return 函数的最大输出值
*/
def largest(fun: Int => Int, inputs: Seq[Int]): Int = {
// 先对每个输入应用函数,再求最大值
inputs.map(fun).max
// 也可通过reduceLeft实现:inputs.map(fun).reduceLeft((x,y) => if(x>y) x else y)
}
def main(args: Array[String]): Unit = {
// 测试
val maxValue = largest(x => 10 * x - x * x, 1 to 10)
println(s"函数最大值:$maxValue") // 输出:25
}
}(3)修改前一个函数,返回最大的输出对应的输入。举例来说,largestAt (fun:(Int)=>Int,inputs:Seq [Int]) 应该返回 5。不得使用循环或递归。
object Homework3 {
/**
* 求函数最大输出值对应的输入
* @param fun 输入Int输出Int的函数
* @param inputs 输入序列
* @return 最大输出对应的输入值
*/
def largestAt(fun: Int => Int, inputs: Seq[Int]): Int = {
// 1. 将输入转为(输入, 函数输出)的对偶
// 2. 按函数输出值降序排序
// 3. 取第一个元素的输入值
inputs.map(x => (x, fun(x)))
.maxBy(_._2)
._1
}
def main(args: Array[String]): Unit = {
// 测试
val input = largestAt(x => 10 * x - x * x, 1 to 10)
println(s"最大输出对应的输入:$input") // 输出:5
}
}学习总结
- Scala 特殊类型:Nothing 辅助类型推断、Option [T] 解决空指针异常、Tuple 实现多值返回,三个特殊类型解决了 Java 开发中的高频痛点,也是 Scala 类型系统的核心特色。
- Scala 函数基础:Scala 函数是 “一等公民”,支持自动类型推断、参数默认值、命名参数、可变参数、链式风格,比 Java 方法更简洁、更灵活,大幅减少了模板代码。
- 函数式编程核心:函数字面量、闭包、柯里化、部分应用函数是 Scala 函数式编程的四大核心特性,是理解 Spark 高阶算子的基础,也是大数据开发中必须掌握的核心能力。
- 高阶函数:map/filter/flatMap/reduce 等高阶函数是 Scala 集合操作的核心,与 Spark 的核心算子完全对应,掌握这些函数,就掌握了 Spark 大数据开发的基础。
- 大数据场景适配:Scala 的函数式编程特性完美适配大数据的分布式数据处理场景,Spark/Flink 的 API 设计完全基于 Scala 的函数式编程思想,本节内容是后续大数据框架学习的核心基础。
下一章预习内容:Scala 的继承与 trait 特质。