Skip to content

6 访问权限、包、集合与迭代器 ​

笔记衔接前序《Scala 继承、多态与 Trait》,严格匹配课程体系,针对有 Java 基础的大数据专业大学生设计,全程Java 语法对比 + 课程配套源码拆解 + Spark 大数据场景应用 + 避坑指南 + 作业全解析。Scala 访问权限、集合体系是 Spark/Flink 大数据开发的基础,Spark RDD 的算子设计完全基于 Scala 集合的函数式编程思想,迭代器更是大数据海量数据处理的核心工具,本节内容必须重点掌握。

  • 前置知识:Scala 面向对象基础、函数式编程、类与 Trait

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

  • 核心学习目标:

    1. 掌握 Scala 的 5 种访问权限控制符,明确与 Java 访问权限的核心差异
  1. 熟练掌握 Scala 包的定义、作用域与高级导入特性,解决类名冲突问题
  2. 深入理解 Scala 集合的整体体系,明确可变集合与不可变集合的核心差异与适用场景
  3. 掌握 Scala 迭代器的核心特性与常用方法,理解其在大数据海量数据处理中的价值
  4. 独立完成课程配套作业,掌握 Scala 包对象、集合互操作的实战技巧

第一章 Scala 访问权限控制 ​

Scala 的访问权限控制比 Java 更精细、更灵活,提供了 5 种访问控制级别,核心设计思想是更严格的封装、更灵活的作用域控制,是编写规范、安全的 Scala 代码的基础。

1.1 Scala 与 Java 访问权限整体对比 ​

Java 提供 4 种访问控制符:public > protected > default(包私有) > private;

Scala 无public关键字,提供 5 种访问控制级别,权限从大到小为:

默认访问权限 > protected[包名] > private[包名] > protected > private > private[this]

表格

访问控制符Scala 作用域Java 作用域核心差异
无修饰符(默认)全局任何位置均可访问,等同于 Java 的public同包内可访问(包私有)Scala 无public关键字,无修饰符即为全局公开
protected仅本类 + 子类可访问,同包内其他类不可访问本类 + 子类 + 同包内其他类均可访问Scala 的protected权限比 Java 更严格,取消了同包访问权限
private仅本类 + 伴生对象可访问仅本类内部可访问Scala 的private允许伴生对象访问,Java 不允许
private[this]仅当前对象内部可访问,同一个类的其他对象也无法访问无对应特性Scala 独有,对象级别的私有,最严格的访问权限
private[包名]本类 + 指定包及其子包内可访问无对应特性Scala 独有,包级别的作用域控制,灵活调整访问范围

1.2 成员属性与构造器的访问权限 ​

Scala 的属性成员、方法、构造器均可通过访问控制符控制权限,课程配套源码Person0601.scala、Person0602.scala完整演示了各类访问权限的用法。

1.2.1 默认访问权限(等同于 Java public) ​

Scala 中没有public关键字,成员无任何访问修饰符时,即为默认全局公开权限,任何位置都可访问。

scala
class Person0601 {
  // 无修饰符:默认访问权限,全局可访问
  val name = "xiaoming"
}

object Person0601 {
  def main(args: Array[String]): Unit = {
    val p = new Person0601()
    println(p.name) // 合法,全局可访问
  }
}

1.2.2 private 访问权限 ​

private修饰的成员,仅能在本类内部和其伴生对象中访问,子类、外部类均无法访问,与 Java 的核心差异是允许伴生对象访问。

scala
class Person0601 {
  // private:仅本类和伴生对象可访问
  private val city = "beijing"

  // 本类内部可访问
  println(s"internal city=${city}")
}

class p0601 extends Person0601 {
  // println(s"p0601 city=${this.city}") // 非法,子类无法访问父类的private成员
}

object Person0601 {
  def main(args: Array[String]): Unit = {
    val p = new Person0601()
    println(s"object city=${p.city}") // 合法,伴生对象可访问private成员
  }
}

1.2.3 protected 访问权限 ​

protected修饰的成员,仅能在本类和其子类中访问,同包内的其他类无法访问,这是与 Java 最核心的差异(Java 同包内可访问)。

scala
class Person0601 {
  // protected:仅本类和子类可访问
  protected var age = 10

  // 本类内部可访问
  println(s"internal age=${age}")
}

class p0601 extends Person0601 {
  // 合法,子类可访问父类的protected成员
  println(s"p0601 age=${this.age}")
}

object Person0601 {
  def main(args: Array[String]): Unit = {
    val p = new Person0601()
    println(s"object age=${p.age}") // 合法,伴生对象可访问protected成员
  }
}

