Skip to content

3 内建控制、类与对象 ​

笔记衔接前序《Scala 数据类型与基本运算》,严格匹配课程体系,针对有 Java 基础的大数据专业大学生设计,全程Java 语法对比 + 可运行代码案例 + 课堂案例拆解 + 避坑指南 + 作业全解析,重点贴合 Spark/Flink 大数据开发的高频语法,所有案例均来自课程配套源码,可直接复制运行。

  • 前置知识:Scala 基础语法、val/var 变量、Java 面向对象基础

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

  • 核心学习目标:

    1. 掌握 IDEA+Scala 开发环境搭建,完成本地开发环境配置
  1. 熟练使用 Scala 内建控制结构,重点掌握 if 表达式、for 循环的 Scala 特色用法
  2. 彻底掌握 Scala 类的定义、主构造器与辅助构造器,明确与 Java 构造器的核心差异
  3. 理解 Scala 伴生类与伴生对象的设计思想,掌握其在大数据开发中的高频应用
  4. 掌握 Scala 对象的类型判断与相等性规则,规避新手高频踩坑点

第一章 IDEA + Scala 开发环境搭建 ​

Scala 的开发主要基于 IDEA 完成,大数据开发中 Spark/Flink 的项目也均基于 IDEA 开发,本节详细讲解环境搭建全流程,与课程 PPT 完全匹配。

1.1 环境准备 ​

搭建前必须准备好以下基础环境:

  1. JDK1.8+:大数据场景统一使用 JDK1.8,保证与 Spark/Flink 的兼容性
  2. Scala2.12.x:课程指定版本,Spark3.x、Flink1.15 + 均主流兼容该版本
  3. IDEA:社区版 / 旗舰版均可,建议 2019.2 及以上版本

1.2 Scala 插件安装 ​

Scala 插件是 IDEA 支持 Scala 开发的核心,提供代码高亮、语法检查、编译运行等能力,支持在线安装和离线安装两种方式。

方式 1:在线安装(推荐,网络良好时使用) ​

  1. 打开 IDEA,进入Settings(快捷键Ctrl+Alt+S)
  2. 选择Plugins → Marketplace,在搜索框输入Scala
  3. 找到 JetBrains 官方出品的 Scala 插件,点击Install安装
  4. 安装完成后,点击Restart IDE重启 IDEA,插件生效

方式 2:离线安装(网络受限场景使用) ​

  1. 提前下载插件离线包:官方下载地址 http://plugins.jetbrains.com/plugin/1347-scala/versions,选择与 IDEA 版本匹配的插件版本
  2. 打开 IDEA,进入Settings → Plugins
  3. 点击右上角齿轮图标 → 选择Install Plugin from Disk
  4. 选择下载好的插件离线包,完成安装后重启 IDEA 即可

1.3 IDEA 项目与 SDK 配置 ​

步骤 1:创建 Scala 项目 ​

  1. 打开 IDEA,点击New Project
  2. 选择Scala → IDEA,点击 Next
  3. 填写项目名称、项目路径,选择 JDK1.8 版本
  4. 点击Finish,完成项目创建

步骤 2:配置 Scala SDK ​

  1. 进入项目后,右键项目名 → 选择Open Module Settings(快捷键Ctrl+Alt+Shift+S)
  2. 选择Libraries → 点击+号 → 选择Scala SDK
  3. 选择本地安装的 Scala2.12.x 版本,点击OK,完成 SDK 配置
  4. 配置完成后,即可在项目中创建 Scala 类 / 对象,编写并运行 Scala 代码

第二章 Scala 内建控制结构 ​

Scala 的内建控制结构分为顺序结构、分支结构、循环结构三大类,语法与 Java 大体相似,但有几个核心的 Scala 特色特性,是大数据开发中高频使用的语法,必须重点掌握。

2.1 顺序结构 ​

顺序结构是程序最基础的执行结构,程序按照代码从上到下的顺序依次执行,中间无任何判断和跳转,与 Java 完全一致。

scala
object SequenceDemo {
  def main(args: Array[String]): Unit = {
    // 从上到下依次执行
    val name = "scala"
    val age = 10
    println("hello scala")
    println(s"$name has been $age years old")
  }
}

2.2 分支结构:if 表达式(核心重点) ​

Scala 的 if 语句与 Java 的核心差异:Scala 的 if 表达式是有返回值的,可以直接将 if 表达式的结果赋值给变量,而 Java 的 if 语句没有返回值。这是 Scala 函数式编程的基础特性,大数据开发中常用于数据清洗的条件判断。

2.2.1 基础语法 ​

scala
// 标准if-else语法,与Java一致
if(布尔表达式1){
  // 表达式1为true时执行
}else if(布尔表达式2){
  // 表达式2为true时执行
}else{
  // 所有条件都为false时执行
}

