Scala 如何在 JVM 上融合函数式与面向对象编程
探讨 Scala 为何在 JVM 上将函数式与面向对象思想结合、它做对了什么,以及团队应注意的权衡取舍。

Scala 试图解决的问题
Java 让 JVM 成功,但也带来了许多团队最终会遇到的约定:大量样板代码、对可变状态的偏重,以及常常需要框架或代码生成才能保持可控的模式。开发者喜欢 JVM 的速度、工具和部署方式——但他们也希望有种能更直接表达想法的语言。
开发者希望超越“经典 Java”的地方
到 2000 年代早期,日常的 JVM 工作涉及冗长的类层次结构、getter/setter 仪式以及可能滑入生产环境的 null 相关错误。编写并发程序是可能的,但共享的可变状态让细微的竞态条件容易产生。即使团队遵循良好的面向对象设计,日常代码仍然承载很多偶然的复杂性。
Scala 的赌注是:通过一门更好的语言可以减少这种摩擦,而不必放弃 JVM:通过编译成字节码保持“足够好的”性能,同时给开发者提供能更清晰建模领域并更容易变更系统的特性。
在真实项目中为何混合 FP 和 OOP 很重要
大多数 JVM 团队并不是在“纯函数式”与“纯面向对象”之间二选一——他们是在截止日期前交付软件。Scala 的目标是让你在合适的场景下使用 OO(封装、模块化 API、服务边界),而在这些模块内部依靠函数式思想(不可变性、表达式导向、可组合的变换)来使程序更安全、更易推理。
这种融合反映了真实系统的构建方式:在模块和服务周围使用面向对象边界,在这些边界内部用函数式技术来减少错误并简化测试。
目标:更安全的代码、重用性、JVM 的实用性
Scala 的出发点是提供更强的静态类型、更好的组合与重用,以及在语言层面减少样板的工具——同时保持与 JVM 库和运行方式的兼容。
一个简短的历史说明
Martin Odersky 在研究 Java 的泛型时设计了 Scala,并受到 ML、Haskell、Smalltalk 等语言的启发。围绕 Scala 形成的社区——学术界、企业 JVM 团队,后来还有数据工程领域——帮助把它塑造成一门尝试在理论与生产需求间取得平衡的语言。
Scala 核心:"一切皆对象"
Scala 认真对待“一切皆对象”这句话。你在其他 JVM 语言中可能认为是“原始值”的东西——像 1、true 或 'a'——在 Scala 中像普通对象一样具有方法。这意味着你可以写出 1.toString 或 'a'.isLetter 之类的代码,而无需在“原始操作”和“对象操作”之间切换思维模式。
为什么这对 Java 开发者感觉熟悉
如果你习惯了 Java 风格的建模,Scala 的面向对象表层会立即让人觉得熟悉:你定义 class,创建 实例,调用 方法,并用像接口一样的类型来组织行为。
你可以以直观的方式建模领域:
class User(val name: String) {
def greet(): String = s"Hi, $name"
}
val u = new User("Sam")
println(u.greet())
这种熟悉感在 JVM 上很重要:团队可以采用 Scala,而不必放弃“对象与方法”的基本思路。
Scala 的 OO 与 Java 在实际上的差异
Scala 的对象模型比 Java 更统一、更灵活:
- 单例对象是第一类概念(
object Config { ... }),常常替代 Java 中的static模式。 - 方法更偏向表达式:返回值被强调,许多“语句”以产值表达的形式书写。
- 构造器与字段更紧凑:构造参数可以用
val/var直接成为字段,减少样板代码。
继承仍然存在并且常用,但通常更轻量:
class Admin(name: String) extends User(name) {
override def greet(): String = s"Welcome, $name"
}
在日常工作中,这意味着 Scala 支持人们依赖的相同 OO 基石——类、封装、重写——同时平滑了一些 JVM 时代的不便(例如大量 static 使用和冗长的 getter/setter)。
Scala 的函数式基础:不可变性与表达式
Scala 的函数式一面并不是一个独立的“模式”——它体现在语言引导你常用的默认做法。两个核心思想驱动着大部分实践:优先 不可变 数据,并把代码当作产生值的 表达式。
把不可变性当作默认心态(vals vs vars)
在 Scala 中,用 val 声明值,用 var 声明变量。两者都存在,但文化上的默认是 val。
当你使用 val 时,你是在说:“这个引用不会被重新赋值。”这个小小的选择减少了程序中的隐式状态量。更少的状态意味着当代码增长时更少的惊喜,尤其是在多个步骤的数据变换流程中,值不断被转换。
var 仍然有其位置——UI 粘合、计数器或性能关键的部分——但使用它应该是有意为之,而不是习惯性操作。
作为返回值的表达式(更少的逐步状态)
Scala 鼓励把代码写成返回结果的表达式,而不是主要通过改变状态的语句序列。
这通常表现为从更小的结果构建一个最终结果:
val discounted =
if (isVip) price * 0.9
else price
这里 if 是一个表达式,因此返回一个值。这种风格更容易理解“这个值是什么”,而不必追踪一连串赋值的来龙去脉。
日常代码中的高阶函数(map/filter)
替代修改集合的循环,Scala 代码通常对数据进行变换:
val emails = users
.filter(_.isActive)
.map(_.email)
filter 和 map 是高阶函数:它们接受其他函数作为输入。其好处并非纯理论上的——它带来清晰度:你可以把这个流水线读作一个小故事:保留活跃用户,然后提取邮箱地址。
纯函数为何有助于测试和推理
纯函数仅依赖输入且没有副作用(不进行隐藏写操作、不做 I/O)。当更多代码是纯的时,测试变得直接:给定输入,断言输出。推理也更简单,因为你不需要猜测系统其他地方还会改变什么。
Traits 与 Mixins:无需深层继承的可重用 OO
Scala 对“如何在不构建巨大家族树的情况下共享行为”的回答是 trait。trait 有点像接口,但它也可以包含真实实现——方法、字段和小的辅助逻辑。
trait 是什么(以及为何 Scala 倾向使用它们)
Traits 让你描述一种能力(“能记录日志”、“能验证”、“能缓存”),然后把这种能力附加到不同的类上。这鼓励把构件做得小而专注,而不是让每个人都继承一个臃肿的基类。
与单继承的类层次不同,traits 被设计成以受控的方式支持多重行为继承。你可以把多个 trait 加到一个类上,Scala 定义了明确的方法线性化(linearization)规则来解析方法调用顺序。
Mixins:用组合代替类树
当你“mix in” traits 时,你是在类边界处组合行为,而不是在继承树上往下钻。这通常更易维护:
- 你可以在无关类型间重用特性。
- 每个 trait 可以保持精简并易于测试。
- 通过添加/移除 mixin 来演进行为,通常比重构继承结构更简单。
一个简单示例:
trait Timestamped { def now(): Long = System.currentTimeMillis() }
trait ConsoleLogging { def log(msg: String): Unit = println(msg) }
class Service extends Timestamped with ConsoleLogging {
def handle(): Unit = log(s"Handled at ${now()}")
}
Traits 与抽象类:实用建议
使用 traits 的场景:
- 你想在许多类之间共享一种“能力”。
- 你预期会有多种行为组合。
- 你不需要构造函数参数(Scala 2 的限制;Scala 3 更灵活)。
使用 抽象类 的场景:
- 你需要构造参数或必须在一个地方初始化的内部状态。
- 你在建模一个较小且稳定的“is-a”关系层次。
真正的收益在于 Scala 让重用更像是组装零件,而不是继承注定的命运。
模式匹配与代数数据类型(ADT)
Scala 的模式匹配是让这门语言感觉“更函数式”的特性之一,尽管它同时支持经典的面向对象设计。与其把逻辑塞进一堆虚方法里,你可以 检查 一个值并根据其形状选择行为。
什么是模式匹配(以及它为何感觉很函数式)
最简单的情况,模式匹配就是更强大的 switch:它可以匹配常量、类型、嵌套结构,甚至把值的部分绑定到名字上。因为它是一个表达式,它自然会产生结果——常导致紧凑且可读的代码。
sealed trait Payment
case class Card(last4: String) extends Payment
case object Cash extends Payment
def describe(p: Payment): String = p match {
case Card(last4) => s"Card ending $last4"
case Cash => "Cash"
}
用 sealed trait 与 case class 建模数据
上面的例子也展示了 Scala 风格的 ADT:
sealed trait定义了一组封闭的可能性。case class和case object定义了具体变体。
“sealed” 是关键:编译器知道所有有效的子类型(在同一文件内),这为更安全的模式匹配打开了可能性。
让无效状态更难表达
ADT 鼓励你用显式的方式建模领域的真实状态。与使用 null、魔法字符串或可能组合出不可能状态的布尔值相比,你明确列出允许的情况。这使得许多错误在编码阶段就无法表达——因此也就不会滑入生产。
可读性上的好处(以及过度使用的场景)
当你在做以下事情时,模式匹配特别出色:
- 解码输入(例如把解析结果映射为成功/失败情况),
- 在工作流中处理不同的消息类型,
- 把“一个值可以是这些之一”翻译为“为每种情况做正确的事情”。
当每个行为都写成巨大的 match 块并散布在代码库各处时,模式匹配就会被滥用。如果匹配块变得庞大或无处不在,通常表示你需要更好的分解(辅助函数)或把部分行为移到数据类型本身附近。
类型系统:安全、推断与复杂性
Scala 的类型系统是团队选择它的主要原因之一,同时也是一些团队放弃它的主要原因之一。在最好的时候,它让你写出既简洁又有强校验的代码;在最差的情况下,你可能会觉得像在调试编译器。
类型推断为你带来了什么
类型推断意味着你通常不必在每处都写出类型。编译器常常能从上下文推断出类型。
这转化为更少的样板:你可以把注意力放在值的含义上,而不是不断标注它的类型。当你确实添加类型注解时,通常是为了在边界处(公共 API、复杂泛型)澄清意图,而不是每个局部变量都注解。
泛型与方差,用通俗语言说
泛型让你为多种类型写容器和工具(比如 List[Int] 和 List[String])。方差关系到当类型参数变化时,泛型类型是否可以被替换。
- 协变(
+A)大致意味着“猫的列表可以当做动物的列表使用”。 - 逆变(
-A)大致意味着“动物处理器可以当做猫处理器使用”。
这对库设计很强大,但初学时可能让人困惑。
通过隐式(Scala 2)和 givens(Scala 3)实现的类型类
Scala 推广了一种模式:可以在不修改类型的情况下,传入能力(capability)来“为类型添加行为”。例如,你可以定义如何比较或打印某个类型,并自动选取相应逻辑。
在 Scala 2 中用 implicit 实现;在 Scala 3 中用 given/using 更直接地表达。核心思想相同:以可组合的方式扩展行为。
缺点:错误信息与“过于聪明”的类型
权衡在于复杂性。类型级的技巧可能产生冗长的错误信息,过度抽象的代码会让新来者难以阅读。许多团队采用经验法则:用类型系统去简化 API 并防止错误,但避免让每个人都需要像编译器那样思考才能修改代码的设计。
常见的并发工具
Scala 有多条并发写法路径。这很有用——因为并非每个问题都需要相同级别的工具——但也意味着团队应对采用哪个方案保持审慎。
Futures:日常默认选择
对于许多 JVM 应用,Future 是以最简单的方式并发运行任务并组合结果的工具。你启动工作,然后用 map/flatMap 来构建异步工作流,而不阻塞线程。
一个良好的心理模型:Futures 适用于独立任务(API 调用、数据库查询、后台计算),你希望合并结果并在一个地方处理失败。
异步工作流:更可读的组合
Scala 允许你以更线性的风格表达 Future 链(通过 for 推导)。这并没有新增并发原语,但使意图更清晰并减少回调地狱。
权衡在于:仍然容易意外阻塞(例如等待 Future),或在没有区分 CPU 绑定与 IO 绑定工作时过载执行上下文。
流式处理:带背压的并发
对于长期运行的管道——事件、日志、数据处理——流式库(如 Akka/Pekko Streams、FS2 等)关注的是 流量控制。关键特性是背压:当消费者跟不上时,生产者会放慢速度。
这种模型通常优于“多次 spawn Futures”,因为它把吞吐与内存作为一等关注点。
Actor 风格并发:消息传递
Actor 库(Akka/Pekko)把并发建模为通过消息通信的独立组件。这可以简化对状态的推理,因为每个 actor 一次处理一条消息。
当你需要长期存在、带状态的进程(设备、会话、协调器)时,actor 很合适;而对于简单的请求/响应应用可能显得过重。
无论使用何种方式,不可变性都有效
不可变数据结构减少了共享可变状态——这是许多竞态条件的根源。即使在使用线程、Futures 或 actors 时,传递不可变值也能让并发错误更少、调试更轻松。
选择合适的级别
对简单并行工作先用 Futures;当需要可控吞吐时转向流;当状态与协调占主导地位时考虑 actors。
与 Java 协作:互操作性、库与 JVM 现实
Scala 最大的实际优势是它运行在 JVM 上,可以直接使用 Java 生态。你可以实例化 Java 类,实现 Java 接口,调用 Java 方法而几乎不显山露水——很多时候感觉你只是在用另一个 Scala 库。
从 Scala 调用 Java 库:哪些是简单的
大多数“顺利路径”的互操作是直接的:
- 使用现有 Java 库(数据库驱动、HTTP 客户端、日志)而不用等 Scala 专门版本。
- 在 Scala 中实现 Java 接口(例如 servlet API 或 Kafka 回调)。
- 与其他 JVM 服务共享构建工具与部署实践。
在运行时,Scala 编译为 JVM 字节码。从操作角度看,它像其他 JVM 语言一样运行:由相同运行时管理,使用相同的 GC,并用熟悉的工具进行性能分析与监控。
互操作会在哪些地方不顺畅
摩擦主要出现在 Scala 的默认与 Java 的默认不一致时:
Null。 许多 Java API 返回 null;Scala 代码偏好 Option。你常常需要防御性地封装 Java 结果以避免意外的 NullPointerException。
受检异常。 Scala 不强制你声明或捕获受检异常,但 Java 库可能抛出它们。这可能导致错误处理感受不一致,除非你标准化异常如何被翻译。
可变性。 Java 集合和以 setter 为主的 API 鼓励可变。在 Scala 中混合可变与不可变风格可能导致令人迷惑的代码,尤其在 API 边界处。
混合 Scala/Java 代码库的建议
把边界当作翻译层:
- 立即把 null 转为
Option,并仅在边界处再转换回 null。 - 把 Java 集合映射为团队统一使用的 Scala 集合类型。
- 把 Java 异常封装成领域错误(或统一的错误模型),以免调用者面对不可预测的失败模式。
- 对于面向 Java 的公共 API,保持简单并易于 Java 使用;对内部 Scala 模块则使用更地道的 Scala 风格。
做好这些工作后,互操作可以让 Scala 团队通过重用成熟的 JVM 库更快地推进,同时在服务内部保持更可表达和更安全的 Scala 代码。
团队实际感受到的权衡取舍
Scala 的宣称很吸引人:你可以把优雅的函数式代码写出来,在需要时保留 OO 结构,并留在 JVM 上。实际上,团队并不是简单地“得到 Scala”——他们会感受到一组日常权衡,这些在入职、构建和代码审查中显现出来。
学习曲线更陡(因为存在许多有效风格)
Scala 给了你很多表达能力:多种建模数据的方式、抽象行为的多种途径、多种组织 API 的方式。灵活性一旦形成共享心智模型就很高效——但在早期可能会拖慢团队速度。
新来者的困难往往不在语法,而在选择:“这该是 case class、普通 class 还是 ADT?”“我们使用继承、trait、类型类还是仅仅函数?”难点不在于 Scala 不可学——而是要就“什么是我们认同的 Scala”达成一致。
编译时间和构建复杂度是真实成本
随着项目增长或依赖宏/复杂库(在 Scala 2 中更常见),Scala 的编译往往比团队预期更重。增量构建能有所帮助,但编译时间仍是反复出现的实际问题:更慢的 CI、更慢的反馈循环,以及更大的压力去保持模块小且依赖干净。
构建工具也带来一层复杂性。无论你用 sbt 还是其他系统,都要关注缓存、并行度以及如何拆分子模块。这些不是学术问题——它们影响开发者幸福感和修复 Bug 的速度。
工具链与 IDE 支持:在承诺之前评估
Scala 的工具支持已有明显改进,但仍值得用你自己的堆栈做一次评估。在标准化之前,团队应该测试:
- IDE 在你的代码库规模下的性能(索引速度、导航、重构)
- 自动补全与类型提示的可靠性(对高级类型尤为重要)
- 在典型工作流中的调试体验
- CI 的稳定性(特别是依赖解析与缓存)
如果 IDE 表现不佳,语言的表达能力反而会适得其反:代码“正确”但难以探索,会增加维护成本。
风格一致性不是可选项
因为 Scala 同时支持 FP 与 OOP(以及许多混合写法),代码库可能会变成看起来像几种语言混合的产物。这通常是挫败的根源:问题不是 Scala 本身,而是约定不一。
约定与 linter 很重要,因为它们减少争论。提前决定团队认为什么是“好的 Scala”——如何处理不可变性、错误处理、命名、以及何时使用高级类型级模式。统一风格能让入职更顺畅,并把审查焦点放在行为而非审美上。
Scala 2 vs Scala 3:发生了什么变化及其意义
Scala 3(在开发期常称为 “Dotty”)并不是对 Scala 身份的改写——而是试图在保留 FP/OOP 混合的同时,抚平团队在 Scala 2 中遇到的尖锐问题。
语法与理念:更小的“表面面积”
Scala 3 保留了熟悉的基础,但推动代码更清晰的结构。
你会注意到带有显著缩进的可选大括号,它使日常代码更像现代语言而不是密集 DSL。它还把一些在 Scala 2 中“可能但混乱”的模式标准化——例如通过 extension 添加方法比使用一堆 implicit 技巧更直接。
在理念上,Scala 3 试图让强大特性显得更显式,从而使读者在不必记住大量约定的情况下也能看懂代码发生了什么。
为什么 implicits 和 enums 改变了
Scala 2 的 implicits 极为灵活:这对类型类和依赖注入很有利,但也导致令人困惑的编译错误和“远程行为”。
Scala 3 用 given/using 替代了大部分 implicit 使用。功能相似,但意图更清晰:given 表示“这是一个被提供的实例”,using 表示“这个方法需要一个实例”。这提高了可读性,使 FP 风格的类型类模式更易于追踪。
枚举也是一项重要改进。许多 Scala 2 团队用 sealed trait + case objects/classes 来建模 ADT。Scala 3 的 enum 提供了专门且整洁的语法——更少样板代码,但保留相同的建模能力。
迁移:团队实际的做法
多数真实项目通过交叉构建(同时发布 Scala 2 和 Scala 3 的制品)并逐模块迁移。
工具有所帮助,但仍需工作:源不兼容(尤其是 implicits 周围)、依赖宏-heavy 的库,以及构建工具链都会拖慢进度。好消息是:典型的业务代码比大量依赖编译器魔法的代码更容易迁移。
Scala 3 如何改变 FP/OOP 的平衡
在日常代码中,Scala 3 往往让 FP 模式感觉更“第一类”:类型类的连线更清晰、用 enum 写 ADT 更干净、以及在没有过多仪式的情况下使用更强的类型工具(如并/交类型)。
同时它并不抛弃 OO——traits、classes 与 mixin 组合依然很中心。不同之处在于 Scala 3 让“OO 结构”与“FP 抽象”之间的边界更明显,通常有助于团队长期保持代码一致性。
何时选择 Scala(何时不该选)
Scala 可以成为 JVM 上的“高功率”语言,但它不是默认的万金油。最大收益出现在问题受益于更强的建模与更安全的组合时,并且团队准备好有意识地使用这门语言。
适合的场景
数据密集型系统与管道。 如果你在做大量的变换、校验和增强(流、ETL、事件处理),Scala 的函数式风格和强类型能让这些变换更显式、更少出错。
复杂领域建模。 当业务规则细致复杂(定价、风控、资格判定、权限)时,Scala 用类型表达约束和构建小而可组合的组件的能力能减少“if-else 爆炸”,并让无效状态更难以表示。
已经投入 JVM 的组织。 如果你的世界已依赖 Java 库、JVM 工具与运维实践,Scala 能在不离开生态的情况下带来 FP 风格的可用性。
团队准备度:比语言本身更重要的因素
Scala 奖励一致性。团队通常在具备以下条件时更容易成功:
- 对函数式概念有一定熟悉(不可变性、近似纯函数、组合)
- 有代码审查文化,强调可读性胜过技巧性写法
- 有共享的风格指南和约定(如何建模错误、如何组织模块、何时使用高级类型)
没有这些,代码库可能会滑入多种风格并存的混乱状态,新来者难以跟进。
不该选 Scala 的情形
需要快速上手的小团队。 如果你预计频繁交接、许多少年贡献者或快速的人力变动,学习曲线与多样化的写法会拖慢速度。
简单的 CRUD 服务。 对于简单的“接收请求/写入记录”的服务,如果领域复杂度低,Scala 的好处可能不足以抵消构建工具、编译时间与风格决策的成本。
一个简易决策检查表
问自己:
- 我们是否在建模复杂规则或做大量变换?
- 我们是否会从更强的编译期保证中受益?
- 我们是否已经依赖 JVM 库与运维?
- 我们能否承诺一套清晰的风格指南并进行严格审查?
- 团队是否愿意学习并限制 Scala 的高级特性?
如果大多数答案是“是”,Scala 往往是很合适的选择。如果不是,更简单的 JVM 语言可能更快产出结果。
一个实用建议:在评估语言时把原型循环做短。例如,有的团队会使用类似 Koder.ai 之类的快速原型平台,从基于聊天的规范快速生成一个小型参考应用(API + 数据库 + UI),在规划阶段反复迭代,并用快照/回滚来快速探索替代方案。即便你的生产目标是 Scala,拥有一个可导出源码并与其他 JVM 实现比较的快速原型,也能让“是否选择 Scala”这个讨论更加具体——基于工作流、部署和可维护性,而不仅仅是语言特性。
常见问题
Scala 最初在 JVM 上试图解决什么问题?
Scala 的设计目标是减少典型 JVM 开发中的痛点——样板代码过多、与 null 有关的错误、以及脆弱的继承驱动设计——同时保留 JVM 的性能、工具链和库访问能力。目标是在不离开 Java 生态的前提下,更直接地表达领域逻辑。
在真实的 Scala 项目中,函数式编程和面向对象的混合有什么帮助?
在实际项目中,用 OO 来划定清晰的模块边界(API、封装、服务接口),在这些边界内部使用 FP 技术(不可变性、表达式导向代码、接近纯函数的写法)可以减少隐式状态,使行为更容易测试和修改。
什么时候在 Scala 中使用 val 而不是 var?
默认应优先使用 val,以避免意外重赋值并减少隐藏状态。当确实需要时(如性能敏感的紧循环或 UI 粘合代码),再有意识地使用 var。尽量把可变性限制在局部和受控范围内,不要把它带入核心业务逻辑。
什么时候应该选择 trait 而不是抽象类?
Trait 是可重用的“能力”组件,可以混入到许多类中,避免构建深而脆弱的继承树。
- 使用 trait:当你想在不相关类型间共享行为,并希望灵活组合时。
- 使用 abstract class:当你需要构造函数参数或集中初始化/状态(在 Scala 2 中尤其常见)时。
ADT(代数数据类型)和模式匹配如何让 Scala 代码更安全?
用 sealed trait 结合 case class/case object 建模一个封闭的状态集合,然后用 match 处理每个分支。
这种方式使得无效状态更难表达,也能让编译器在新增分支时提醒未处理的情况,从而提高安全性。
Scala 的类型推断带来什么好处,什么时候应当添加类型注解?
类型推断减少了重复的类型注解,使代码更简洁,同时仍然有静态类型检查。
通常的做法是在 边界处添加显式类型(公共方法、模块 API、复杂泛型),以便提高可读性并稳定编译错误,而不是给每个局部变量都加注解。
用通俗的话解释 Scala 中的协变和逆变是什么意思?
方差描述泛型类型在子类型关系下的替换规则。
- 协变(
+A):容器可以“放宽”,例如List[Cat]可被视为List[Animal]。 - 逆变(
-A):消费者/处理器可以被放宽,例如Handler[Animal]可以用在需要Handler[Cat]的地方。
这些概念在设计接受或返回泛型类型的库/API 时会特别明显。
隐式(Scala 2)和 given/using(Scala 3)是用来做什么的?
它们是实现类型类风格设计的机制:在不修改原始类型的情况下“从外部”提供行为。
- Scala 2:使用
implicit。 - Scala 3:使用
given/using。
Scala 3 更清晰地表达了“谁提供了实例”和“谁需要实例”的意图,通常能提高可读性并减少“远程作用”的问题。
在 Scala 中如何在 Futures、流和 actor 之间选择并发模型?
从简单到复杂逐步选择:
- Futures:适合独立任务的并发和异步组合(API 调用、DB 查询、后台计算)。
- Streaming(有背压):适合长期运行的流水线(事件、日志、数据处理),能控制吞吐和内存。
- Actors/消息传递:适合需要长期存在且有状态、以消息协调的组件(设备、会话、协调器)。
无论哪种方式,传递不可变数据都会显著降低竞态问题的概率。
在混合的 Scala–Java 代码库中有哪些最佳实践?
把 Java/Scala 边界当作翻译层:
- 发现 Java 返回的
null时,立即转换为Option,并且只在边界处再把Option转回 null。 - 把 Java 集合映射为团队约定的 Scala 集合类型。
- 将 Java 异常映射到一致的错误模型,以避免调用方面对不一致的失败模式。
- 对外暴露给 Java 的 API 保持简单、Java 友好;内部模块可以采用更符合 Scala 的风格。
这样可以让互操作性可预测,防止 Java 的默认(null、可变)蔓延到代码库全部位置。