四、Scala深入面向对象:类、对象与伴生关系

四、Scala深入面向对象:类、对象与伴生关系

在前几节中,我们学习了 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 {privatevar privateName ="Secret"protectedvar 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"// 辅助构造器一:提供品牌、年份和颜色defthis(brand:String, year:Int, color:String){this(brand, year)// 必须先调用主构造器this.color = color }// 辅助构造器二:只提供品牌defthis(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){// 主构造器设为 privateprivatedef secretMethod():String= s"User $name has a secret."def greet():Unit={// 访问伴生对象的私有成员 println(User.defaultGreeting +", "+ name)}}// 伴生对象object User {privateval 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=null println(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){// 辅助构造器defthis(brand:String){this(brand,8)// 调用主构造器}}val defaultLaptop =new Laptop("Dell") println(s"${defaultLaptop.brand} has ${defaultLaptop.ramInGB}GB RAM")
解析: 辅助构造器 def this(...) 必须在其第一行调用另一个构造器。

答案七:

class BankAccount {privatevar 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 的私有成员 radius Math.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{privatevar 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教程

Read more

GLM-4v-9b实战指南:用llama.cpp GGUF格式在消费级GPU部署多模态模型

GLM-4v-9b实战指南:用llama.cpp GGUF格式在消费级GPU部署多模态模型 1. 为什么你需要关注GLM-4v-9b 你有没有遇到过这样的场景:一张密密麻麻的财务报表截图发到工作群,大家却没人愿意花十分钟手动抄录数据;或者客户发来一张手机拍的电路板照片,问“这个元件型号是什么”,你只能回个尴尬的微笑;又或者团队正在做竞品分析,需要从几十份PDF产品手册里快速提取图表信息——这些不是小问题,而是每天真实消耗工程师、运营、产品经理大量时间的“视觉理解黑洞”。 过去,这类任务要么靠人工硬啃,要么得调用API付费接口,响应慢、成本高、隐私难保障。直到2024年,智谱AI开源了glm-4v-9b——一个真正能在你自己的RTX 4090上跑起来的90亿参数多模态模型。它不只是一张“能看图说话”的新名片,而是把高分辨率图像理解能力,塞进了一张消费级显卡的显存里。 重点来了:它支持原生1120×1120输入,这意味着你不用再把一张A4扫描件缩成模糊小图上传;它对中文表格、小字号OCR、技术类图表的理解,在公开评测中直接超过了GPT-4-turbo和Claude 3 Opus;

By Ne0inhk

服务器环境 VsCode:Github Copilot 安装完成却用不了?关键步骤补全

GitHub Copilot在VS Code中无法使用的关键解决步骤 1. 基础环境检查 * VS Code版本:确保使用最新版(至少≥1.60),旧版可能导致兼容问题 * Copilot状态:在VS Code左侧活动栏点击Copilot图标(飞机形状),检查是否显示已登录和启用状态 * 网络环境:Copilot需访问GitHub服务器,尝试关闭代理或检查防火墙是否屏蔽api.github.com 2. 核心配置步骤 # 步骤1:检查Copilot是否激活 # 在VS Code命令面板(Ctrl+Shift+P)输入: > GitHub Copilot: Check Status # 步骤2:重置授权令牌(常见问题根源) > GitHub Copilot: Reset GitHub Copilot Token # 步骤3:强制刷新扩展 >

By Ne0inhk

Claude Code的完美平替:OpenCode + GitHub Copilot

引言:Claude 虽好,但你真的能用上吗? 在当前席卷全球的“Vibe Coding”浪潮中,Anthropic 推出的 Claude 系列模型 + 终端工具 Claude Code,凭借极强的逻辑推理能力,成为了开发者眼中的“白月光”。但现实是残酷的:对于中国开发者而言,账号随时被封、海外信用卡支付遭拒、API 额度受限以及复杂的网络环境,构成了一道难以逾越的门槛。 虽然最近国产编程模型不断发力,Claude Code + GLM-4.7的表现非常出色,但面对复杂问题,Claude系列模型依然完胜。难道我们只能眼馋Claude全家桶的编程体验吗? 作为一名追求极致生产力的开发者,我发现了一个绝佳的完美替代方案:OpenCode + GitHub Copilot。这个组合不仅能让你享受如 GLM-4.7 一样的性价比,还能更方便的使用 Claude 的顶级模型。 Claude Code 的开源免费平替:OpenCode 想要复刻

By Ne0inhk

3大突破性功能揭秘:Duix.Avatar开源数字人全栈技术深度剖析

3大突破性功能揭秘:Duix.Avatar开源数字人全栈技术深度剖析 【免费下载链接】HeyGem.ai 项目地址: https://gitcode.com/GitHub_Trending/he/HeyGem.ai 在AI数字人技术快速迭代的今天,开源解决方案正在重塑行业生态。Duix.Avatar作为一款全离线操作的数字人工具,凭借其独特的技术架构和卓越的性能表现,成为众多创作者的首选。本文将从技术原理、性能表现、应用场景和部署实践四个维度,深度解析这款工具的核心价值。 一、技术架构解析:分布式本地计算引擎 1.1 核心模块设计原理 Duix.Avatar采用模块化架构设计,将复杂的数字人生成流程分解为独立的功能单元: * ASR语音识别模块:基于FunASR开源框架,支持中英文混合识别,准确率高达95% * TTS语音合成引擎:集成Fish-Speech技术,实现自然流畅的语音生成 * 计算机视觉系统:自主研发的口型匹配算法,确保音视频同步精度 1.2 数据处理流程优化 与传统云端方案不同,Duix.Avatar的数据处理完全在本地完成,

By Ne0inhk