2.2.2 带返回值的 if 表达式 ​

Scala 中,if 表达式的返回值为满足条件的代码块中最后一行表达式的值,可直接赋值给变量。

scala
object IfDemo {
  def main(args: Array[String]): Unit = {
    val age = 18
    // if表达式的返回值直接赋值给变量
    val ageType = if (age < 12) {
      "童年"
    } else if (age >= 12 && age < 30) {
      "青年"
    } else if (age >= 30 && age < 40) {
      "中年"
    } else if (age >= 40 && age < 50) {
      "壮年"
    } else {
      "老年"
    }
    println(s"年龄段:$ageType") // 输出:青年
  }
}

课程案例对应:ClassifyPerson.scala,实现了根据年龄划分人群的完整逻辑。

2.2.3 极简写法 ​

如果 if 代码块只有一行语句,可省略花括号,代码更简洁:

scala
val num = 10
val result = if (num > 0) "正数" else "负数"

2.3 循环结构 ​

Scala 提供while、do-while、for三种循环结构,其中for 循环是 Scala 的特色,也是大数据开发中最常用的循环结构,必须重点掌握。

2.3.1 while 循环 ​

Scala 的 while 循环语法与 Java 基本一致,有 2 个核心差异:

  1. while/do-while 循环的返回值是Unit(即()),而非 Java 的无返回值;
  2. Scala 不支持在 while 条件中使用赋值语句,Java 中常用的while((a = getName()) != null)写法在 Scala 中会编译报错。
基础语法 ​
scala
// while循环:先判断条件,再执行循环体
var i = 0
while (i < 5) {
  println(s"i=$i")
  i += 1 // Scala没有++运算符,用+=1替代
}

// do-while循环:先执行一次循环体,再判断条件
var j = 1
do {
  println(s"j=$j")
  j += 1
} while (j < 1) // 条件不满足,仍会执行一次循环体

2.3.2 for 循环(核心重点) ​

Scala 的 for 循环比 Java 的 for 循环功能更强大、语法更简洁,支持范围遍历、集合遍历、卫语句过滤、yield 返回值四大核心能力,是大数据数据处理的核心语法。

1. 基础范围遍历 ​

语法:for(变量 <- 范围),<-称为「生成器」,用于将范围中的每个元素依次赋值给变量。

scala
object ForDemo {
  def main(args: Array[String]): Unit = {
    // 1. 遍历1到5(包含5)
    for (i <- 1 to 5) {
      print(i + " ")
    } // 输出:1 2 3 4 5
    println()

    // 2. 遍历1到5(不包含5)
    for (i <- 1 until 5) {
      print(i + " ")
    } // 输出:1 2 3 4
    println()

    // 3. 遍历集合
    val fruits = List("apple", "banana", "orange", "strawberry")
    for (fruit <- fruits) {
      println(fruit)
    }
  }
}
2. 带卫语句的 for 循环(条件过滤) ​

卫语句即 for 循环中的条件判断,可在遍历中直接过滤元素,语法:for(变量 <- 范围 if 条件1; if 条件2...),多个条件用分号分隔。

大数据场景应用:ETL 数据清洗中,直接在遍历中过滤不符合规则的数据,无需额外的 if 判断嵌套。

scala
object ForGuardDemo {
  def main(args: Array[String]): Unit = {
    val fruits = List("apple", "banana", "orange", "strawberry")
    // 卫语句:只遍历以b开头的水果
    for (fruit <- fruits if fruit.startsWith("b")) {
      println(fruit)
    } // 输出:banana

    // 多条件卫语句:遍历1-10中的偶数,且大于5
    for (i <- 1 to 10 if i % 2 == 0; if i > 5) {
      print(i + " ")
    } // 输出:6 8 10
  }
}
3. 带返回值的 for 循环:yield 关键字 ​

Scala 的 for 循环可通过yield关键字,将每次循环的处理结果收集起来,返回一个新的集合,这是 Java for 循环不具备的能力,大数据开发中常用于数据转换、RDD/DataSet 的元素处理。

语法:for(变量 <- 范围 if 条件) yield 处理逻辑

scala
object ForYieldDemo {
  def main(args: Array[String]): Unit = {
    val fruits = List("apple", "banana", "orange", "strawberry")
    // 遍历以b开头的水果,转换为大写,返回新集合
    val upperFruit = for (fruit <- fruits if fruit.startsWith("b")) yield fruit.toUpperCase
    println(upperFruit) // 输出:List(BANANA)

    // 遍历1-10,对每个偶数乘2,返回新集合
    val numList = for (i <- 1 to 10 if i % 2 == 0) yield i * 2
    println(numList) // 输出:List(4, 8, 12, 16, 20)
  }
}