1.2.4 private [this] 对象私有权限 ​

private[this]是 Scala 独有的访问权限,称为对象私有,是最严格的访问级别:

  • 仅能在当前对象的内部访问,同一个类的其他对象实例也无法访问;
  • 伴生对象也无法访问private[this]修饰的成员;
  • Java 中无对应特性,体现了 Scala 更彻底的面向对象设计。
scala
class Person0601 {
  // private[this]:仅当前对象内部可访问
  private[this] val password = "123456"

  // 本对象内部可访问
  println(s"internal password=${this.password}")

  def print(other: Person0601) = {
    // 合法,访问当前对象的password
    println(s"this password=${this.password}")
    // 非法,无法访问其他对象的private[this]成员
    // println(s"other password=${other.password}")
  }
}

object Person0601 {
  def main(args: Array[String]): Unit = {
    val p = new Person0601()
    // 非法,伴生对象也无法访问private[this]成员
    // println(s"object password=${p.password}")
    p.print(p)
  }
}

1.2.5 构造器参数的访问权限 ​

Scala 主构造器的参数可直接添加访问控制符,控制参数的访问权限,对应课程源码Person0602.scala:

scala
// 主构造器参数直接添加访问控制符
class Person0602(
                  protected val name: String,  // protected:子类可访问
                  private val age: Int,        // private:仅本类/伴生对象可访问
                  val city: String,             // 默认public:全局可访问
                  private[this] val weighs: Int // 对象私有:仅当前对象可访问
                )

class p0602 extends Person0602("yangmi", 30, "shanghai", 120) {
  // 合法,子类可访问protected的name
  println(s"protected name=${this.name}")
  // 非法,子类无法访问private的age
  // println(s"private age=${this.age}")
}

object Person0602 {
  def main(args: Array[String]): Unit = {
    val p = new Person0602("suyoupeng", 20, "beijing", 150)
    println(s"private age=${p.age}") // 合法,伴生对象可访问private成员
    // println(s"private weighs=${p.weighs}") // 非法,伴生对象无法访问private[this]成员
  }
}

1.3 包级作用域访问控制 ​

Scala 支持通过private[包名]/protected[包名]实现包级别的作用域控制,可灵活指定成员在哪些包内可访问,对应课程源码Person0603.scala。

1.3.1 private [包名] 包私有 ​

语法:private[包名] 成员,表示该成员仅能在本类、指定的包及其子包内访问,突破了 private 的限制,又比 public 更安全。

scala
package scala06

class Person0603 {
  // private[scala06]:仅scala06包及其子包内可访问
  private[scala06] def print3(): Unit = {
    println("private[scala06]")
  }

  // private:仅本类/伴生对象可访问
  private def print1(): Unit = {
    println("private")
  }

  // private[this]:仅当前对象可访问
  private[this] def print2(): Unit = {
    println("private[this]")
  }
}

object Person0603 {
  def main(args: Array[String]): Unit = {
    val p3 = new Person0603()
    p3.print1() // 合法,伴生对象可访问private
    // p3.print2() // 非法,无法访问private[this]
    p3.print3() // 合法,同包内可访问private[scala06]
  }
}

// 嵌套包定义,演示跨包访问
package com.a.b.c {
  class Foo {
    private[c] def c(): Unit = println("c") // 仅c包内可访问
    private[b] def b(): Unit = println("b") // 仅b包及其子包可访问
    private[a] def a(): Unit = println("a") // 仅a包及其子包可访问
  }
}

// b包的子包c1,可访问private[b]和private[a]
package com.a.b.c1 {
  import com.a.b.c._
  class Bar {
    val f = new Foo()
    // f.c() // 非法,不在c包内
    f.b() // 合法,在b包的子包内
    f.a() // 合法,在a包的子包内
  }
}

// a包的子包b1,仅可访问private[a]
package com.a.b1 {
  import com.a.b.c._
  class Bar {
    val f = new Foo()
    // f.c() // 非法
    // f.b() // 非法,不在b包内
    f.a() // 合法,在a包的子包内
  }
}

1.3.2 protected [包名] 包保护 ​

语法:protected[包名] 成员,表示该成员仅能在本类、子类、指定的包及其子包内访问,比private[包名]多了子类的访问权限。


第二章 Scala 包与导入 ​

Scala 的包用于解决类的命名冲突、代码的模块化管理,与 Java 的包核心思想一致,但提供了更灵活的定义方式、更强大的导入特性,对应课程源码RunPackage.scala、RunImport.scala。

2.1 Scala 包的定义 ​

Scala 提供两种包的定义方式,比 Java 更灵活,支持嵌套包定义与作用域控制。

