Skip to content

4 自适应类型与函数 ​

笔记衔接前序《Scala 内建控制、类与对象》,严格匹配课程体系,针对有 Java 基础的大数据专业大学生设计,全程Java 语法对比 + 课程配套源码拆解 + Spark 大数据场景应用 + 避坑指南 + 作业全解析。Scala 函数式编程是 Spark/Flink 大数据开发的核心基础,Spark 的所有核心算子均基于高阶函数实现,本节内容必须重点掌握。

  • 前置知识:Scala 面向对象基础、类与伴生对象、Java 基础语法

  • 开发环境:JDK1.8+、Scala2.12.x、IDEA + Scala 插件

  • 核心学习目标:

    1. 掌握 Scala 特殊类型(Nothing、Option [T]、Tuple)的用法与解决的核心问题
  1. 熟练掌握 Scala 函数的定义、规则、参数特性,明确与 Java 方法的核心差异
  2. 深入理解函数字面量、闭包、柯里化、部分应用函数等函数式编程核心特性
  3. 熟练使用 Scala 常用高阶函数,掌握 Spark 大数据开发的核心算子基础
  4. 独立完成课程配套作业,彻底掌握函数式编程的基础应用

第一章 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 时抛出异常:

scala
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 获取值,避免空指针异常:

scala
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,实现元组的创建、访问、遍历、求和、交换:

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 基础语法 ​

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

2.1.2 核心规则(与 Java 核心差异) ​

  1. 函数体只有一行代码时,可省略花括号{};

  2. 函数的返回值默认是函数体最后一行表达式的值,无需写 return 关键字;

  3. 编译器可自动推断返回值类型,大多数场景可省略返回值类型声明;

  4. 类型推导的两个强制限制(必须显式声明返回值类型):

    • 函数体中使用return关键字返回值时;
    • 函数存在递归调用时。

2.1.3 课程案例实战 ​

对应配套源码RunFunction.scala,实现基础函数与递归函数:

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,实现带默认参数的连接创建函数:

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,实现可变参数函数:

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,实现支持继承的链式风格:

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 基础语法 ​

scala
(参数名: 参数类型, ...) => { 函数体 }
  • =>是函数字面量的核心符号,左边是参数列表,右边是函数体;
  • 函数体只有一行时,可省略花括号;
  • 可通过_占位符简化函数字面量,编译器可自动推断类型。

3.1.2 课程案例实战 ​

对应配套源码RunLiteralFuntion.scala,实现函数字面量的多种写法与简化:

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,实现接受函数作为参数的高阶函数:

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,实现闭包的基础用法:

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,实现柯里化函数:

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 柯里化的核心作用 ​

  1. 简化函数固定参数的重复调用:将固定的参数提前传入,返回一个专用函数,后续调用无需重复传参;
  2. 支持隐式参数:Scala 的隐式参数必须放在单独的柯里化参数列表中,是 Spark 隐式转换的基础;
  3. 辅助编译器类型推断:分步传参可帮助编译器更精准地推断类型。

3.5 部分应用函数 ​

3.5.1 部分应用函数的定义 ​

如果一个函数有多个参数,调用时只传递部分参数,剩余参数用_占位,会返回一个新的函数,这个新函数就是部分应用函数。

  • 核心作用:固定函数的部分参数,生成一个参数更少的新函数,简化重复调用;
  • 课程难点:与柯里化的区别 —— 柯里化是拆分参数列表,部分应用函数是固定部分参数、返回新函数。

3.5.2 课程案例实战 ​

对应配套源码RunPartialFunction.scala,实现部分应用函数:

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 打印星号图形:

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

运行结果:

plaintext
*
**
***
****
*****

4.2 filter ​

4.2.1 核心作用 ​

对集合中的每个元素应用断言函数(返回 Boolean 的函数),仅保留返回 true 的元素,返回一个新的集合。

  • 语法:filter(p: T => Boolean)
  • 大数据场景应用:ETL 数据过滤、脏数据清洗,Spark 的 filter 算子完全对应此功能。

4.2.2 课程案例实战 ​

scala
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,实现字符串拆分求和:

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 课程案例实战 ​

scala
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 案例实战 ​

scala
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 函数式编程的核心特性,是一个函数与其引用的非局部自由变量的引用环境共同组成的整体,核心要点如下:

  1. 核心构成:闭包包含两部分,一个函数,以及该函数引用的、定义在函数外部作用域的自由变量;
  2. 核心特性:闭包捕获的是自由变量的引用,而非变量的值,即使外部变量发生修改,闭包内访问到的也是变量的最新值;
  3. 生命周期:即使闭包函数脱离了自由变量的原作用域,被传递到其他地方执行,仍能正常访问该自由变量,延长了变量的生命周期;
  4. 与 Java 的差异:Java 的匿名内部类只能访问 final 修饰的外部变量,而 Scala 闭包可直接访问、修改非 final 的外部变量,无需额外限制;
  5. 应用场景:闭包常用于高阶函数的参数传递、上下文信息携带、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 中所有类型的子类,处于类层级的最底端,是无实例的抽象类,核心作用有两点:

  1. 辅助编译器进行类型推断:当方法抛出异常时,返回值类型为 Nothing,而 Nothing 是所有类型的子类,可与方法声明的任意返回值类型兼容,让编译器能正确推断方法的返回值类型,无需额外的类型处理。例如除法方法中,正常返回 Int,异常抛出的 Nothing 可与 Int 兼容,不会出现类型不匹配的问题。
  2. 标记非正常终止的程序流程:Nothing 用于表示永远不会有正常返回值的场景(如抛出异常、程序无限循环),明确标记该流程不会产生有效返回值,提升代码的可读性与类型安全性。

二、编程题 ​

(1)编写函数 values (fun:(Int)=>Int,low:Int,high:Int),该函数输出一个集合,对应给定区间内给定函数的输入和输出。比如,values (x=>x*x,-5,5) 应该产出一个对偶的集合 (-5,25),(-4,16),(-3,9),…,(5,25)。 ​

scala
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。不得使用循环或递归。 ​

scala
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。不得使用循环或递归。 ​

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

学习总结 ​

  1. Scala 特殊类型:Nothing 辅助类型推断、Option [T] 解决空指针异常、Tuple 实现多值返回,三个特殊类型解决了 Java 开发中的高频痛点,也是 Scala 类型系统的核心特色。
  2. Scala 函数基础:Scala 函数是 “一等公民”,支持自动类型推断、参数默认值、命名参数、可变参数、链式风格,比 Java 方法更简洁、更灵活,大幅减少了模板代码。
  3. 函数式编程核心:函数字面量、闭包、柯里化、部分应用函数是 Scala 函数式编程的四大核心特性,是理解 Spark 高阶算子的基础,也是大数据开发中必须掌握的核心能力。
  4. 高阶函数:map/filter/flatMap/reduce 等高阶函数是 Scala 集合操作的核心,与 Spark 的核心算子完全对应,掌握这些函数,就掌握了 Spark 大数据开发的基础。
  5. 大数据场景适配:Scala 的函数式编程特性完美适配大数据的分布式数据处理场景,Spark/Flink 的 API 设计完全基于 Scala 的函数式编程思想,本节内容是后续大数据框架学习的核心基础。

下一章预习内容:Scala 的继承与 trait 特质。

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