2.4 课堂案例实战 ​

案例 1:打印 1-100,每行 5 个数字 ​

scala
object PrintNumDemo {
  def main(args: Array[String]): Unit = {
    for (i <- 1 to 100) {
      print(i + "\t")
      // 每5个数字换行
      if (i % 5 == 0) {
        println()
      }
    }
  }
}

案例 2:打印等腰三角形,参数 n 控制行数 ​

scala
object TriangleDemo {
  def main(args: Array[String]): Unit = {
    val n = 4
    for (i <- 1 to n) {
      // 打印空格
      for (_ <- 1 to n - i) {
        print(" ")
      }
      // 打印#号
      for (_ <- 1 to i) {
        print("#")
      }
      // 换行
      println()
    }
  }
}

运行结果(n=4):

plaintext
   #
  ##
 ###
####

第三章 Scala 类与构造器(核心重点) ​

类是面向对象编程的核心,Scala 的类定义与 Java 有显著差异,语法更简洁,构造器体系与 Java 完全不同,是学习 Scala 面向对象的核心难点,本节结合课程配套的Car.scala、Socket.scala、Pig.scala等案例,逐点拆解讲解。

3.1 类的基础定义 ​

Scala 中使用class关键字定义类,与 Java 一致,类中可定义属性、方法、构造器、内部类等成员。

3.1.1 无参类的定义 ​

scala
// 定义一个无参的Person类
class Person {
  // 定义属性
  var name = "person"
  val age = 20
  // 定义方法
  def sayHello(): Unit = {
    println(s"Hello, my name is $name")
  }
}

object PersonTest {
  def main(args: Array[String]): Unit = {
    // 创建对象,使用new关键字
    val p = new Person()
    // 访问属性
    println(p.name)
    // 修改var属性
    p.name = "张三"
    // 调用方法
    p.sayHello()
  }
}

3.1.2 类的属性访问控制(与 Java 核心差异) ​

Scala 类的属性,根据val/var/private关键字的不同,编译器会自动生成对应的 get/set 方法,这是与 Java 最核心的差异:

属性定义生成 get 方法?生成 set 方法?访问权限
var name: String是是外部可访问、可修改
val name: String是否外部可访问、不可修改
private var name: String否否仅类内部可访问、可修改
private val name: String否否仅类内部可访问、不可修改

核心结论:

  1. var修饰的属性,编译器自动生成公有的 getter 和 setter 方法;
  2. val修饰的属性,编译器仅生成公有的 getter 方法,无 setter 方法(不可修改);
  3. private修饰的属性,会阻止 getter/setter 方法的生成,仅类内部可访问。
课程案例对应:Pig 类实现 ​
scala
/**
 * 课程案例:Pig类
 * 要求:
 * 1. name和age既可访问也可修改 → var修饰
 * 2. color可以访问 → val修饰
 * 3. sex不可访问和修改 → private val修饰
 */
class Pig {
  var name = "peiqi"
  var age = 90
  val color = "red"
  private val sex = true // 私有属性,外部无法访问
}

object RunPig {
  def main(args: Array[String]): Unit = {
    val p1 = new Pig()
    // 合法:访问var/val属性
    println(s"name=${p1.name},age=${p1.age},color=${p1.color}")
    // 合法:修改var属性
    p1.age = 100
    // 非法:private属性,外部无法访问
    // println(p1.sex)
  }
}

3.2 主构造器 ​

Scala 的构造器分为主构造器和辅助构造器,这是与 Java 构造器体系最核心的差异。

  • Java 中,类的构造器是多个重载的方法,无主次之分;
  • Scala 中,每个类有且仅有一个主构造器,其他构造器都是辅助构造器,辅助构造器必须直接或间接调用主构造器。

3.2.1 主构造器的定义 ​

Scala 的主构造器直接写在类名后面,与类的定义融合在一起,语法极简,这是 Scala 代码简洁性的核心体现。

语法:class 类名([val/var] 参数名: 类型 [= 默认值], ...) { 类体 }

主构造器参数的 3 种类型 ​
参数定义作用生成 get/set 方法?
val name: String成为类的只读属性,外部可访问生成 get 方法,无 set 方法
var age: Int成为类的可读写属性,外部可访问修改生成 get+set 方法
address: String仅类内部可访问,不生成 get/set 方法不生成
主构造器的执行逻辑 ​

主构造器的代码,就是类体中除了方法定义之外的所有代码,创建对象时,主构造器会依次执行类体中的所有代码。

scala
/**
 * 主构造器示例
 * 主构造器参数:name(只读)、age(可读写)、address(类内部访问)
 */
class Student(val name: String, var age: Int, address: String) {
  // 主构造器执行的代码
  println("主构造器开始执行")
  println(s"学生信息:name=$name, age=$age, address=$address")