2.1.1 方式 1:文件顶部声明(与 Java 一致) ​

在 Scala 文件的第一行通过package 包名声明包,文件内的所有类 / 对象都属于该包,与 Java 完全一致。

scala
// 文件顶部声明包,与Java一致
package scala06

// 该类属于scala06包
class RunPackage {
  // 类内容
}

2.1.2 方式 2:花括号嵌套声明(Scala 独有) ​

通过package 包名 { }的方式嵌套定义包,可在同一个文件中定义多个包,精准控制包的作用域,这是 Scala 独有的特性。

scala
// 嵌套定义com.acme.store包
package com.acme.store {
  class Foo0601 {
    override def toString = s"Foo0601() is com.acme.store.Foo0601"
  }
}

// 嵌套定义order包
package order {
  class Foo0601 {
    override def toString = s"Foo0601() is order.Foo0601"
  }

  // 嵌套子包order.customer
  package customer {
    class Foo0601 {
      override def toString = s"Foo0601() is order.customer.Foo0601"
    }
  }
}

object RunPackage {
  def main(args: Array[String]): Unit = {
    // 不同包的同名类,通过全限定名区分,解决命名冲突
    println(new com.acme.store.Foo0601().toString)
    println(new order.Foo0601().toString)
    println(new order.customer.Foo0601().toString)
  }
}

2.1.3 包的声明位置 ​

Scala 的包声明不局限于文件顶部,可在代码的任意位置声明,而 Java 的包声明只能在文件的第一行(注释除外)。

2.2 Scala 包的导入特性 ​

Scala 通过import关键字导入包 / 类,核心语法与 Java 一致,但提供了更强大、更灵活的导入特性,可在代码的任意位置使用 import 语句(Java 只能在文件顶部、类定义之前使用 import),对应课程源码RunImport.scala。

2.2.1 基础导入 ​

scala
// 导入单个类
import java.io.File
// 导入同一个包的多个类
import java.io.{File, BufferedWriter}
// 导入包内所有类(Scala用_,Java用*)
import java.nio._

注意:Scala 中导入包内所有类用通配符_,而 Java 用*,因为 Scala 中*是合法的标识符(用于方法名、运算符)。

2.2.2 导入重命名(解决同名类冲突) ​

当导入不同包的同名类时,可通过import {原类名=>新别名}的方式给类重命名,解决命名冲突,这是 Scala 导入的核心特性之一。

scala
// 导入Java的ArrayList,重命名为javaList
import java.util.{ArrayList => javaList}
// 导入Scala的List
import scala.collection.immutable.List

object PrintCollection {
  def main(args: Array[String]): Unit = {
    // 使用别名创建Java的ArrayList,避免与Scala的List冲突
    val javaArrayList = new javaList
    val scalaList = List(1,2,3)
  }
}

2.2.3 导入隐藏(排除指定类) ​

可通过import {类名=>_, _}的方式,导入包内除指定类之外的所有类,排除不需要的类,避免命名冲突。

scala
// 导入java.util包内所有类,排除Random类
import java.util.{Random => _, _}

object Test {
  def main(args: Array[String]): Unit = {
    // 此时使用的Random是scala.util.Random,而非java.util.Random
    val r = new scala.util.Random()
  }
}

2.2.4 局部导入 ​

Scala 的import语句可放在代码的任意位置,包括方法内部、代码块内部,称为局部导入,导入的类仅在当前作用域内有效,避免全局命名污染。

scala
object RunImport {
  def main(args: Array[String]): Unit = {
    // 方法内部局部导入,仅在main方法内有效
    import scala.util.Random
    val r = new Random()
    println(s"RunImport=${r.nextInt(10)}")
  }
}

2.3 包对象 package object ​

这是一个非常有特色的功能。如果你想让整个包共享一些常量或方法,Java 通常需要建一个 Constants 类,而 Scala 使用 Package Object。

包对象是 Scala 的特殊特性,用于在包级别定义全局的函数、变量、常量,解决了 Java 中只能在类中定义静态成员的限制。

  • 每个包只能有一个包对象,包对象必须与包同名,定义在package.scala文件中;
  • 包对象中定义的成员,在该包内可直接访问,无需通过类名调用;
  • 对应作业中的问题:Scala 设计者提供 package object,而非直接在包中添加函数 / 变量,是因为 JVM 的包本身不能包含成员,只能通过包对象这种语法糖,在 JVM 的规则内实现包级别的全局成员。

包对象定义示例 ​

scala
// 文件名必须为package.scala
package cn.scala

