Scala 类为什么看起来更简洁
Scala 同样支持完整的面向对象编程,但它把很多在 Java 和 C# 里需要展开写的样板代码,直接压缩进了类定义本身。对刚接触 Scala 的读者来说,最容易困惑的地方通常不是“能不能写类”,而是“构造器参数、字段、方法和初始化代码到底各放在哪里”。
这篇文章按实际使用顺序梳理 Scala 类的几个核心点:先看最基础的类和主构造器,再看 var 与 val 的差别,接着理解类体结构、辅助构造器、默认参数和命名参数,最后补上类中方法的基本写法。看完之后,你可以更清楚地判断哪些写法适合传统面向对象场景,哪些写法更符合 Scala 的简洁风格。
主构造器怎么定义,参数为什么能直接当字段用
Scala 最常见的类定义方式,是直接把构造器参数写在类名后面。下面这个例子定义了一个带两个参数的 Person 类:
class Person(var firstName: String, var lastName: String)
// Use Class Person
val person = new Person("Andrew", "Li")
println(person.firstName + " " + person.lastName) // Andrew Li
这里最关键的一点是:构造器参数前面如果写了 var 或 val,编译时会自动把它们变成类中的字段。因此在创建对象后,可以直接通过 person.firstName 和 person.lastName 访问。
如果参数使用的是 var,字段就是可修改的:
person.firstName = "Shuai"
println(person.firstName + " " + person.lastName) // Shuai Li
这也是 Scala 类定义显得更紧凑的原因之一:很多字段声明、构造器赋值和基础访问逻辑,都被合并到了这一行里。
和 Java 对比,Scala 省掉了哪些样板代码
把同一个简单类分别用 Scala 和 Java 表达,差异会非常直观。
// Create a class in Scala
class Person(var firstName: String, var lastName: String)
// Create a class in Java
public class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
这个对比说明的不是 Scala 功能更少,而是它把“字段 + 构造器 + 基本访问”这种高频写法做了语法收敛。对于数据结构简单、以传值和封装为主的类,这种写法会明显更省代码。
构造器里用 var 还是 val
前面的 Person 使用的是 var,所以字段可变。如果改成 val,字段就变成只读:
class Person(val firstName: String, val lastName: String)
// Use Person class
val person = new Person("Andrew", "Li")
person.firstName = "Shuai" // error: reassignment to val
这里的报错 error: reassignment to val 很直接:val 一旦初始化,就不能再次赋值。
从使用场景看,可以这样理解:
- 如果类主要服务于传统面向对象建模,字段需要在对象生命周期中被修改,
var更方便。 - 如果数据创建后就不应该变化,
val会更稳妥。
原文还提到一个很重要的判断:在 Scala 的函数式编程风格里,通常更常见的是 case class,而不是普通 class。因为函数式编程更强调不可变数据,不可变对象往往更容易保证程序健壮性。
一个 Scala 类内部通常包含什么
理解 Scala 类,不能只看构造器参数。一个完整的类通常还包含下面几部分:
- 类构造器参数
- 类主体中定义的方法
- 在对象初始化时会直接执行的语句和表达式
也就是说,类体并不只是“放方法的地方”,它本身也可以包含字段定义和初始化逻辑。下面这个例子把这些元素放在了一起:
class Person(var firstName: String, var lastName: String) {
println("the constructor begins")
// 'public' access by default
var age = 0
// some class fields
private val AMOUNT_OF_EYES = 2
// some methods
override def toString(): String = s"$firstName $lastName is $age years old"
def printAmountOfEyes(): Unit = println(s"AMOUNT_OF_EYES = $AMOUNT_OF_EYES")
def printDescription(): Unit = println(this)
printAmountOfEyes()
printDescription()
println("you've reached the end of the constructor")
}
创建对象时:
var person = new Person("Andrew", "Li")
// When executing the above code to init person object, we can see those logs as follows:
// the constructor begins
// AMOUNT_OF_EYES = 2
// Andrew Li is 0 years old
// you've reached the end of the constructor
这个例子可以帮助你抓住两个关键事实:
类体中的语句会在 new 时立即执行
println("the constructor begins")、printAmountOfEyes()、printDescription() 这些语句不是普通说明代码,而是在 new Person(...) 时就会执行的初始化逻辑。
字段、方法和访问控制可以直接写在类体中
例如 var age = 0 是实例字段,private val AMOUNT_OF_EYES = 2 是私有字段,def printDescription(): Unit 则是实例方法。Scala 默认访问级别是公开的,因此原例特别用注释标出了 // 'public' access by default。
辅助构造器怎么写,限制是什么
除了主构造器,Scala 还支持通过 this 定义辅助构造器。它适合在不同入参形式下创建同一个类实例:
// the primary constructor
class Person(var name: String, var age: Int) {
// one-arg auxiliary constructor
def this(age: Int) = {
this("default name", age)
}
// one-arg auxiliary constructor
def this(name: String) = {
this(name, 0)
}
// zero-arg auxiliary constructor
def this() = {
this("default name", 0)
}
override def toString = s"The person called $name is $age years old"
}
写辅助构造器时,需要注意两条规则:
- 每个辅助构造器都必须有不同的方法签名,最常见的区别就是参数列表不同。
- 每个辅助构造器都必须调用一个已经定义好的构造器。
第二条尤其重要。也就是说,辅助构造器不能随意绕开已有初始化流程,它必须通过 this(...) 把调用链收束到某个已定义构造器上,最终回到主构造器。
默认参数很多时候比多个构造器更实用
虽然辅助构造器能解决不同初始化方式的问题,但在 Scala 里,很多时候更常用的是“给构造器参数设置默认值”。
class Socket(var timeout: Int = 2000, var linger: Int = 3000) {
override def toString = s"timeout: $timeout, linger: $linger"
}
这样定义后,可以用多种方式创建对象:
new Socket()
new Socket(1000)
new Socket(4000, 6000)
这种写法的价值很明显:
- 可以提前给参数提供首选默认值。
- 调用方可以只覆盖自己关心的那部分配置。
因此,原文也特别提醒:辅助构造器是有用的特性,但因为 Scala 支持默认参数,很多场景下并不需要专门写多个构造器。
命名参数让调用点更容易读懂
当参数较多,或者多个参数类型相近时,命名参数能明显提升可读性:
val socket = new Socket(linger=3000, timeout=2000)
val socket = new Socket(timeout=2000)
val socket = new Socket(linger=3000)
相比之下,下面这种位置参数调用虽然更短,但读代码的人需要自己记住参数顺序:
val socket = new Socket(2000, 3000)
如果参数名本身已经能表达业务含义,那么命名参数在维护阶段通常会更占优势。
Scala 类中的方法怎么定义
Scala 中的方法定义在类里,基础写法如下:
def double(a: Int) = a * a
// Explicitly declare the return value
def double(a: Int): Int = a * a
这里展示的是同一个方法的两种写法:一种省略返回值类型,一种显式声明返回值类型。Scala 可以根据方法体做类型推断,但是否省略,要看代码复杂度和可读性。
一个方法通常包含这些部分:
def:声明方法的关键字double:方法名,最好直接体现业务语义a: Int:参数名和参数类型: Int:显式声明的返回值类型a * a:方法体
如果方法体只有一行,可以直接写在 = 后面;如果方法体有多行,就使用花括号:
def addThenDouble(a: Int, b: Int): Int = {
val sum = a + b
val doubled = sum * 2
doubled
}
实际开发里,一个简单判断原则是:
- 方法体很短、返回类型一眼可见时,可以依赖类型推断。
- 方法逻辑较复杂,或者返回值是自定义类型时,显式写出返回类型更利于阅读和维护。
怎么理解这组 Scala 类语法
把整篇内容串起来看,Scala 类的核心优势在于“用更少的语法表达完整的对象结构”。主构造器参数可以直接成为字段,类体里既能放字段和方法,也能放初始化时立即执行的语句;在对象创建方式上,默认参数和命名参数通常比堆叠多个辅助构造器更简洁。
如果你正在从 Java 过渡到 Scala,最值得先建立的习惯有两个:一是先想清楚字段到底该用 var 还是 val,二是优先考虑默认参数和命名参数,减少不必要的构造器重载。这样更容易写出既符合 Scala 风格、又便于维护的类定义。
