  // 定义方法
  def showAddress(): Unit = {
    // 类内部可访问address参数
    println(s"地址:$address")
  }
}

object StudentTest {
  def main(args: Array[String]): Unit = {
    // 创建对象时,直接调用主构造器传参
    val s = new Student("张三", 18, "北京市朝阳区")
    s.showAddress()
  }
}
主构造器参数默认值 ​

Scala 支持给主构造器参数设置默认值,创建对象时可省略带默认值的参数,简化对象创建。

scala
/**
 * 主构造器默认值示例
 * 课程案例:Socket类
 */
class Socket1(
               val sourceIP: String = "127.0.0.1", // 不可修改,默认值127.0.0.1
               var port: Int = 22, // 可修改,默认值22
               var deIp: String = "" // 可修改,默认空字符串
             )

object RunSocket {
  def main(args: Array[String]): Unit = {
    // 使用默认值创建对象
    val sk = new Socket1()
    println(s"sourceIp=${sk.sourceIP},port=${sk.port},deip=${sk.deIp}")
    // 修改可写属性
    sk.port = 60010
    sk.deIp = "192.168.2.1"
    println(s"修改后:sourceIp=${sk.sourceIP},port=${sk.port},deip=${sk.deIp}")
  }
}
私有主构造器 ​

使用private关键字修饰主构造器,可将构造器私有化,外部无法通过new关键字创建对象,常用于单例模式。

语法:class 类名 private (参数列表) { 类体 }

3.3 辅助构造器 ​

Scala 的辅助构造器用于提供多种创建对象的方式,类似于 Java 的构造器重载,有 2 个强制规则(必须遵守,否则编译报错):

  1. 辅助构造器的名称必须为this;
  2. 辅助构造器的第一行代码,必须调用主构造器,或者已经定义好的其他辅助构造器。

3.3.1 辅助构造器基础语法 ​

scala
class User(val name: String, val age: Int) {
  // 辅助构造器1:仅传name,age默认-1
  def this(name: String) {
    this(name, -1) // 第一行必须调用主构造器
  }

  // 辅助构造器2:无参,name默认未知,age默认-1
  def this() {
    this("未知") // 第一行调用已定义的辅助构造器1
  }
}

3.3.2 课程核心案例:Car 类全解析 ​

课程配套的Car.scala是辅助构造器的经典案例,需求如下:

定义 Car 类,提供四组构造器

  1. 只读属性:制造商 producerName、型号 productType、生产年份 carYear
  2. 可读写属性:车牌号 carNum
  3. 所有构造器中,制造商和型号为必填项
  4. 生产年份和车牌号为可选,未设置年份为 - 1,车牌为空字符串
scala
package scala03

/**
 * 课程核心案例:Car类
 * 主构造器:包含所有4个属性
 */
class Car(
           val producerName: String, // val:只读,不可修改
           val productType: String,  // val:只读,不可修改
           val carYear: Int,         // val:只读,不可修改
           var carNum: String        // var:可读写,可修改
         ) {

  /**
   * 辅助构造器1:仅传制造商、型号
   * 年份默认-1,车牌默认空字符串
   */
  def this(producerName: String, productType: String) {
    this(producerName, productType, -1, "空字符串") // 调用主构造器
  }

  /**
   * 辅助构造器2:传制造商、型号、年份
   * 车牌默认空字符串
   */
  def this(producerName: String, productType: String, carYear: Int) {
    this(producerName, productType, carYear, "空字符串") // 调用主构造器
  }

  /**
   * 辅助构造器3:传制造商、型号、车牌号
   * 年份默认-1
   */
  def this(producerName: String, productType: String, carNum: String) {
    this(producerName, productType, -1, carNum) // 调用主构造器
  }

  // 重写toString方法,方便打印对象
  override def toString = s"Car($producerName, $productType, $carYear, $carNum)"
}

/**
 * 伴生对象,测试Car类的构造器
 */
object Car {
  def main(args: Array[String]): Unit = {
    // 调用主构造器
    val car1 = new Car("audi", "Q5", 2008, "888888")
    // 调用辅助构造器1
    val car2 = new Car("audi", "A4")
    // 调用辅助构造器2
    val car3 = new Car("audi", "A6", 2008)
    // 调用辅助构造器3
    val car4 = new Car("audi", "Q3", "666666")

    // 打印结果
    println(s"car1=$car1")
    println(s"car2=$car2")
    println(s"car3=$car3")
    println(s"car4=$car4")
  }
}

运行结果:

plaintext
car1=Car(audi, Q5, 2008, 888888)
car2=Car(audi, A4, -1, 空字符串)
car3=Car(audi, A6, 2008, 空字符串)
car4=Car(audi, Q3, -1, 666666)