// 包对象,与包cn.scala同名
package object Utils {
  // 包级别的全局函数
  def toString(x: String): Unit = {
    println(s"Utils=$x")
  }

  // 包级别的全局常量
  val DEFAULT_VERSION = "2.12.18"
}

// 包内的类,可直接访问包对象的成员
package person {
  class Teacher(var name: String) {
    def printName(): Unit = {
      // 直接调用包对象的函数,无需导入
      toString(name)
    }
  }
}

// 测试
object packageDemo {
  def main(args: Array[String]): Unit = {
    // 外部包访问,通过全限定名调用
    cn.scala.Utils.toString("scala")
    new cn.scala.person.Teacher("teacher").printName()
  }
}

💡 总结与对比 ​

特性JavaScala
位置必须在文件顶部随处可见(局部作用域)
通配符*_
重命名不支持支持 (=>)
静态导入import static直接 import Object._
包共享只能通过静态类Package Object

在实际开发中,如果你不确定用什么,就保持默认(Public)。如果你在写工具库,希望某些方法只对当前包内的代码开放,那么 private[yourPackage] 是最优雅的选择。


第三章 Scala 集合体系 ​

Scala 集合是 Spark/Flink 大数据开发的核心基础,Spark RDD 的所有算子设计都完全对标 Scala 集合的高阶函数,掌握 Scala 集合是学习 Spark 的前提。

3.1 Scala 集合整体概述 ​

3.1.1 与 Java 集合的核心差异 ​

  1. 体系整合:Java 中Map是独立的体系,不继承自Collection;Scala 中Map、Set、Seq都继承自统一的Iterable特质,属于同一个集合体系;
  2. 可变与不可变双分支:Scala 集合分为不可变集合和可变集合两大分支,Java 集合仅提供可变集合,不可变集合需第三方库实现;
  3. 函数式编程支持:Scala 所有集合都内置了map、flatMap、filter、reduce等高阶函数,Java8 之前无 Lambda 支持,集合操作繁琐;
  4. 默认不可变:Scala 默认使用不可变集合,无需手动声明,而 Java 默认是可变集合。

3.1.2 集合的三大顶级包 ​

Scala 所有集合都来自三个包,核心分为不可变和可变两大分支:

包名特性说明
scala.collection.immutable不可变集合集合一旦创建,内容永远不可修改;修改操作会返回一个全新的集合,原集合保持不变;Scala 默认导入,是默认使用的集合
scala.collection.mutable可变集合集合创建后,可在原集合上修改内容,不会生成新集合;需手动导入使用
scala.collection通用集合包含了可变和不可变集合的通用特质 / 抽象类,定义了集合的通用规范

大数据场景核心原则:优先使用不可变集合,不可变集合线程安全,无副作用,完美适配 Spark 的分布式不可变 RDD 模型,避免并发修改带来的问题。

3.2 集合的核心继承层级 ​

Scala 集合的顶级父特质是Iterable(可迭代),所有集合都继承自该特质,核心分为三大分支:Seq、Set、Map。

92a9c8a443d35ace3cb252282b7b504

plaintext
Iterable(所有可迭代集合的顶级父特质)
├─ Seq(有序、可重复的序列,对应Java的List)
│  ├─ IndexedSeq(索引序列,支持随机访问,如Vector、Array)
│  └─ LinearSeq(线性序列,支持顺序访问,如List、LazyList)
├─ Set(无序、不可重复的集合,与Java的Set一致)
│  ├─ SortedSet(排序集合,如TreeSet)
│  └─ HashSet(哈希集合)
└─ Map(键值对映射,与Java的Map一致)
   ├─ SortedMap(排序映射,如TreeMap)
   └─ HashMap(哈希映射)

3.2.1 Seq 序列 ​

  • 核心特性:元素有序、可重复,每个元素都有对应的索引(从 0 开始);

  • 常用实现类:

    • 不可变:List、Vector、Range、String(Scala 中 String 可直接当作 Char 序列);
    • 可变:ArrayBuffer、ListBuffer、StringBuilder、Queue、Stack。

3.2.2 Set 集合 ​

  • 核心特性:元素无序、不可重复,自动去重;

  • 常用实现类:

    • 不可变:HashSet、TreeSet、BitSet;
    • 可变:HashSet、LinkedHashSet、SortedSet。

3.2.3 Map 映射 ​

  • 核心特性:键值对 (KV) 存储,键不可重复,值可重复;

  • 常用实现类:

    • 不可变:HashMap、TreeMap、ListMap;
    • 可变:HashMap、LinkedHashMap、TreeMap、MultiMap。

scala.collection

image-20250120014009577

scala.collection.mutable

image-20250120014032567

scala.collection.immutable

image-20250120014043340

