Scala语法
本文基于 Scala 2.13
https://docs.scala-lang.org/tour/tour-of-scala.html
import
import 路径.目标 // 基础导入
import 路径.{成员1, 成员2} // 选择性导入
import 路径.{旧名 => 新名} // 重命名导入
import 路径._ // 通配导入(类似 Java 的 *)
import 可在任何作用域使用(包、类、方法、代码块内),作用域内有效。Scala 中 package 是目录结构映射,但导入时支持 “相对路径”(基于当前包)或 “绝对路径”(以 root 开头,跳过当前包层级)。
注意:通配导入可能导致命名冲突(如同时导入 java.util.List 和 scala.collection.immutable.List),尽量避免在顶层滥用,可在局部作用域(如方法内)使用。
导入时排除成员(过滤不需要的成员)
// 导入 java.util 包下所有成员,但排除 Date 类(避免冲突)
import java.util.{Date => _, _}
val map = new HashMap[String, Int]() // 可用
// val date = new Date() // 编译报错:Date 已被排除
导入作用域
import 的作用域由声明位置决定,遵循 “就近原则”(局部导入覆盖外层导入):
- 顶层导入:在包声明后、类 / 对象定义前,作用于整个文件。
- 类 / 对象内导入:作用于整个类 / 对象。
- 方法 / 代码块内导入:仅作用于该方法 / 代码块(局部生效,推荐用于避免冲突)。
package com.example
// 顶层导入:作用于整个文件
import scala.collection.immutable.List
class Demo {
// 类内导入:作用于整个 Demo 类
import java.util.HashMap
def test(): Unit = {
// 方法内导入:仅作用于 test 方法(局部生效)
import scala.collection.mutable.ListBuffer
val list = List(1, 2) // 顶层导入的 immutable.List
val map = new HashMap[String, Int]() // 类内导入的 HashMap
val buffer = ListBuffer(3, 4) // 方法内导入的 ListBuffer
}
}
Object
https://docs.scala-lang.org/tour/singleton-objects.html
object 关键字用于定义单例对象
object Circle {
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
}
伴生对象
一个与类同名的对象被称为伴生对象。反之,该类即为该对象的伴生类。伴生类或对象可以访问其伴生对象的私有成员。使用伴生对象来封装那些不特定于伴生类实例的方法和值。
伴生对象可以访问伴生类的私有成员,反之亦然。
class Person(val name: String, val age: Int) {
// 伴生类的私有成员
private var secret: String = "This is a secret"
def revealSecret(): String = secret
}
object Person {
// 伴生对象的工厂方法
def apply(name: String, age: Int): Person = new Person(name, age)
// 伴生对象的静态方法
def greet(person: Person): String = s"Hello, ${person.name}!"
// 隐式转换
implicit def stringToPerson(name: String): Person = new Person(name, 0)
}
// 使用伴生对象
object Main extends App {
// 使用工厂方法创建实例
val john = Person("John", 30)
println(Person.greet(john)) // 输出:Hello, John!
// 使用隐式转换
val jane: Person = "Jane" // 隐式转换
println(Person.greet(jane)) // 输出:Hello, Jane!
}
在Java中,静态成员在Scala中就是伴生对象的普通成员。
当从Java代码中使用伴生对象时,成员将被定义在带有静态修饰符的伴生类中。这称为静态转发。即使没有定义伴生类,这种情况也会发生。
Case Class
https://docs.scala-lang.org/tour/case-classes.html
主要特性:
- 不可变性:case class 的实例默认是不可变的(immutable),一旦创建就不能修改。
- 自动生成的方法:Scala 自动为 case class 生成以下方法:
equals和hashCode方法:用于比较实例。toString方法:提供友好的字符串表示。copy方法:用于创建实例的副本,可以在副本中修改某些属性。apply方法:允许使用不带new关键字来创建实例。
- 模式匹配:case class 可以与模式匹配结合使用,非常方便。
参数默认是 val:case class 的构造参数默认是 val,这意味着它们是不可变的。如果需要可变参数,可以显式指定为 var,但不推荐这样做。
简化构造:使用 case class 时可以省略 new 关键字来创建实例,代码更简洁。
// 定义一个 case class
case class Person(name: String, age: Int)
object Main extends App {
// 创建实例
val john = Person("John", 30)
val jane = Person("Jane", 25)
// 打印实例
println(john) // 输出:Person(John,30)
println(jane) // 输出:Person(Jane,25)
// 使用 equals 方法
println(john == Person("John", 30)) // 输出:true
// 使用 copy 方法
val olderJohn = john.copy(age = 31)
println(olderJohn) // 输出:Person(John,31)
// 模式匹配
john match {
case Person(name, age) => println(s"Name: $name, Age: $age")
}
}
Package Object
Package Object 是一种特殊的对象,它允许在一个包范围内定义共享的变量、函数和类型。这种机制可以帮助你组织代码,避免在多个文件中重复定义相同的内容。
特点
- 共享内容:Package Object 中的成员可以被包内的所有类和对象访问,无需导入。
- 与包关联:Package Object 在定义时与包相结合,使用
package关键字。 - 避免命名冲突:可以有效地组织代码,减少命名冲突的风险。
一个目录或者说一个包下面只能有一个 Package Object
package scala
package object math {
...
}
implicit
https://www.artima.com/pins1ed/implicit-conversions-and-parameters.html
implicit parameter
implicit关键字加在函数参数上,函数可以有2个参数列表,一个是普通的参数列表,另一个是隐式参数列表,例如:
// probably in a library
class Prefixer(val prefix: String)
def addPrefix(s: String)(implicit p: Prefixer) = p.prefix + s
// then probably in your application
implicit val myImplicitPrefixer = new Prefixer("***")
// 调用函数时不需要传隐式参数列表的参数,会自动从当前上下文中取对应的类型
addPrefix("abc") // returns "***abc"
调用时若未显式传入,Scala 会自动在当前作用域(包括当前类、父类、导入的包 / 对象)中查找「类型匹配的隐式值」并自动注入。
关键规则
- 隐式参数必须放在最后一个参数列表中(便于区分普通参数和隐式参数);
- 作用域内只能有「一个类型匹配的隐式值」(否则编译报错 “歧义”);
- 隐式值的查找优先级:当前局部作用域 > 导入的隐式 > 父类 / 伴生对象中的隐式。
implicit function
implicit关键字加在函数声明上,函数就变成了隐式函数。例如:
implicit def doubleToInt(d: Double) = d.toInt
这种函数有什么用呢,举个例子:
val x: Int = 42.0 // 使用Int接收Double类型,会报错类型不匹配
当编译器发现上下文需要的表达式类型不匹配时,它会寻找能使其类型检查通过的隐式函数值。例如,若需A类型却检测到B类型,编译器会查找作用域内是否存在B => A类型的隐式值(如果存在B和A的伴生对象的话,同时还会检查伴生对象等其他位置)。
所以,如果当前上下文中存在doubleToInt函数,那么编译就能通过了
implicit def doubleToInt(d: Double) = d.toInt
val x: Int = 42.0 // 使用 doubleToInt 函数进行隐式转换
// 等效于直接调用函数进行转换
val x: Int = doubleToInt(42.0)
核心用途:扩展现有类的功能(无需继承或修改原类)、解决类型不兼容问题。让一个类型自动转换为另一个类型,从而为原类型 “补充方法” 或 “适配不同接口”。
// 隐式函数:将 Int 转换为 String
implicit def intToString(n: Int): String = n.toString
// 原 Int 类型本身没有 concat 方法,但转换后可调用 String 的 concat
val num: Int = 123
val result: String = num.concat("456") // 等价于 intToString(123).concat("456")
println(result) // 输出:123456
implicit class
Scala 2.10 引入的一个特性:隐式类
创建隐式类时,只需要在对应的类前加上implicit关键字。
object Helpers {
implicit class IntWithTimes(x: Int) {
def times[A](f: => A): Unit = {
def loop(current: Int): Unit =
if(current > 0) {
f
loop(current - 1)
}
loop(x)
}
}
}
隐式类必须包含一个主构造函数,其第一个参数列表中仅有一个参数。它还可以包含一个额外的隐式参数列表。隐式类必须定义在允许方法定义的作用域内(而非顶层)。隐式类会被转换为一个类与隐式方法的配对,其中隐式方法模拟了类的构造函数
生成的隐式方法将与隐式类同名。这样可以通过类名导入隐式转换,就像预期的其他隐式定义一样。
使用隐式类时,类名必须在当前作用域内可见且无歧义,这一要求与隐式值等其他隐式类型转换方式类似。
隐式类有以下限制条件:
- 只能在别的trait/类/对象内部定义。
- 构造函数只能携带一个非隐式参数
- 虽然可以创建带有多个非隐式参数的隐式类,但这些类无法用于隐式转换。
- 在同一作用域内,不能有任何方法、成员或对象与隐式类同名。这意味着隐式类不能是case class
implicit的问题
过度使用implicit会增加代码隐蔽性,复杂场景(如多层隐式转换)可能难以调试;
import package1._
import package2._
import package3._
import package4._
object HelloWorld {
case class Text(content: String)
case class Prefix(text: String)
implicit def String2Text(content: String)(implicit prefix: Prefix) = {
Text(prefix.text + " " + content)
}
def printText(text: Text): Unit = {
println(text.content)
}
def main(args: Array[String]): Unit = {
printText("World!")
}
}
假设上面导入的包中恰好存在一个隐式变量,但是你并不知道
// Best to hide this line somewhere below a pile of completely unrelated code.
// Better yet, import its package from another distant place.
implicit val prefixLOL = Prefix("Hello")
因此你在不知情的情况下调用String2Text函数,并传入参数,并预期返回你想要的结果World,如下所示:
val res = String2Text("World") // 实际上会返回Hello World, 这是一个意料之外的行为

浙公网安备 33010602011771号