第四章 Scala 伴生类与伴生对象(核心重点) ​

Scala 中没有static静态关键字,无法像 Java 一样定义静态属性和静态方法,Scala 通过单例对象 object和伴生类 / 伴生对象来实现 Java 静态成员的功能,这是 Scala 面向对象的核心特色,也是 Spark 源码中最常用的语法,必须重点掌握。

4.1 单例对象 object ​

Scala 中使用object关键字定义单例对象,单例对象在整个程序中只有一个实例,无需通过new关键字创建,直接通过对象名.成员访问,等同于 Java 的单例模式。

4.1.1 单例对象基础定义 ​

scala
/**
 * 单例对象:Single
 * 等同于Java的饿汉式单例模式
 */
object Single {
  // 静态属性
  var name = "scala single"
  // 静态方法
  def printName(): Unit = {
    println(s"name=$name")
  }

  // 程序入口main方法必须定义在单例对象中
  def main(args: Array[String]): Unit = {
    // 直接通过对象名访问,无需new
    Single.printName()
  }
}

4.1.2 与 Java 单例模式的对比 ​

特性Scala 单例对象Java 单例模式
实例创建程序启动时自动创建,饿汉式,线程安全需手动实现,懒汉式需手动保证线程安全
访问方式直接通过对象名访问,无需 getInstance 方法需通过 getInstance () 方法获取实例
语法简洁性极简,一行 object 关键字即可实现繁琐,需私有化构造器、手动控制实例创建

4.2 伴生类与伴生对象 ​

当同名的 class 类和 object 单例对象定义在同一个源码文件中时,这个类称为单例对象的伴生类,这个单例对象称为类的伴生对象。

4.2.1 核心规则 ​

  1. 伴生类和伴生对象必须同名;
  2. 伴生类和伴生对象必须在同一个源码文件中;
  3. 伴生类和伴生对象可以互相访问对方的 private 私有成员,这是 Scala 的特殊权限设计。

4.2.2 核心作用 ​

  1. 替代 Java 的 static 静态成员:伴生对象中定义的属性和方法,等同于 Java 的静态属性和静态方法,直接通过类名.成员访问;
  2. 私有成员互访:伴生类和伴生对象可互相访问对方的私有属性和方法,突破 private 的访问限制;
  3. 简化对象创建:通过重写伴生对象的apply方法,可实现无需new关键字创建对象,是 Spark 源码中最常用的语法。

4.2.3 课程案例:伴生类与伴生对象私有成员互访 ​

scala
/**
 * 伴生类:Student
 */
class Student {
  // 伴生类的私有属性
  private val secrect2 = "i am secrect2 *****"

  // 访问伴生对象的私有属性
  def accessCompanionObject(): Unit = {
    println(s"访问伴生对象的私有属性:${Student.secrect1}")
  }
}

/**
 * 伴生对象:Student,与伴生类同名、同文件
 */
object Student {
  // 伴生对象的私有属性
  private val secrect1 = "i am secrect1 *****"

  // 访问伴生类的私有属性
  def accessCompanionClass(s: Student): Unit = {
    println(s"访问伴生类的私有属性:${s.secrect2}")
  }
}

object Run1 {
  def main(args: Array[String]): Unit = {
    // 伴生类访问伴生对象的私有属性
    new Student().accessCompanionObject()
    // 伴生对象访问伴生类的私有属性
    Student.accessCompanionClass(new Student())
  }
}

4.3 apply 方法:无需 new 创建对象 ​

Scala 中,伴生对象的apply方法是一个特殊方法,当通过类名(参数)的方式创建对象时,Scala 编译器会自动调用伴生对象的apply方法,实现无需new关键字创建对象。

大数据场景应用:Spark 中创建 RDD、DataSet、DataFrame 时,常用的List(1,2,3)、Row(1,"张三",20)等写法,本质都是调用了伴生对象的 apply 方法。

4.3.1 课程案例:Person 类的 apply 方法实现 ​

scala
/**
 * 伴生类:Person
 */
class Person {
  var name = "person"
}

/**
 * 伴生对象:Person,重写apply方法
 */
object Person {
  /**
   * 无参apply方法,返回Person对象
   */
  def apply(): Person = new Person()

  /**
   * 带参apply方法,返回Person对象并给name赋值
   */
  def apply(name: String): Person = {
    val p = new Person()
    p.name = name
    p
  }
}

object RunPerson {
  def main(args: Array[String]): Unit = {
    // 无需new,直接通过Person()创建对象,自动调用apply()
    val p1 = Person("apply person")
    println(s"p1.name = ${p1.name}")

    // 显式调用apply方法,效果一致
    val p2 = Person.apply("apply person2")
    println(s"p2.name = ${p2.name}")
  }
}