Scala的集合的一致性 ​

在 Scala 中,集合的**一致性(Consistency/Uniformity)**是一个非常高级且优雅的设计理念。它确保了无论你使用哪种集合(List, Set, Map),它们的操作逻辑、返回类型以及在可变/不可变之间的转换都遵循一套统一的原则。


1. 层次结构的一致性 (Hierarchy) ​

Scala 的集合框架分为三个主要层级:Seq(序列)、Set(集)和 Map(映射)。

  • 统一根接口:所有的集合都继承自 Iterable。这意味着无论什么集合,你都可以使用 foreach、iterator 或 for-yield 进行遍历。
  • 镜像包结构:Scala 同时提供了 scala.collection.immutable(默认)和 scala.collection.mutable。这两个包的结构几乎是镜像对称的。
    • 例如:你在不可变包里有 Map,在可变包里也有一个对应的 Map,它们拥有大部分相同的方法名。

2. 操作方法的一致性 (Uniform Operations) ​

这是 Scala 最强大的地方。无论你面对的是哪种集合,处理数据的方法几乎是完全通用的。

核心方法三剑客:

  1. 转化操作:map, flatMap, collect
  2. 过滤操作:filter, take, drop, distinct
  3. 聚合操作:reduce, fold, sum, count
scala
val list = List(1, 2, 3)
val set = Set(1, 2, 3)
val map = Map(1 -> "one", 2 -> "two")

// 无论什么容器,用法完全一致
list.map(_ * 2) 
set.map(_ * 2)
map.map { case (k, v) => (k, v.toUpperCase) }

3. 返回类型的一致性 (Same-Result-Type Principle) ​

Scala 遵循一个重要原则:对某种集合进行转换操作,返回的依然是同类型的集合。

  • 如果你对 List 进行 filter,结果一定是 List。
  • 如果你对 Set 进行 map,结果一定是 Set(且会自动去重)。
  • 如果你对 BitSet(位集)进行操作,结果依然是 BitSet。

这种一致性是通过底层复杂的 CanBuildFrom(Scala 2.12以前)或 BuildFrom(Scala 2.13+)机制实现的,它保证了类型信息的最高精确度。


4. 转换的一致性 (Conversion) ​

在可变(Mutable)与不可变(Immutable)之间切换,或者在不同容器间切换,Scala 提供了高度一致的 API:

scala
val immList = List(1, 2, 3)

// 转换到可变版本
val mutList = immList.toBuffer 

// 在不同类型间无缝转换
val set = immList.toSet
val array = immList.toArray
val immutableList = mutList.toList

5. 性能特征的一致性 (Performance Characteristics) ​

Scala 官方文档为所有集合提供了一致的性能指标表(Time Complexity)。开发者可以预知:

  • List 的头部操作始终是 $O(1)$,随机访问是 $O(n)$。
  • Vector 的所有操作几乎都是“近似 $O(1)$”($O(log_{32}n)$)。

这种性能表现的一致性使得在编写高性能代码时,选择合适的集合变得非常直观。

3.3 不可变集合与可变集合实战 ​

对应课程源码RunColletion.scala,演示不可变集合与可变集合的核心差异,以及数字拆解去重的实战案例。

3.3.1 不可变集合 ​

不可变集合的修改操作,不会改变原集合,只会返回一个新的集合,原集合永远保持不变,线程安全,无副作用。

scala
object RunColletion {
  def main(args: Array[String]): Unit = {
    // 不可变Set,默认使用immutable.Set
    val c1 = Set(1)
    // +操作返回新的Set,原集合c1保持不变
    val c2 = c1 + 3
    println(s"c1=${c1},c2=${c2}") // 输出:c1=Set(1),c2=Set(1,3)

    // 不可变Vector
    var v1 = Vector("v1")
    // v1(0) = "v2" // 非法,不可变集合无法修改元素
  }
}

3.3.2 可变集合 ​

可变集合的修改操作,直接在原集合上进行,不会生成新的集合,性能更高,但非线程安全,需手动控制并发。

scala
object RunColletion {
  def main(args: Array[String]): Unit = {
    // 导入可变Set
    val mutableset = scala.collection.mutable.Set(5)
    // add方法直接修改原集合,返回是否添加成功
    mutableset.add(3)
    println(s"mutableset=${mutableset}") // 输出:mutableset=Set(3,5)
  }
}

3.3.3 实战案例:数字拆解去重 ​

课程案例:编写方法,将一个整数的每一位数字拆解,放入 Set 中自动去重,返回无重复的数字集合。

scala
object RunColletion {
  def main(args: Array[String]): Unit = {
    val reslut = dig(1234576311)
    println(reslut) // 输出:Set(1,2,3,4,5,6,7)
  }

