在前几节中,我们学习了 Scala 的基础语法和流程控制。现在,我们将深入探索 Scala 作为一门纯粹的面向对象语言的核心。在 Scala 中,万物皆对象,没有像 Java 那样的原始类型和静态成员的区分。本节将重点介绍如何定义对象的蓝图,以及如何使用 Scala 独特的单例对象和伴生机制。

思维导图

在这里插入图片描述
在这里插入图片描述

一、类和对象

创建对象模板或蓝图。它定义一类事物共同的属性 (成员变量)行为 (成员方法)
对象,也称为实例,是根据类这个蓝图创建出来具体实体

基本语法:

class ClassName {// 成员变量 (字段)// 成员方法
}// 使用 new 关键字创建类的实例 (对象)
val objectName = new ClassName()

二、成员变量与成员方法

1. 定义和访问成员变量

在类中定义的变量或常量,称为成员变量或字段

class Person {// 定义一个可变的成员变量 namevar name: String = "Unknown"// 定义一个不可变的成员变量 (常量) ageval age: Int = 0
}// 创建 Person 类的对象
val person1 = new Person()// 访问和修改成员变量
println(person1.name) // 输出: Unknown
person1.name = "Alice"
println(person1.name) // 输出: Alice
// person1.age = 25 // 这行会编译错误,因为 age 是 val (常量)

2. 使用下划线 _ 初始化成员变量

在 Scala 中,var 类型的成员变量必须被初始化。如果你暂时不想给它一个有意义的初始值,可以使用下划线 _ 作为占位符,Scala 会为其赋予该类型的默认零值

类型默认零值
数值类型 (Int, Double, etc.)0
Booleanfalse
Char\u0000
所有引用类型 (AnyRef)null

代码案例:

class Student {var name: String = _ // 初始化为 nullvar age: Int = _     // 初始化为 0var isMale: Boolean = _ // 初始化为 false
}val student1 = new Student()
println(s"Name: ${student1.name}, Age: ${student1.age}, Is Male: ${student1.isMale}")
// 输出: Name: null, Age: 0, Is Male: false

注意: 这种下划线初始化的方式在现代 Scala 编程中使用得越来越少,因为它容易引入 NullPointerException。更推荐的做法是提供一个有意义的初始值,或者使用Option 类型来表示可能缺失的值。

3. 定义和访问成员方法

成员方法定义了对象的行为

class Circle {val radius: Double = 5.0// 定义一个计算面积的方法def getArea(): Double = {Math.PI * radius * radius}
}val c1 = new Circle()
// 调用方法
val area = c1.getArea()
println(s"The area of the circle is: $area")

三、访问权限修饰符

Scala 通过访问权限修饰符控制成员的可见性,以实现封装

修饰符描述
(无修饰符)默认为 public,在任何地方都可以访问。
private私有成员,只能在定义该成员的类或其伴生对象内部访问。
protected受保护成员,只能在定义该成员的类及其子类中访问。
private[this]对象私有,比 private 更严格。只能在当前对象实例中访问,即使是同一个类的其他对象不能访问。
private[包名]包私有,成员的可见性限定在指定的及其子包中。

代码案例:

class Animal {private var privateName = "Secret"protected var protectedAge = 2def printInfo(): Unit = {println(s"This is a private name: $privateName") // 类内部可以访问 private}
}class Dog extends Animal {def getAge(): Int = {// println(privateName) // 错误:子类不能访问父类的 private 成员protectedAge // 正确:子类可以访问父类的 protected 成员}
}val animal = new Animal()
// println(animal.privateName) // 错误:外部不能访问 private 成员
// println(animal.protectedAge) // 错误:外部不能访问 protected 成员
animal.printInfo()val dog = new Dog()
println(s"Dog's age: ${dog.getAge()}")

四、类的构造器

构造器是在创建对象自动调用特殊方法,用于初始化对象

1. 主构造器

主构造器直接定义类名之后参数列表中。
主构造器会执行类定义所有语句
如果主构造器的参数没有用 valvar 声明,它将是一个私有不可变字段仅在类内部可见。
如果用 valvar 声明,该参数会成为一个公共成员变量

代码案例:

// name 和 age 是主构造器的参数,并成为公共的不可变/可变成员变量
class Employee(val name: String, var age: Int) {// 这部分代码是主构造器的一部分,在 new Employee(...) 时执行println(s"New employee created: $name, age $age")// 一个普通的成员方法def work(): Unit = println(s"$name is working.")
}val emp1 = new Employee("Alice", 30)
println(emp1.name) // 可以访问
emp1.age = 31 // 可以修改

2. 辅助构造器

一个类可以有多个辅助构造器。
辅助构造器的名称必须是 this
关键规则:每个辅助构造器的第一行必须直接或间接地调用主构造器 (或另一个已定义的辅助构造器)。

代码案例:

class Car(val brand: String, val year: Int) {var color: String = "White"// 辅助构造器一:提供品牌、年份和颜色def this(brand: String, year: Int, color: String) {this(brand, year) // 必须先调用主构造器this.color = color}// 辅助构造器二:只提供品牌def this(brand: String) {this(brand, 2024) // 调用主构造器,年份默认为 2024}
}val car1 = new Car("Toyota", 2023)
val car2 = new Car("BMW", 2024, "Black")
val car3 = new Car("Ford")
println(s"${car3.brand} color is ${car3.color} and year is ${car3.year}")

五、单例对象、main方法与伴生对象

1. 单例对象

在 Scala 中,使用 object 关键字定义的不是类,而是一个单例对象——它是一个全局唯一实例
单例对象不能被 new,它的所有成员都类似于 Java 中的静态成员

代码案例:

object Logger {var level: String = "INFO"def log(message: String): Unit = {println(s"[$level] $message")}
}// 直接通过对象名访问成员
Logger.level = "DEBUG"
Logger.log("This is a debug message.")

2. main 方法

Scala 应用程序的入口点是一个名为 main方法,它必须定义在一个单例对象中。

两种实现方式:

  1. 标准 main 方法:
object MyApp {def main(args: Array[String]): Unit = {println("Hello from the main method!")}
}
  1. 继承 App 特质 (更简洁):
object MyApp extends App {// 这里的代码会直接作为 main 方法体执行println("Hello from the App trait!")// 命令行参数可以通过 args 变量访问if (args.length > 0) {println(s"First argument: ${args(0)}")}
}

3. 伴生对象

当一个单例对象与一个具有相同的名称,并且它们定义在同一个源文件中时,这个对象被称为该类的伴生对象,该类被称为该对象的伴生类

核心特性:
伴生类和伴生对象可以互相访问对方的私有 (private) 成员
常见用途

在伴生对象中放置类似于 Java 静态方法工具方法
在伴生对象中定义工厂方法 (特别是名为 apply 的方法),用于创建伴生类实例隐藏 new 关键字。

代码案例:

// 伴生类
class User private (val id: Int, val name: String) { // 主构造器设为 privateprivate def secretMethod(): String = s"User $name has a secret."def greet(): Unit = {// 访问伴生对象的私有成员println(User.defaultGreeting + ", " + name)}
}// 伴生对象
object User {private val defaultGreeting = "Welcome"// 工厂方法,可以访问 User 类的私有构造器def apply(name: String): User = {val newId = scala.util.Random.nextInt(1000)new User(newId, name)}def printSecret(user: User): Unit = {// 访问 User 实例的私有方法println(user.secretMethod())}
}// 使用伴生对象的 apply 工厂方法创建实例 (无需 new)
val user1 = User("Bob")
user1.greet()
User.printSecret(user1)// val user2 = new User(10, "Charlie") // 错误:构造器是私有的

六、综合案例

在 Scala 中,工具类 (包含纯粹的功能方法,不维护状态) 通常被实现单例对象

代码案例:一个简单的字符串工具对象

object StringUtils {/*** 判断字符串是否为空 (null 或 "")* @param s 待检查的字符串* @return 如果为空则返回 true,否则返回 false*/def isEmpty(s: String): Boolean = {s == null || s.trim.isEmpty}/*** 将字符串首字母大写* @param s 待转换的字符串* @return 转换后的字符串*/def capitalize(s: String): String = {if (isEmpty(s)) s else s.substring(0, 1).toUpperCase + s.substring(1)}
}// 在另一个对象中(例如主程序)使用工具类
object MainApp extends App {val str1 = "hello scala"val str2 = "  "val str3: String = nullprintln(s"'${str1}' is empty? ${StringUtils.isEmpty(str1)}")println(s"'${str2}' is empty? ${StringUtils.isEmpty(str2)}")println(s"Capitalized '${str1}': ${StringUtils.capitalize(str1)}")
}

练习题

题目一:简单类定义
定义一个 Book 类,包含两个不可变的成员变量:title (String) 和 author (String)。

题目二:创建和访问对象
创建 Book 类的一个实例,title 为 “Programming in Scala”,author 为 “Martin Odersky”。然后打印出这本书的标题。

题目三:成员方法
Book 类添加一个名为 getInfo 的方法,该方法返回一个格式为 "Title by Author" 的字符串。

题目四:下划线初始化
定义一个 Movie 类,包含一个可变的成员变量 director (String),使用下划线 _ 进行默认初始化。创建实例后打印出 director 的初始值。

题目五:主构造器
定义一个 Laptop 类,其主构造器接收 brand (String, 不可变) 和 ramInGB (Int, 可变) 两个参数,并将它们直接定义为公共成员变量

题目六:辅助构造器
Laptop 类添加一个辅助构造器,该构造器只接收 brand 参数,并默认将 ramInGB 设置为 8。

题目七:访问修饰符
定义一个 BankAccount 类,其中 balance (Double) 是私有的。提供一个公共deposit(amount: Double) 方法和一个公共getBalance() 方法来访问余额。

题目八:单例对象
创建一个名为 MathConstants单例对象,在其中定义两个常量:PI (值为 3.14159) 和 E (值为 2.71828)。

题目九:main 方法
创建一个名为 EntryPoint 的单例对象,并继承 App 特质,在其中打印 “Scala application started!”。

题目十:伴生对象与私有成员
定义一个 Circle 类,其主构造器接收一个私有radius (Double) 参数。然后,为其创建一个伴生对象,该对象有一个 calculateArea(c: Circle) 方法,可以计算并返回给定 Circle 实例的面积 (面积 = PI * r * r)。

题目十一:apply 工厂方法
Circle 的伴生对象中添加一个 apply 方法,该方法接收一个 radius 参数,并返回一个新的 Circle 实例。这样就可以使用 Circle(5.0) 来创建对象。

题目十二:工具类方法
在之前的 StringUtils 单例对象中,添加一个名为 reverse 的方法,接收一个字符串并返回其反转后的结果。

题目十三:主构造器代码块
修改 Employee 类的定义,在其主构造器代码块中添加一条逻辑:检查传入的 age 是否小于18,如果是,则打印一条警告信息 “Warning: Employee age is below 18.”。

题目十四:对象私有成员 private[this]
定义一个 Point 类,包含 xy 两个坐标。再定义一个 isSameAs(other: Point) 方法,比较当前点是否与另一个点相同。然后,修改 xyprivate[this],并观察 isSameAs 方法是否还能正常编译。如果不能,解释原因。

题目十五:综合案例
创建一个 Counter 类,它有一个私有的、可变count 变量,初始值为0。类中提供 increment() 方法 (每次将count加1) 和 current() 方法 (返回当前count值)。为其创建一个伴生对象,提供一个 apply 方法,允许通过 Counter() 创建新实例。

答案与解析

答案一:

class Book(val title: String, val author: String)

解析: 在主构造器参数前使用 val 是将参数直接定义为公共不可变成员变量的简洁语法。

答案二:

val myBook = new Book("Programming in Scala", "Martin Odersky")
println(myBook.title)

解析: 使用 new 关键字和类名来创建对象,通过 . 操作符访问其成员。

答案三:

class Book(val title: String, val author: String) {def getInfo(): String = {s"$title by $author"}
}
val myBook = new Book("A Brief History of Time", "Stephen Hawking")
println(myBook.getInfo())

解析: def 用于在类中定义方法。s"" 字符串插值器用于方便地格式化字符串。

答案四:

class Movie {var director: String = _
}
val m = new Movie()
println(m.director) // 输出: null

解析: String 是引用类型 (AnyRef),其默认零值是 null

答案五:

class Laptop(val brand: String, var ramInGB: Int)

解析: val 使 brand 成为不可变成员,var 使 ramInGB 成为可变成员。

答案六:

class Laptop(val brand: String, var ramInGB: Int) {// 辅助构造器def this(brand: String) {this(brand, 8) // 调用主构造器}
}
val defaultLaptop = new Laptop("Dell")
println(s"${defaultLaptop.brand} has ${defaultLaptop.ramInGB}GB RAM")

解析: 辅助构造器 def this(...) 必须在其第一行调用另一个构造器。

答案七:

class BankAccount {private var balance: Double = 0.0def deposit(amount: Double): Unit = {if (amount > 0) balance += amount}def getBalance(): Double = {balance}
}

解析: private 关键字将 balance 的访问权限限制在类内部,外部只能通过公共的 depositgetBalance 方法进行交互,实现了封装。

答案八:

object MathConstants {val PI = 3.14159val E = 2.71828
}
println(MathConstants.PI)

解析: object 关键字创建了一个全局唯一的单例对象。

答案九:

object EntryPoint extends App {println("Scala application started!")
}

解析: 继承 App 特质是创建可执行应用程序的最简洁方式,对象体内的代码会自动成为 main 方法的内容。

答案十:

class Circle private (val radius: Double)object Circle {def calculateArea(c: Circle): Double = {// 可以访问 Circle 的私有成员 radiusMath.PI * c.radius * c.radius}
}

解析: 伴生对象 Circle 可以访问伴生类 Circleprivate 成员 radius

答案十一:

class Circle private (val radius: Double)object Circle {def apply(radius: Double): Circle = {new Circle(radius)}// ... calculateArea 方法 ...
}val myCircle = Circle(5.0) // 无需 new,直接调用 apply 方法

解析: apply 方法是一个特殊的语法糖,允许你像调用函数一样创建对象。

答案十二:

object StringUtils {// ... isEmpty, capitalize 方法 ...def reverse(s: String): String = {if (s == null) s else s.reverse}
}
println(StringUtils.reverse("scala"))
```*   **解析:** `String` 类型自带 `.reverse` 方法,可以直接使用。**答案十三:**
```scala
class Employee(val name: String, var age: Int) {if (age < 18) {println(s"Warning: Employee $name's age is below 18.")}def work(): Unit = println(s"$name is working.")
}
val youngEmployee = new Employee("Tom", 17)

解析: 类定义体中、成员方法之外的代码都属于主构造器的一部分,会在对象创建时执行。

答案十四:

class Point(private[this] val x: Int, private[this] val y: Int) {def isSameAs(other: Point): Boolean = {// this.x == other.x // 这行代码会编译错误false // 仅为使代码完整}
}

解析: 代码无法正常编译。因为 private[this]对象私有的,意味着只有当前对象 (this) 才能访问 xy。在 isSameAs 方法中,other.x 尝试访问另一个Point 对象的 x 字段,这是不被允许的。如果使用 private,则可以访问,因为 private 允许同一类的不同实例之间互相访问私有成员。

答案十五:

class Counter private {private var count: Int = 0def increment(): Unit = {count += 1}def current(): Int = {count}
}object Counter {def apply(): Counter = new Counter()
}val c1 = Counter()
c1.increment()
c1.increment()
println(c1.current()) // 输出: 2

解析: 这个例子结合了私有构造器、私有成员、公共方法和伴生对象的 apply 工厂方法,是一个典型的Scala封装模式。将构造器设为私有,强制用户通过伴生对象的工厂方法来创建实例。

在这里插入图片描述

日期:2025年9月14日
专栏:Scala教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/96599.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/96599.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/96599.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【大语言模型 58】分布式文件系统:训练数据高效存储

分布式文件系统&#xff1a;训练数据高效存储 关键词&#xff1a;分布式文件系统、HDFS、Lustre、GlusterFS、数据本地性、I/O优化、存储架构、大数据存储、训练数据管理、存储性能调优 摘要&#xff1a;本文深入探讨大语言模型训练中的分布式文件系统技术&#xff0c;从存储架…

【科研绘图系列】R语言绘制散点图以及线性回归拟合曲线图

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍 加载R包 数据下载 函数 导入数据 数据预处理 画图 总结 系统信息 介绍 数据导入 代码的开始部分涉及多个数据集的导入,这些数据集涵盖了不同类型的生态学数据,包括实验室培养…

SQL 数据库操作语言详解

1. SQL 语言概述SQL&#xff08;Structured Query Language&#xff09;是用于管理关系型数据库的标准语言&#xff0c;主要分为以下几个子语言&#xff1a;- DQL&#xff08;数据查询语言&#xff09;&#xff1a;SELECT - 用于数据查询 - DML&#xff08;数据操作语言&#x…

积分变换的前世今生

积分变换常应用于解微分方程微分方程的解法&#xff1a;时域经典法&#xff1b;频域变换法&#xff1b;“积分变换”最初并不是为了解微分方程&#xff0c;而是出于更“纯粹”的数学动机——理解函数的结构、求解代数或几何问题&#xff0c;以及简化复杂的积分运算。微分方程的…

《Linux——gflags》

一、什么是gflags&#xff1f; gflags 是一个由 Google 开发的命令行参数解析库&#xff0c;主要用于在 C&#xff08;也支持其他语言&#xff09;程序中便捷地处理命令行参数。它的核心作用是帮助开发者快速定义、解析和使用命令行选项&#xff0c;避免手动编写繁琐的参数解析…

编译器的前端中端和后端

前面说的词法分析和语法分析&#xff0c;确实是编译器前端 (Front End) 最核心的两个部分。但前端的工作还没有结束。编译器各阶段划分 一个完整的编译器通常可以分为三个部分&#xff1a;前端、中端 (Middle End)、后端 (Back End)。 前端 (Front End) 核心职责: 理解源代码。…

黑马Java进阶教程,全面剖析Java多线程编程,并发和并行,笔记02

黑马Java进阶教程&#xff0c;全面剖析Java多线程编程&#xff0c;并发和并行&#xff0c;笔记02 一、并发和并行 并发&#xff1a;在同一时刻&#xff0c;有多个指令在单个CPU上交替执行 并行&#xff1a;在同一时刻&#xff0c;有多个指令在多个CPU上同时执行 二、为什么有…

20250908 背包DP总结

引子 ~ 我们都有一个家&#xff0c;名字叫背包 ~ 背包DP 顾名思义&#xff0c;背包DP是用来解决背包最值问题的。题目会给出背包的容量&#xff0c;以及几个物品的属性&#xff0c;比如重量&#xff0c;价值&#xff0c;限额等等&#xff0c;具体是什么看题目。 01背包 01…

Redis持久化之RDB:快照机制原理、配置与最佳实践

Redis持久化之RDB&#xff1a;快照机制原理、配置与最佳实践 1. RDB持久化概述 1.1 什么是RDB RDB&#xff08;Redis Database&#xff09;是Redis的默认持久化方式&#xff0c;它在指定的时间间隔内生成数据集的快照&#xff08;snapshot&#xff09;&#xff0c;并将快照保…

daily notes[44]

文章目录基础references基础 hello,world是几乎所有编程语言的第一例子&#xff0c;rust也不例外。但和其它语言不一样&#xff0c;Rust的源码最好拥有自己的项目目录。 $ mkdir ~/pro $ cd ~/pro $ mkdir helloWorld $ cd helloWorld源代码文件名为main.rs&#xff0c;内容如…

JavaScript对象创建方式完全指南:从原始到现代的演进之路

前言 作为一名前端开发者&#xff0c;JavaScript中对象创建是很重要。在JavaScript这门基于原型的语言中&#xff0c;对象几乎无处不在。今天&#xff0c;我将带领大家回顾JavaScript对象创建的7种方式&#xff0c;从最原始的字面量到现代的ES6 class&#xff0c;每一步演进都解…

基于单片机的无线水塔监控系统设计(论文+源码)

本设计为基于单片机的无线水塔监控系统设计&#xff0c;主要由以下几部分组成&#xff1a;均采用STC89C52RC单片机为主控&#xff1b;主机&#xff1a;NRF24L01无线通讯模块&#xff0c;1602LCD液晶显示屏。从机&#xff1a;NRF24L01无线通讯模块&#xff0c;水位传感器&#x…

凌晨0-3点不睡,你熬的不是夜,是人生!

“熬夜”这个词&#xff0c;早已成为现代生活的常态。有人为了工作加班到深夜&#xff0c;有人为了娱乐刷剧到天明&#xff0c;但你知道吗&#xff1f;熬夜最“要命”的时间段&#xff0c;其实是凌晨0点到凌晨3点。别以为只是少睡几个小时而已&#xff0c;这个时间段不睡&#…

大语言模型基石:Transformer

一、引言 如今火爆的 GPT、LLaMA、通义千问、ChatGLM 等大语言模型&#xff0c;背后都离不开一个核心架构——Transformer。 2017 年&#xff0c;Google 在论文《Attention Is All You Need》中首次提出 Transformer 模型&#xff0c;彻底改变了自然语言处理的发展方向。它摒…

【算法】【链表】160.相交链表--通俗讲解

算法通俗讲解推荐阅读 【算法–链表】83.删除排序链表中的重复元素–通俗讲解 【算法–链表】删除排序链表中的重复元素 II–通俗讲解 【算法–链表】86.分割链表–通俗讲解 【算法】92.翻转链表Ⅱ–通俗讲解 【算法–链表】109.有序链表转换二叉搜索树–通俗讲解 【算法–链表…

MySQL——库的操作

1、创建数据库语法&#xff1a;CREATE DATABASE [IF NOT EXISTS] db_name [create_specification [, create_specification] ...] create_specification: [DEFAULT] CHARACTER SET charset_name [DEFAULT] COLLATE collation_name这里的CHARACTER SET表示指定数据库采用的字符集…

Python ast模块(Abstract Syntax Trees,抽象语法树)介绍及使用

文章目录 核心概念 基本使用流程 常用节点类型 示例代码 实际应用场景 注意事项 `ast.literal_eval()` 功能说明 适用场景 使用示例 限制与安全特性 与 `eval()` 的对比 总结 Python 的 ast 模块( Abstract Syntax Trees,抽象语法树)允许你解析、分析和修改 Python 代码的…

C++宽度优先搜索算法:队列与优先级队列

本期我们就来深入学习一下C算法中一个很重要的算法思想&#xff1a;宽度优先搜索算法 宽度优先算法是一个应用十分广泛的算法思想&#xff0c;涉及的领域也十分繁多&#xff0c;因此本篇我们先只涉猎它的一部分算法题&#xff1a;队列/优先级队列&#xff0c;后续我们会进一步地…

类的property属性

​​Python 中的 property 特性详解​​property 是 Python 中用于​​将方法转换为属性​​的装饰器&#xff0c;它允许开发者以访问属性的方式调用方法&#xff0c;同时可以添加逻辑控制&#xff08;如数据校验、计算属性等&#xff09;。以下是其核心用法和优势&#xff1a;…