4.3.2 课程作业案例:Point 类实现 ​

作业需求:定义一个 Point,使得我们可以不用 new,而是用 Point (3,4) 创建对象,并求横纵坐标的和。

scala
/**
 * 伴生类:Point
 */
class Point(val x: Int, val y: Int) {
  // 求横纵坐标的和
  def sum(): Int = x + y
}

/**
 * 伴生对象:Point,重写apply方法
 */
object Point {
  def apply(x: Int, y: Int): Point = new Point(x, y)
}

object PointTest {
  def main(args: Array[String]): Unit = {
    // 无需new,直接Point(3,4)创建对象
    val p = Point(3, 4)
    println(s"横纵坐标和:${p.sum()}") // 输出:7
  }
}

第五章 Scala 对象的类型判断与相等性 ​

Scala 的对象类型判断、相等性判断规则与 Java 有显著差异,是新手最高频的踩坑点,本节结合课程配套的RunEqual.scala、RunOverwriteEq.scala等案例,详细讲解核心规则。

5.1 对象所属类型判断 ​

Scala 提供两种方式判断对象的所属类型,分别是getClass和isInstanceOf,对应 Java 的getClass()和instanceof关键字。

5.1.1 getClass:精确类型判断 ​

getClass用于获取对象的精确类类型,可判断两个对象是否属于完全相同的类,不考虑继承关系。

  • 语法:对象.getClass == 另一个对象.getClass
  • 特点:精确匹配,子类和父类的 getClass 结果不相等。

5.1.2 isInstanceOf:类型兼容判断 ​

isInstanceOf用于判断对象是否属于指定类型或其子类型,考虑继承关系,等同于 Java 的instanceof关键字。

  • 语法:对象.isInstanceOf[类型名]
  • 特点:兼容继承关系,子类对象 isInstanceOf 父类,返回 true。

5.1.3 课程案例:类型判断实战 ​

scala
class Person2
class Student2 extends Person2
class Teacher2 extends Person2

object RunJudgeObject {
  def main(args: Array[String]): Unit = {
    val p1 = new Person2()
    val p2 = new Person2()
    val s = new Student2()
    val t = new Teacher2()

    // 1. getClass精确判断
    println(s"p1和p2是否同类型:${p1.getClass == p2.getClass}") // true
    println(s"p1和s是否同类型:${p1.getClass == s.getClass}") // false,子类和父类类型不同

    // 2. isInstanceOf兼容判断
    println(s"s是否是Person2类型:${s.isInstanceOf[Person2]}") // true,子类属于父类类型
    println(s"s是否是Student2类型:${s.isInstanceOf[Student2]}") // true
    println(s"p1是否是Student2类型:${p1.isInstanceOf[Student2]}") // false
  }
}

5.2 对象相等性判断(核心重点) ​

Scala 提供 3 种对象相等性判断方式,分别是==/!=、equals、eq/ne,与 Java 的规则完全不同,必须重点区分。

5.2.1 三种相等性判断的核心区别 ​

判断方式作用与 Java 的对应关系null 安全?
== / !=判断值是否相等,底层调用 equals 方法等同于 Java 的equals()方法是,null 比较不会抛空指针
equals判断值是否相等,可重写自定义规则与 Java 的equals()方法一致否,调用者为 null 会抛异常
eq / ne判断引用地址是否相等,即两个对象是否是 JVM 中的同一个实例等同于 Java 的==对引用类型的判断是

核心记忆点:

  • Scala 的==是值相等,Java 的==对引用类型是地址相等;
  • Scala 的eq是地址相等,等同于 Java 的==对引用类型的作用;
  • 绝大多数场景,我们只需要用==判断值相等即可,无需关心引用地址。

5.2.2 课程案例:相等性判断基础实战 ​

scala
object RunEqual {
  def main(args: Array[String]): Unit = {
    // 1. String类型相等性判断
    val s1 = new String("scala")
    val s2 = new String("scala")
    val s3 = new String("java")

    println("=== String类型 ===")
    println(s"s1 == s2 ? ${s1 == s2}") // true,值相等
    println(s"s1 == s3 ? ${s1 == s3}") // false,值不相等
    println(s"s1 eq s2 ? ${s1 eq s2}") // false,地址不同,两个不同的对象
    println(s"s1 equals s2 ? ${s1 equals s2}") // true,值相等

    // 2. List集合相等性判断
    val l1 = List(1,2)
    val l2 = List(1,2)
    println("\n=== List集合 ===")
    println(s"l1 == l2 ? ${l1 == l2}") // true,集合元素值相等
    println(s"l1 eq l2 ? ${l1 eq l2}") // false,地址不同
  }
}