  // 递归拆解数字,返回无重复的数字集合
  def dig(n: Int): Set[Int] = {
    if (n < 0) dig(-n) // 处理负数
    else if (n < 10) Set(n) // 个位数,直接返回单元素Set
    else dig(n / 10) + (n % 10) // 递归拆解高位,加上当前个位数字
  }
}

第四章 Scala 迭代器 ​

Scala 迭代器Iterator不是一个集合,而是一种访问集合的方法,提供了逐个遍历集合元素的能力,核心价值在于懒加载、不一次性加载所有数据到内存,是大数据海量数据处理的核心工具。

大数据场景应用:处理超大文件、海量数据集时,直接用集合会一次性加载所有数据到内存,导致 OOM 内存溢出;而迭代器是逐条读取数据,内存中仅保留当前元素,完美解决海量数据的遍历问题,Spark 的分区数据遍历底层就是基于迭代器实现。

4.1 迭代器的核心特性 ​

  1. 一次性遍历:迭代器遍历完成后,指针会移动到末尾,无法重复遍历,再次调用next()会抛出异常;

  2. 懒加载:迭代器不会提前加载所有元素,只有调用next()时才会加载下一个元素,内存占用极低;

  3. 两个核心抽象方法:

    • hasNext():判断是否还有下一个元素,返回 Boolean;
    • next():获取下一个元素,指针向后移动一位。

4.2 迭代器的基础使用 ​

对应课程源码RunIterator.scala,演示迭代器的创建、遍历与核心特性。

4.2.1 迭代器的创建 ​

scala
object RunIterator {
  def main(args: Array[String]): Unit = {
    // 方式1:直接创建迭代器,传入多个元素
    val it2 = Iterator(1, 2, 3)
    // 方式2:将Range转为迭代器,_*表示将Range展开为可变参数
    val it3 = Iterator(1 to 5: _*)
  }
}

4.2.2 迭代器的遍历 ​

Scala 提供两种遍历迭代器的方式:while循环(最常用)和for循环。

scala
object RunIterator {
  def main(args: Array[String]): Unit = {
    val it3 = Iterator(1 to 5: _*)

    // 方式1:while循环遍历(推荐,大数据场景最常用)
    while (it3.hasNext) {
      println(it3.next())
    }

    // 方式2:for循环遍历
    val it4 = Iterator(1 to 3: _*)
    for (i <- it4) {
      println(s"i=$i")
    }

    // 核心特性:迭代器遍历一次后失效,无法重复遍历
    // 第二次遍历it4,无任何输出,因为指针已到末尾
    it4.foreach(x => println(x))
  }
}

4.3 迭代器的常用方法 ​

对应课程源码RunIterator.scala,演示迭代器的高频常用方法,这些方法与集合的高阶函数用法一致,且保持懒加载特性。

4.3.1 duplicate 复制迭代器 ​

duplicate方法会生成两个完全相同的新迭代器,可分别独立遍历,解决迭代器无法重复遍历的问题。

scala
object RunIterator {
  def main(args: Array[String]): Unit = {
    val it5 = Iterator(6 to 8: _*)
    // 复制为两个独立的迭代器
    val (it51, it52) = it5.duplicate
    
    // 遍历第一个迭代器
    it51.foreach(x => println(x)) // 输出6、7、8
    // 遍历第二个迭代器,不受第一个影响
    it52.foreach(x => println(x)) // 输出6、7、8
  }
}

4.3.2 take 取前 n 个元素 ​

take(n)方法返回一个包含前 n 个元素的新迭代器,原迭代器的指针会移动 n 位。

scala
object RunIterator {
  def main(args: Array[String]): Unit = {
    val it6 = Iterator(9 to 12: _*)
    // 取前2个元素
    val it61 = it6.take(2)
    it61.foreach(x => println("it61=" + x)) // 输出9、10
    // 原迭代器剩余元素:11、12
    it6.foreach(x => println("it6=" + x)) // 输出11、12
  }
}

4.3.3 slice 切片 ​

slice(from, to)方法返回一个从 from 索引开始、到 to 索引结束(不包含 to)的新迭代器,用于截取迭代器的指定区间。

scala
object RunIterator {
  def main(args: Array[String]): Unit = {
    val it7 = Iterator(13 to 18: _*)
    // 截取索引1到10的元素,实际只有13(0)、14(1)~18(5)
    val it71 = it7.slice(1, 10)
    it71.foreach(x => println("it71=" + x)) // 输出14、15、16、17、18
  }
}

4.3.4 zip/zipAll 拉链 ​

