3 内建控制、类与对象
笔记衔接前序《Scala 数据类型与基本运算》,严格匹配课程体系,针对有 Java 基础的大数据专业大学生设计,全程Java 语法对比 + 可运行代码案例 + 课堂案例拆解 + 避坑指南 + 作业全解析,重点贴合 Spark/Flink 大数据开发的高频语法,所有案例均来自课程配套源码,可直接复制运行。
前置知识:Scala 基础语法、val/var 变量、Java 面向对象基础
开发环境:JDK1.8+、Scala2.12.x、IDEA + Scala 插件
核心学习目标:
- 掌握 IDEA+Scala 开发环境搭建,完成本地开发环境配置
- 熟练使用 Scala 内建控制结构,重点掌握 if 表达式、for 循环的 Scala 特色用法
- 彻底掌握 Scala 类的定义、主构造器与辅助构造器,明确与 Java 构造器的核心差异
- 理解 Scala 伴生类与伴生对象的设计思想,掌握其在大数据开发中的高频应用
- 掌握 Scala 对象的类型判断与相等性规则,规避新手高频踩坑点
第一章 IDEA + Scala 开发环境搭建
Scala 的开发主要基于 IDEA 完成,大数据开发中 Spark/Flink 的项目也均基于 IDEA 开发,本节详细讲解环境搭建全流程,与课程 PPT 完全匹配。
1.1 环境准备
搭建前必须准备好以下基础环境:
- JDK1.8+:大数据场景统一使用 JDK1.8,保证与 Spark/Flink 的兼容性
- Scala2.12.x:课程指定版本,Spark3.x、Flink1.15 + 均主流兼容该版本
- IDEA:社区版 / 旗舰版均可,建议 2019.2 及以上版本
1.2 Scala 插件安装
Scala 插件是 IDEA 支持 Scala 开发的核心,提供代码高亮、语法检查、编译运行等能力,支持在线安装和离线安装两种方式。
方式 1:在线安装(推荐,网络良好时使用)
- 打开 IDEA,进入
Settings(快捷键Ctrl+Alt+S) - 选择
Plugins→Marketplace,在搜索框输入Scala - 找到 JetBrains 官方出品的 Scala 插件,点击
Install安装 - 安装完成后,点击
Restart IDE重启 IDEA,插件生效
方式 2:离线安装(网络受限场景使用)
- 提前下载插件离线包:官方下载地址 http://plugins.jetbrains.com/plugin/1347-scala/versions,选择与 IDEA 版本匹配的插件版本
- 打开 IDEA,进入
Settings→Plugins - 点击右上角齿轮图标 → 选择
Install Plugin from Disk - 选择下载好的插件离线包,完成安装后重启 IDEA 即可
1.3 IDEA 项目与 SDK 配置
步骤 1:创建 Scala 项目
- 打开 IDEA,点击
New Project - 选择
Scala→IDEA,点击 Next - 填写项目名称、项目路径,选择 JDK1.8 版本
- 点击
Finish,完成项目创建
步骤 2:配置 Scala SDK
- 进入项目后,右键项目名 → 选择
Open Module Settings(快捷键Ctrl+Alt+Shift+S) - 选择
Libraries→ 点击+号 → 选择Scala SDK - 选择本地安装的 Scala2.12.x 版本,点击
OK,完成 SDK 配置 - 配置完成后,即可在项目中创建 Scala 类 / 对象,编写并运行 Scala 代码
第二章 Scala 内建控制结构
Scala 的内建控制结构分为顺序结构、分支结构、循环结构三大类,语法与 Java 大体相似,但有几个核心的 Scala 特色特性,是大数据开发中高频使用的语法,必须重点掌握。
2.1 顺序结构
顺序结构是程序最基础的执行结构,程序按照代码从上到下的顺序依次执行,中间无任何判断和跳转,与 Java 完全一致。
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 基础语法
// 标准if-else语法,与Java一致
if(布尔表达式1){
// 表达式1为true时执行
}else if(布尔表达式2){
// 表达式2为true时执行
}else{
// 所有条件都为false时执行
}2.2.2 带返回值的 if 表达式
Scala 中,if 表达式的返回值为满足条件的代码块中最后一行表达式的值,可直接赋值给变量。
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 代码块只有一行语句,可省略花括号,代码更简洁:
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 个核心差异:
- while/do-while 循环的返回值是
Unit(即()),而非 Java 的无返回值; - Scala 不支持在 while 条件中使用赋值语句,Java 中常用的
while((a = getName()) != null)写法在 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(变量 <- 范围),<-称为「生成器」,用于将范围中的每个元素依次赋值给变量。
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 判断嵌套。
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 处理逻辑
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 个数字
object PrintNumDemo {
def main(args: Array[String]): Unit = {
for (i <- 1 to 100) {
print(i + "\t")
// 每5个数字换行
if (i % 5 == 0) {
println()
}
}
}
}案例 2:打印等腰三角形,参数 n 控制行数
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):
#
##
###
####第三章 Scala 类与构造器(核心重点)
类是面向对象编程的核心,Scala 的类定义与 Java 有显著差异,语法更简洁,构造器体系与 Java 完全不同,是学习 Scala 面向对象的核心难点,本节结合课程配套的Car.scala、Socket.scala、Pig.scala等案例,逐点拆解讲解。
3.1 类的基础定义
Scala 中使用class关键字定义类,与 Java 一致,类中可定义属性、方法、构造器、内部类等成员。
3.1.1 无参类的定义
// 定义一个无参的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 | 否 | 否 | 仅类内部可访问、不可修改 |
核心结论:
var修饰的属性,编译器自动生成公有的 getter 和 setter 方法;val修饰的属性,编译器仅生成公有的 getter 方法,无 setter 方法(不可修改);private修饰的属性,会阻止 getter/setter 方法的生成,仅类内部可访问。
课程案例对应:Pig 类实现
/**
* 课程案例: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 方法 | 不生成 |
主构造器的执行逻辑
主构造器的代码,就是类体中除了方法定义之外的所有代码,创建对象时,主构造器会依次执行类体中的所有代码。
/**
* 主构造器示例
* 主构造器参数: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 支持给主构造器参数设置默认值,创建对象时可省略带默认值的参数,简化对象创建。
/**
* 主构造器默认值示例
* 课程案例: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 个强制规则(必须遵守,否则编译报错):
- 辅助构造器的名称必须为
this; - 辅助构造器的第一行代码,必须调用主构造器,或者已经定义好的其他辅助构造器。
3.3.1 辅助构造器基础语法
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 类,提供四组构造器
- 只读属性:制造商 producerName、型号 productType、生产年份 carYear
- 可读写属性:车牌号 carNum
- 所有构造器中,制造商和型号为必填项
- 生产年份和车牌号为可选,未设置年份为 - 1,车牌为空字符串
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")
}
}运行结果:
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 单例对象基础定义
/**
* 单例对象: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 核心规则
- 伴生类和伴生对象必须同名;
- 伴生类和伴生对象必须在同一个源码文件中;
- 伴生类和伴生对象可以互相访问对方的 private 私有成员,这是 Scala 的特殊权限设计。
4.2.2 核心作用
- 替代 Java 的 static 静态成员:伴生对象中定义的属性和方法,等同于 Java 的静态属性和静态方法,直接通过
类名.成员访问; - 私有成员互访:伴生类和伴生对象可互相访问对方的私有属性和方法,突破 private 的访问限制;
- 简化对象创建:通过重写伴生对象的
apply方法,可实现无需new关键字创建对象,是 Spark 源码中最常用的语法。
4.2.3 课程案例:伴生类与伴生对象私有成员互访
/**
* 伴生类: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 方法实现
/**
* 伴生类: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) 创建对象,并求横纵坐标的和。
/**
* 伴生类: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 课程案例:类型判断实战
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 课程案例:相等性判断基础实战
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 方法的强制规则
方法名必须为
equals,参数类型必须为Any,返回值为Boolean;必须使用
override关键字修饰;必须遵守 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 类,包含姓名、年龄、性别、身份证号属性,身份证号相等则认为两个对象相等。
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类中:
- 标准 main 方法:在单例对象中定义
def main(args: Array[String]): Unit = {}方法,等同于 Java 的 main 方法,是最通用的程序入口; - 继承 App 特质:单例对象继承
App特质,直接在单例对象的花括号中编写业务代码,无需手动定义 main 方法,是 Scala 的极简入口写法。
(2)Scala 对外访问变量的原因是什么?
答:Scala 中,类的属性对外可访问的核心原因是编译器会为属性自动生成对应的 getter/setter 方法,具体规则如下:
var修饰的属性:编译器自动生成公有的 getter 和 setter 方法,外部可通过对象.属性名访问(getter)和修改(setter);val修饰的属性:编译器自动生成公有的 getter 方法,无 setter 方法,外部仅可访问,不可修改;- 若属性被
private修饰,编译器不会生成公有的 getter/setter 方法,外部无法访问该属性。
二、编程题
(1)打印数字 1 到 100,每行包含一组 5 个数
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”,其余打印数字本身
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 并打印
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,输出等腰三角形
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) 创建对象,求横纵坐标的和
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)学生注册信息类,支持姓名 + 电话 / 姓名 + 身份证注册,姓名 / 电话 / 身份证不可修改,专业可修改,姓名 + 电话 / 姓名 + 身份证相等则为同一个学生
/**
* 学生注册信息类
* @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
}
}学习总结
开发环境:IDEA+Scala 环境搭建核心是安装 Scala 插件、配置 Scala SDK,是后续 Spark 大数据开发的基础。
内建控制结构:
- if 表达式有返回值,可直接赋值给变量,是 Scala 函数式编程的基础;
- for 循环支持卫语句过滤、yield 返回集合,是大数据数据处理的核心语法,比 Java for 循环更简洁、更强大;
- Scala 没有 ++/-- 运算符,用 +=1/-=1 替代。
类与构造器:
- Scala 类的属性通过 val/var/private 控制访问权限,编译器自动生成 get/set 方法,无需手动编写;
- Scala 构造器分为主构造器(类名后直接定义)和辅助构造器(this 关键字定义),辅助构造器必须第一行调用主构造器或已定义的辅助构造器。
伴生类与伴生对象:
- Scala 通过 object 单例对象替代 Java 的 static 静态成员,伴生类和伴生对象必须同名、同文件;
- 重写伴生对象的 apply 方法,可实现无需 new 关键字创建对象,是 Spark 源码中最常用的语法。
对象相等性:
- Scala 的
==判断值相等,eq判断引用地址相等,与 Java 规则完全相反; - 自定义类需重写 equals 方法实现按属性值判断相等,同时需重写 hashCode 方法保证一致性。
- Scala 的
下一章预习内容:Scala 的方法和函数、函数式编程核心特性。