5.3 自定义对象相等性:重写 equals 方法 ​

对于我们自己定义的类,Scala 默认的==/equals方法仍然是判断引用地址相等,若要实现按属性值判断相等,需要重写equals方法,同时建议重写hashCode方法。

5.3.1 重写 equals 方法的强制规则 ​

  1. 方法名必须为equals,参数类型必须为Any,返回值为Boolean;

  2. 必须使用override关键字修饰;

  3. 必须遵守 equals 的四大特性:

    • 反射性:x.equals(x)必须返回 true;
    • 对称性:若x.equals(y)为 true,则y.equals(x)也必须为 true;
    • 传递性:若x.equals(y)为 true,y.equals(z)为 true,则x.equals(z)也必须为 true;
    • 非空性:非空对象x.equals(null)必须返回 false。

5.3.2 课程案例:Student 类重写 equals 方法 ​

需求:定义 Student 类,包含姓名、年龄、性别、身份证号属性,身份证号相等则认为两个对象相等。

scala
class Student3(
                var name: String,
                var age: Int,
                val sex: Boolean,
                val iden: String // 身份证号,唯一标识
              ) {

  /**
   * 重写equals方法:身份证号相等则对象相等
   */
  override def equals(other: Any): Boolean = {
    // 非空判断 + 类型判断
    if (other != null && this.getClass == other.getClass) {
      // 类型转换
      val s = other.asInstanceOf[Student3]
      // 核心判断:身份证号相等
      return this.iden == s.iden
    }
    false
  }

  /**
   * 重写hashCode方法,与equals保持一致
   */
  override def hashCode(): Int = iden.hashCode
}

object RunOverwriteEq {
  def main(args: Array[String]): Unit = {
    val s1 = new Student3("xiaoming", 15, true, "110111123")
    val s2 = new Student3("xiaoming", 16, true, "110111123") // 身份证号相同
    val s3 = new Student3("xiaoming", 15, true, "110111124") // 身份证号不同

    println(s"s1 == s2 ? ${s1 == s2}") // true,身份证号相等
    println(s"s1 == s3 ? ${s1 == s3}") // false,身份证号不同
    println(s"s1 == null ? ${s1 == null}") // false,非空性
  }
}

第六章 课后作业全解析 ​

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

一、简答题 ​

(1)Scala 的程序入口是什么? ​

答:Scala 的程序入口有两种实现方式,均必须定义在object单例对象中,不能定义在class类中:

  1. 标准 main 方法:在单例对象中定义def main(args: Array[String]): Unit = {}方法,等同于 Java 的 main 方法,是最通用的程序入口;
  2. 继承 App 特质:单例对象继承App特质,直接在单例对象的花括号中编写业务代码,无需手动定义 main 方法,是 Scala 的极简入口写法。

(2)Scala 对外访问变量的原因是什么? ​

答:Scala 中,类的属性对外可访问的核心原因是编译器会为属性自动生成对应的 getter/setter 方法,具体规则如下:

  1. var修饰的属性:编译器自动生成公有的 getter 和 setter 方法,外部可通过对象.属性名访问(getter)和修改(setter);
  2. val修饰的属性:编译器自动生成公有的 getter 方法,无 setter 方法,外部仅可访问,不可修改;
  3. 若属性被private修饰,编译器不会生成公有的 getter/setter 方法,外部无法访问该属性。

二、编程题 ​

(1)打印数字 1 到 100,每行包含一组 5 个数 ​

scala
object Homework1 {
  def main(args: Array[String]): Unit = {
    for (i <- 1 to 100) {
      print(i + "\t")
      // 每5个数字换行
      if (i % 5 == 0) {
        println()
      }
    }
  }
}

(2)打印数字 1 到 100,3 的倍数打印 “triple”,5 的倍数打印 “five”,3 和 5 的倍数打印 “triple_five”,其余打印数字本身 ​

scala
object Homework2 {
  def main(args: Array[String]): Unit = {
    for (i <- 1 to 100) {
      if (i % 3 == 0 && i % 5 == 0) {
        println("triple_five")
      } else if (i % 3 == 0) {
        println("triple")
      } else if (i % 5 == 0) {
        println("five")
      } else {
        println(i)
      }
    }
  }
}

(3)给一个双精度数,大于 0 返回 “greater”,等于 0 返回 “equal”,否则返回 “less”,结果赋值给 result 并打印 ​

scala
object Homework3 {
  def main(args: Array[String]): Unit = {
    // 测试用双精度数,可修改
    val num = 2.7255
    // if表达式赋值给result
    val result = if (num > 0) {
      "greater"
    } else if (num == 0) {
      "equal"
    } else {
      "less"
    }
    // 打印结果
    println(result)
  }
}

(4)根据参数 n,输出等腰三角形 ​