zip方法将两个迭代器的元素配对,生成键值对迭代器,以最短的迭代器为准;zipAll可指定默认值,补齐长度不一致的情况。

scala
object RunIterator {
  def main(args: Array[String]): Unit = {
    val it8k2 = Iterator("k1") // 键迭代器,1个元素
    val it8v1 = Iterator("v1", "v2") // 值迭代器,2个元素

    // zipAll:指定键的默认值default1,值的默认值default2
    val k2_v1 = it8k2.zipAll(it8v1, "default1", "default2")
    k2_v1.foreach(x => println(x))
    // 输出:
    // (k1,v1)
    // (default1,v2)
  }
}

第五章 课后作业全解析 ​

对应课程配套作业文档,包含简答题与编程题全解析,所有代码可直接复制运行。

一、简答题 ​

(1)Scala 有哪些控制符? ​

答:Scala 提供了 6 种访问控制符,按权限从大到小排序如下,同时明确了各控制符的作用域:

  1. 无修饰符(默认访问权限):等同于 Java 的public,全局任何位置均可访问,Scala 无public关键字,无修饰符即为默认公开权限。
  2. protected [包名]:包级保护权限,本类、子类、指定包及其子包内均可访问。
  3. private [包名]:包级私有权限,本类、指定包及其子包内均可访问,子类无访问权限。
  4. protected:仅本类、子类、伴生对象可访问,同包内其他类无法访问,比 Java 的protected权限更严格。
  5. private:仅本类及其伴生对象可访问,子类、外部类均无法访问,与 Java 的核心差异是允许伴生对象访问。
  6. private[this]:对象级私有,Scala 独有的最严格权限,仅当前对象内部可访问,同一个类的其他对象、伴生对象均无法访问。

(2)Scala 包都可以在哪些位置声明,有哪些访问权限? ​

答:

1. Scala 包的声明位置 ​

Scala 包的声明比 Java 灵活,支持两个位置声明:

  • 文件顶部声明:与 Java 一致,在 Scala 文件的第一行(注释除外)通过package 包名声明,文件内所有类 / 对象都属于该包;
  • 代码任意位置嵌套声明:Scala 独有特性,通过package 包名 { }的花括号语法,可在代码的任意位置嵌套声明包,支持多层子包嵌套,同一个文件中可定义多个不同的包。
2. Scala 包的访问权限控制 ​

Scala 通过包级访问控制符,精准控制成员在包内的访问权限,核心有两种:

  • private [包名]:包级私有,成员仅能在本类、指定的包及其子包内访问,其他包无法访问;
  • protected [包名]:包级保护,成员仅能在本类、子类、指定的包及其子包内访问,比 private [包名] 多了子类的访问权限;
  • 无修饰符的成员,在所有包内均可访问;private/protected 无包名限定的成员,仅能在本类 / 子类中访问,跨包无法访问。

(3)在你看来 Scala 的设计者为什么要提供 package object 语法而不是简单地让你将函数和变量添加到包中呢? ​

答:核心原因是Scala 需要兼容 JVM 的底层规范,同时实现包级别的全局函数 / 变量,具体分析如下:

  1. JVM 的底层限制:JVM 的虚拟机规范中,包本身是一个命名空间,不能包含任何成员(函数、变量、常量),所有的成员必须定义在类 / 接口中。Java 中只能通过类的静态成员实现类似全局的功能,无法直接在包中添加函数和变量。
  2. package object 的语法糖作用:Scala 的 package object 是 JVM 规范之上的语法糖,编译器会将包对象编译为一个特殊的类,包对象中定义的函数 / 变量会被编译为该类的静态成员,从而在 JVM 规则内实现了包级别的全局成员。
  3. 简化代码,提升开发效率:在包对象中定义的函数 / 变量,在该包内可直接访问,无需通过类名调用,避免了 Java 中必须通过工具类名.方法名的繁琐写法,更符合函数式编程的习惯。
  4. 统一包级别的工具方法 / 常量:package object 为整个包提供了统一的全局工具方法、常量、类型别名,无需在每个类中重复定义,实现了包级别的代码复用。

二、编程题 ​

(1)编写一个包 random,加入函数:nextInt ():Int,nextDouble ():Double,setSeed (seed:Int):Unit。生成随机数的算法采用线性同余生成器:后值 = (前值 * a + b) mod 2^n,其中,a = 1664525,b=1013904223,n = 32,前值的初始值为 seed。 ​

scala
// 文件名:package.scala,必须放在random包对应的目录下
package com.scala