scala
object Homework4 {
  def main(args: Array[String]): Unit = {
    // 三角形行数,可修改
    val n = 4
    for (i <- 1 to n) {
      // 打印前置空格
      for (_ <- 1 to n - i) {
        print(" ")
      }
      // 打印#号
      for (_ <- 1 to i) {
        print("#")
      }
      // 换行
      println()
    }
  }
}

(5)定义 Point 类,用 Point (3,4) 创建对象,求横纵坐标的和 ​

scala
class Point(val x: Int, val y: Int) {
  // 求横纵坐标的和
  def sum(): Int = x + y
}

object Point {
  // 重写apply方法,无需new创建对象
  def apply(x: Int, y: Int): Point = new Point(x, y)
}

object Homework5 {
  def main(args: Array[String]): Unit = {
    val p = Point(3, 4)
    println(s"横纵坐标和:${p.sum()}")
  }
}

(6)学生注册信息类,支持姓名 + 电话 / 姓名 + 身份证注册,姓名 / 电话 / 身份证不可修改,专业可修改,姓名 + 电话 / 姓名 + 身份证相等则为同一个学生 ​

scala
/**
 * 学生注册信息类
 * @param name 姓名,不可修改
 * @param phone 电话,不可修改,可选
 * @param idCard 身份证号,不可修改,可选
 * @param major 专业,可修改,可选
 */
class StudentRegister private (
                                 val name: String,
                                 val phone: Option[String],
                                 val idCard: Option[String],
                                 var major: String = ""
                               ) {

  /**
   * 辅助构造器1:姓名+电话注册
   */
  def this(name: String, phone: String) {
    this(name, Some(phone), None)
  }

  /**
   * 辅助构造器2:姓名+身份证注册
   */
  def this(name: String, idCard: String) {
    this(name, None, Some(idCard))
  }

  /**
   * 重写equals方法:姓名+电话 或 姓名+身份证相等,则为同一个学生
   */
  override def equals(other: Any): Boolean = {
    if (other != null && this.getClass == other.getClass) {
      val s = other.asInstanceOf[StudentRegister]
      // 姓名+电话相等 或 姓名+身份证相等
      val phoneEqual = this.name == s.name && this.phone.isDefined && s.phone.isDefined && this.phone.get == s.phone.get
      val idCardEqual = this.name == s.name && this.idCard.isDefined && s.idCard.isDefined && this.idCard.get == s.idCard.get
      if (phoneEqual || idCardEqual) {
        println("信息已经注册或请核对信息")
        return true
      }
    }
    false
  }

  override def hashCode(): Int = {
    if (phone.isDefined) (name + phone.get).hashCode else (name + idCard.get).hashCode
  }
}

object Homework6 {
  def main(args: Array[String]): Unit = {
    // 姓名+电话注册
    val s1 = new StudentRegister("张三", "13800138000")
    s1.major = "大数据技术"
    // 姓名+身份证注册
    val s2 = new StudentRegister("张三", "110101199001011234")
    // 同姓名同电话的学生
    val s3 = new StudentRegister("张三", "13800138000")

    // 测试相等性
    println(s"s1 == s3 ? ${s1 == s3}") // 触发提示,返回true
    println(s"s1 == s2 ? ${s1 == s2}") // 返回false
  }
}

学习总结 ​

  1. 开发环境:IDEA+Scala 环境搭建核心是安装 Scala 插件、配置 Scala SDK,是后续 Spark 大数据开发的基础。

  2. 内建控制结构:

    • if 表达式有返回值,可直接赋值给变量,是 Scala 函数式编程的基础;
    • for 循环支持卫语句过滤、yield 返回集合,是大数据数据处理的核心语法,比 Java for 循环更简洁、更强大;
    • Scala 没有 ++/-- 运算符,用 +=1/-=1 替代。
  3. 类与构造器:

    • Scala 类的属性通过 val/var/private 控制访问权限,编译器自动生成 get/set 方法,无需手动编写;
    • Scala 构造器分为主构造器(类名后直接定义)和辅助构造器(this 关键字定义),辅助构造器必须第一行调用主构造器或已定义的辅助构造器。
  4. 伴生类与伴生对象:

    • Scala 通过 object 单例对象替代 Java 的 static 静态成员,伴生类和伴生对象必须同名、同文件;
    • 重写伴生对象的 apply 方法,可实现无需 new 关键字创建对象,是 Spark 源码中最常用的语法。
  5. 对象相等性:

    • Scala 的==判断值相等,eq判断引用地址相等,与 Java 规则完全相反;
    • 自定义类需重写 equals 方法实现按属性值判断相等,同时需重写 hashCode 方法保证一致性。

下一章预习内容:Scala 的方法和函数、函数式编程核心特性。

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