// 包对象random
package object random {
  // 算法常量
  private val a: Long = 1664525L
  private val b: Long = 1013904223L
  private val n: Int = 32
  // 2的n次方,用于取模
  private val modValue: Long = 1L << n // 2^32

  // 种子,初始值可通过setSeed设置
  private var seed: Long = 1L

  /**
   * 设置随机数种子
   * @param seed 初始种子
   */
  def setSeed(seed: Int): Unit = {
    this.seed = seed.toLong
  }

  /**
   * 生成下一个随机Int值
   * @return 随机Int整数
   */
  def nextInt(): Int = {
    // 线性同余公式计算下一个种子
    seed = (seed * a + b) % modValue
    // 转为Int返回
    seed.toInt
  }

  /**
   * 生成下一个随机Double值,范围[0.0, 1.0)
   * @return 随机Double小数
   */
  def nextDouble(): Double = {
    // 先获取随机Int,转为0~2^32之间的无符号数,再除以2^32,得到0~1之间的小数
    val next = nextInt().toLong & 0xFFFFFFFFL // 转为无符号32位整数
    next.toDouble / modValue.toDouble
  }
}

// 测试类
object RandomTest {
  def main(args: Array[String]): Unit = {
    // 导入random包的所有函数
    import com.scala.random._

    // 设置种子
    setSeed(123456)
    println("===== 随机Int =====")
    for (_ <- 1 to 5) {
      println(nextInt())
    }

    println("\n===== 随机Double =====")
    for (_ <- 1 to 5) {
      println(nextDouble())
    }

    // 重新设置种子,验证可复现
    println("\n===== 重置种子后 =====")
    setSeed(123456)
    println(nextInt()) // 与第一次的第一个Int值一致
  }
}

(2)编写一段程序,将 Java 哈希映射中的所有元素拷贝到 Scala 哈希映射。用引入语句重命名这两个类。 ​

scala
import java.util.{HashMap => JavaHashMap} // Java HashMap重命名为JavaHashMap
import scala.collection.mutable.{HashMap => ScalaHashMap} // Scala可变HashMap重命名为ScalaHashMap

object HashMapCopy {
  def main(args: Array[String]): Unit = {
    // 1. 创建Java的HashMap,并添加元素
    val javaMap = new JavaHashMap
    javaMap.put("java", 1)
    javaMap.put("scala", 2)
    javaMap.put("spark", 3)
    javaMap.put("flink", 4)
    println(s"Java HashMap: $javaMap")

    // 2. 创建Scala的HashMap
    val scalaMap = new ScalaHashMap

    // 3. 遍历Java HashMap,拷贝所有元素到Scala HashMap
    // 方式1:迭代器遍历
    val entryIterator = javaMap.entrySet().iterator()
    while (entryIterator.hasNext) {
      val entry = entryIterator.next()
      scalaMap.put(entry.getKey, entry.getValue)
    }

    // 方式2:Scala函数式写法(更简洁)
    // import scala.collection.JavaConverters._
    // javaMap.asScala.foreach { case (k, v) => scalaMap.put(k, v) }

    // 4. 打印结果,验证拷贝完成
    println(s"Scala HashMap: $scalaMap")
  }
}

学习总结 ​

  1. Scala 访问权限:Scala 提供了比 Java 更精细的访问控制,核心是取消了public关键字,新增了private[this]对象私有和private[包名]包级私有,实现了更灵活的封装控制,编写 Scala 代码时应遵循最小权限原则,优先使用严格的访问控制符。
  2. Scala 包与导入:Scala 支持嵌套包定义、任意位置导入、导入重命名、导入隐藏等高级特性,完美解决了不同包同名类的冲突问题;包对象package object实现了包级别的全局成员,是 Scala 对 JVM 规范的优雅扩展。
  3. Scala 集合体系:Scala 集合分为不可变和可变两大分支,默认使用不可变集合,线程安全、无副作用,完美适配 Spark 的分布式不可变 RDD 模型;集合分为Seq、Set、Map三大分支,内置了丰富的高阶函数,是函数式编程的核心载体。
  4. Scala 迭代器:迭代器不是集合,而是访问集合的工具,核心特性是懒加载、一次性遍历、低内存占用,是大数据场景下处理海量数据、超大文件的核心工具,避免了一次性加载数据导致的 OOM 内存溢出,Spark 分区数据的底层遍历完全基于迭代器实现。
  5. 大数据开发适配:本节内容是 Spark 开发的前置核心基础,Spark RDD 的算子设计完全对标 Scala 集合的高阶函数,RDD 的不可变特性与 Scala 不可变集合完全一致,掌握本节内容,后续学习 Spark 会事半功倍。

下一章预习内容:Scala 常见集合操作。

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