30 分钟速成 Scala

0. 前言

要想 30 分钟速成,必须要有面向对象的经验,不然可能得花一个小时来适应。这个教程将涵盖以下内容:

  1. 变量(Variables)
  2. 函数(Functions)
  3. 类(Classes)
  4. 继承(Inheritance)
  5. 方法(Methods)
  6. 对象(Objects)
  7. 伴生对象(Companion Objects)
  8. 特质(Traits)

每个部分都会包含简要的解释和示例代码,帮助你快速掌握 Scala 的基本语法和概念。


1. 变量(Variables)

在 Scala 中,变量分为两种类型:可变变量不可变变量

1.1 不可变变量 (val)

使用 val 声明的变量是不可变的,一旦赋值后不能再改变。

1
2
3
4
5
val name: String = "Alice"
println(name) // 输出: Alice

// 尝试重新赋值会导致编译错误
// name = "Bob" // 错误: reassignment to val

1.2 可变变量 (var)

使用 var 声明的变量是可变的,可以在之后重新赋值。

1
2
3
4
5
var age: Int = 25
println(age) // 输出: 25

age = 30
println(age) // 输出: 30

1.3 类型推断

Scala 具有强大的类型推断功能,可以根据赋值自动推断变量类型,无需显式声明。

1
2
3
4
5
val greeting = "Hello, World!" // 自动推断为 String
val number = 42 // 自动推断为 Int

println(greeting) // 输出: Hello, World!
println(number) // 输出: 42

2. 函数(Functions)

函数是 Scala 中的基本构建块,可以定义在类内部或外部。Scala 支持高阶函数,即可以将函数作为参数传递或返回。

2.1 定义函数

使用 def 关键字定义函数。

1
2
3
4
5
6
def add(a: Int, b: Int): Int = {
a + b
}

val result = add(3, 5)
println(result) // 输出: 8

2.2 函数参数和返回类型

Scala 函数的参数和返回类型需要明确声明,但如果函数体非常简单,可以省略花括号。

1
2
3
def multiply(a: Int, b: Int): Int = a * b

println(multiply(4, 6)) // 输出: 24

2.3 匿名函数(Lambda 表达式)

Scala 支持匿名函数,可以在需要函数作为参数的地方使用。

1
2
val square = (x: Int) => x * x
println(square(5)) // 输出: 25

2.4 高阶函数

高阶函数是接受函数作为参数或返回函数的函数。

1
2
3
4
5
6
7
8
9
def applyFunction(f: Int => Int, value: Int): Int = {
f(value)
}

val increment = (x: Int) => x + 1
println(applyFunction(increment, 10)) // 输出: 11

// 直接传递匿名函数
println(applyFunction(x => x * 2, 7)) // 输出: 14

3. 类(Classes)

类是面向对象编程的核心,用于定义对象的属性和行为。

3.1 定义类

1
2
3
4
5
6
7
8
class Person(val name: String, var age: Int) {
def greet(): Unit = {
println(s"Hello, my name is $name and I am $age years old.")
}
}

val alice = new Person("Alice", 30)
alice.greet() // 输出: Hello, my name is Alice and I am 30 years old.

3.2 构造器

Scala 的类可以有主构造器和辅助构造器。

主构造器

在类名后面的参数列表即为主构造器。

1
2
3
4
5
6
class Rectangle(val width: Double, val height: Double) {
def area(): Double = width * height
}

val rect = new Rectangle(5.0, 3.0)
println(rect.area()) // 输出: 15.0

辅助构造器

使用 def this(...) 定义辅助构造器。

1
2
3
4
5
6
7
8
9
class Point(val x: Int, val y: Int) {
def this(x: Int) = this(x, 0) // 辅助构造器,默认 y = 0
}

val p1 = new Point(5, 10)
println(s"Point1: (${p1.x}, ${p1.y})") // 输出: Point1: (5, 10)

val p2 = new Point(7)
println(s"Point2: (${p2.x}, ${p2.y})") // 输出: Point2: (7, 0)

4. 继承(Inheritance)

Scala 支持类的继承,允许一个类继承自另一个类,获取其属性和方法。

4.1 基类和子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 基类
class Animal {
def speak(): Unit = {
println("Animal makes a sound.")
}
}

// 子类
class Dog extends Animal {
override def speak(): Unit = {
println("Dog barks.")
}
}

val genericAnimal = new Animal()
genericAnimal.speak() // 输出: Animal makes a sound.

val dog = new Dog()
dog.speak() // 输出: Dog barks.

4.2 构造器和继承

子类在构造时需要调用基类的构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Vehicle(val brand: String, val model: String) {
def info(): Unit = {
println(s"Vehicle: $brand $model")
}
}

class Car(brand: String, model: String, val doors: Int) extends Vehicle(brand, model) {
override def info(): Unit = {
super.info()
println(s"Number of doors: $doors")
}
}

val car = new Car("Toyota", "Corolla", 4)
car.info()
// 输出:
// Vehicle: Toyota Corolla
// Number of doors: 4

5. 方法(Methods)

方法是在类或对象中定义的函数,用于描述对象的行为。

5.1 定义方法

1
2
3
4
5
6
7
8
class Calculator {
def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
}

val calc = new Calculator()
println(calc.add(10, 5)) // 输出: 15
println(calc.subtract(10, 5)) // 输出: 5

5.2 方法的访问修饰符

Scala 的方法默认是 public 的,可以使用 privateprotected 修饰符限制访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
class SecretAgent {
private def secretMethod(): Unit = {
println("This is a secret method.")
}

def revealSecret(): Unit = {
secretMethod()
}
}

val agent = new SecretAgent()
// agent.secretMethod() // 编译错误: secretMethod is private
agent.revealSecret() // 输出: This is a secret method.

5.3 方法重载

在同一个类中,可以定义多个同名但参数不同的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Printer {
def print(message: String): Unit = {
println(s"String: $message")
}

def print(number: Int): Unit = {
println(s"Int: $number")
}
}

val printer = new Printer()
printer.print("Hello") // 输出: String: Hello
printer.print(100) // 输出: Int: 100

6. 对象(Objects)

在 Scala 中,object 用于定义单例对象,即在整个程序中只有一个实例。对象通常用于存放工具方法或作为应用程序的入口点。

6.1 定义对象

1
2
3
4
5
6
7
object MathUtils {
def square(x: Int): Int = x * x
def cube(x: Int): Int = x * x * x
}

println(MathUtils.square(4)) // 输出: 16
println(MathUtils.cube(3)) // 输出: 27

6.2 伴生对象(Companion Objects)

伴生对象是与类同名的对象,且必须在同一个源文件中定义。它们可以相互访问 private 成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person(private val name: String) {
private def greetPrivate(): Unit = {
println(s"Hello, my name is $name.")
}

def greet(): Unit = {
greetPrivate()
}
}

object Person {
def createPerson(name: String): Person = {
val person = new Person(name)
// person.greetPrivate() // 错误:greetPrivate 是 private
person.greet() // 正确:通过公共方法调用
person
}
}

val john = Person.createPerson("John")
// 输出: Hello, my name is John.

7. 伴生对象(Companion Objects)

伴生对象与伴生类共享名称,并且可以访问彼此的 private 成员。常用于定义工厂方法和静态成员。

7.1 定义伴生对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Rectangle(val width: Double, val height: Double) {
def area(): Double = width * height
}

object Rectangle {
def apply(width: Double, height: Double): Rectangle = new Rectangle(width, height)

def unitSquare(): Rectangle = new Rectangle(1.0, 1.0)
}

val rect1 = Rectangle(5.0, 3.0) // 调用 apply 方法
println(rect1.area()) // 输出: 15.0

val rect2 = Rectangle.unitSquare()
println(rect2.area()) // 输出: 1.0

7.2 使用 apply 方法

apply 方法允许你像调用函数一样创建类的实例,而无需使用 new 关键字。

1
2
3
4
5
6
7
8
9
10
class Circle(val radius: Double) {
def area(): Double = math.Pi * radius * radius
}

object Circle {
def apply(radius: Double): Circle = new Circle(radius)
}

val circle = Circle(2.5) // 实际调用了 Circle.apply(2.5)
println(circle.area()) // 输出: 19.634954084936208

8. 特质(Traits)

特质类似于 Java 中的接口,可以包含抽象方法和具体方法。类可以通过 extendswith 关键字混入多个特质,实现多重继承的效果。

8.1 定义和使用特质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
trait Greeter {
def greet(name: String): Unit = {
println(s"Hello, $name!")
}
}

trait Logger {
def log(message: String): Unit = {
println(s"LOG: $message")
}
}

class FriendlyPerson extends Greeter with Logger {
def sayHello(name: String): Unit = {
greet(name)
log(s"Said hello to $name.")
}
}

val person = new FriendlyPerson()
person.sayHello("Alice")
// 输出:
// Hello, Alice!
// LOG: Said hello to Alice.

8.2 抽象方法

特质可以包含抽象方法,子类需要实现这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
trait Animal {
def makeSound(): Unit // 抽象方法
}

class Cat extends Animal {
def makeSound(): Unit = {
println("Meow")
}
}

val kitty = new Cat()
kitty.makeSound() // 输出: Meow

完整示例

下面是一个综合示例,展示了变量、函数、类、继承、方法、对象、伴生对象和特质的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 定义特质
trait Logger {
def log(message: String): Unit = {
println(s"LOG: $message")
}
}

// 定义类
class Vehicle(val brand: String, val model: String) {
def info(): Unit = {
println(s"Vehicle: $brand $model")
}
}

// 定义继承自 Vehicle 的子类
class Car(brand: String, model: String, val doors: Int) extends Vehicle(brand, model) with Logger {
override def info(): Unit = {
super.info()
println(s"Number of doors: $doors")
log("Car info displayed.")
}
}

// 定义伴生对象
object Car {
def apply(brand: String, model: String, doors: Int): Car = new Car(brand, model, doors)

def createDefaultCar(): Car = Car("Toyota", "Corolla", 4)
}

// 定义对象作为应用入口
object TestApp extends App {
val myCar = Car("Honda", "Civic", 2) // 调用 apply 方法
myCar.info()
// 输出:
// Vehicle: Honda Civic
// Number of doors: 2
// LOG: Car info displayed.

val defaultCar = Car.createDefaultCar()
defaultCar.info()
// 输出:
// Vehicle: Toyota Corolla
// Number of doors: 4
// LOG: Car info displayed.

// 使用函数
def calculateArea(width: Double, height: Double): Double = width * height
println(s"Area: ${calculateArea(5.0, 3.0)}") // 输出: Area: 15.0
}

总结

通过这个入门教程,你应该已经了解了 Scala 的以下基本概念:

  • 变量val(不可变)和 var(可变)。
  • 函数:定义、参数、返回类型、匿名函数和高阶函数。
  • :定义类、构造器和实例化。
  • 继承:类的继承、方法重写和调用基类方法。
  • 方法:定义方法、访问修饰符和方法重载。
  • 对象:单例对象的定义和使用。
  • 伴生对象:与类同名的对象,用于工厂方法和静态成员。
  • 特质:类似于接口,可以混入多个特质实现多重继承。

一些问题

当然,我会认真回答你的问题,并尽量详细解释每个概念之间的关系和区别。

1. Scala 子类调用父类方法 vs Java 中的 super.func1()

问题:Scala 的子类可以直接调用父类中的方法,这本质上是不是相当于 Java 中的 super.func1()?

回答

在 Scala 中,子类确实可以直接调用父类的方法,这在功能上类似于 Java 中的 super.func1()。然而,两者在语法和一些细节上有些不同。

Scala 中的父类方法调用

在 Scala 中,当子类继承自父类时,可以直接调用父类的方法,无需显式使用 super 关键字,除非你需要调用被子类重写的方法。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 父类定义
class Animal {
def speak(): Unit = {
println("Animal makes a sound.")
}
}

// 子类继承父类并重写方法
class Dog extends Animal {
override def speak(): Unit = {
println("Dog barks.")
}

def parentSpeak(): Unit = {
super.speak() // 调用父类的方法
}
}

object TestApp extends App {
val dog = new Dog()
dog.speak() // 输出: Dog barks.
dog.parentSpeak() // 输出: Animal makes a sound.
}

Java 中的父类方法调用

在 Java 中,子类调用父类的方法通常使用 super 关键字,特别是在方法被重写的情况下。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 父类定义
class Animal {
void speak() {
System.out.println("Animal makes a sound.");
}
}

// 子类继承父类并重写方法
class Dog extends Animal {
@Override
void speak() {
System.out.println("Dog barks.");
}

void parentSpeak() {
super.speak(); // 调用父类的方法
}
}

public class TestApp {
public static void main(String[] args) {
Dog dog = new Dog();
dog.speak(); // 输出: Dog barks.
dog.parentSpeak(); // 输出: Animal makes a sound.
}
}

总结

  • 相似点

    • 两者都允许子类调用父类的方法。
    • 当方法被重写时,都需要使用 super 来调用父类的实现。
  • 不同点

    • 在 Scala 中,如果子类没有重写父类的方法,可以直接调用父类的方法,无需 super
    • 在 Java 中,无论是否重写,通常推荐使用 super 来明确调用父类的方法。

2. Scala 的 Lambda 表达式 vs C++ 的 Lambda 表达式

问题:Scala 支持了 Lambda 表达式,也就是匿名函数,这个用法和 C++ 有什么区别?

回答

Scala 和 C++ 都支持 Lambda 表达式(匿名函数),但在语法、功能和使用场景上有一些差异。

Scala 的 Lambda 表达式

Scala 的 Lambda 表达式语法简洁,功能强大,广泛应用于集合操作、高阶函数等场景。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个高阶函数
def applyFunction(f: Int => Int, value: Int): Int = {
f(value)
}

// 使用命名函数
def increment(x: Int): Int = x + 1
println(applyFunction(increment, 10)) // 输出: 11

// 使用匿名函数(Lambda 表达式)
println(applyFunction(x => x * 2, 7)) // 输出: 14

// 更简洁的语法
println(applyFunction(_ * 3, 5)) // 输出: 15

特点

  • 类型推断:Scala 能够自动推断 Lambda 表达式的参数类型。
  • 简洁语法:支持多种简写形式,如 _ * 3
  • 高阶函数支持:函数可以作为参数传递或返回。

C++ 的 Lambda 表达式

C++ 在 C++11 引入了 Lambda 表达式,功能强大但语法相对复杂,尤其是在捕获列表和返回类型方面。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <functional>

// 定义一个高阶函数
int applyFunction(std::function<int(int)> f, int value) {
return f(value);
}

int main() {
// 使用命名函数
auto increment = [](int x) -> int { return x + 1; };
std::cout << applyFunction(increment, 10) << std::endl; // 输出: 11

// 使用匿名函数
std::cout << applyFunction([](int x) -> int { return x * 2; }, 7) << std::endl; // 输出: 14

// 更复杂的捕获
int factor = 3;
std::cout << applyFunction([factor](int x) -> int { return x * factor; }, 5) << std::endl; // 输出: 15

return 0;
}

特点

  • 捕获列表:可以捕获外部变量,支持值捕获和引用捕获。
  • 显式返回类型:通常需要显式指定返回类型,尤其在复杂表达式中。
  • 语法复杂度:相比 Scala,C++ 的 Lambda 语法更为复杂,特别是在捕获和类型推断方面。

主要区别

  1. 语法简洁性

    • Scala 的 Lambda 表达式语法更简洁,支持多种简写形式,减少了代码量。
    • C++ 的 Lambda 语法相对复杂,尤其在处理捕获和返回类型时。
  2. 类型推断

    • Scala 的类型推断更强大,能自动推断大部分情况下的类型。
    • C++ 的类型推断在 Lambda 表达式中需要更多显式信息,尤其在复杂情况下。
  3. 捕获机制

    • C++ 支持多种捕获方式(值捕获、引用捕获、按需捕获等),适用于底层内存管理和性能优化。
    • Scala 的 Lambda 表达式不需要显式捕获列表,变量捕获通过闭包自动处理,更适合函数式编程。
  4. 应用场景

    • Scala 的 Lambda 表达式广泛用于函数式编程、高阶函数和集合操作。
    • C++ 的 Lambda 表达式主要用于回调函数、事件处理和算法中的自定义操作。

3. 高阶函数与匿名函数的联系

问题:高阶函数中的参数可以是一个匿名函数,比如:

1
2
3
4
5
6
7
8
9
def applyFunction(f: Int => Int, value: Int): Int = {
f(value)
}

val increment = (x: Int) => x + 1
println(applyFunction(increment, 10)) // 输出: 11

// 直接传递匿名函数
println(applyFunction(x => x * 2, 7)) // 输出: 14

这两者之间有什么联系?

回答

高阶函数(Higher-Order Functions)匿名函数(Lambda 表达式) 在 Scala 中紧密结合,高阶函数是以函数作为参数或返回值的函数,而匿名函数则是没有名称的函数表达式。两者结合使得 Scala 支持强大的函数式编程范式。

高阶函数的定义

高阶函数是指以下两种情况之一的函数:

  1. 接受一个或多个函数作为参数
  2. 返回一个函数作为结果

示例

1
2
3
4
5
6
7
8
9
// 接受函数作为参数
def applyFunction(f: Int => Int, value: Int): Int = {
f(value)
}

// 返回函数
def makeIncrementer(): Int => Int = {
x => x + 1
}

匿名函数的定义

匿名函数(Lambda 表达式)是没有名称的函数,可以直接在需要函数的地方定义和使用。

示例

1
2
3
4
5
// 定义一个匿名函数并赋值给变量
val increment = (x: Int) => x + 1

// 直接传递匿名函数作为参数
applyFunction(x => x * 2, 7)

联系与结合

高阶函数和匿名函数的结合允许你在调用高阶函数时,灵活地传递自定义的操作逻辑,而无需事先定义命名函数。

示例详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 高阶函数定义
def applyFunction(f: Int => Int, value: Int): Int = {
f(value)
}

// 使用命名函数
def increment(x: Int): Int = x + 1
println(applyFunction(increment, 10)) // 输出: 11

// 使用匿名函数
println(applyFunction(x => x * 2, 7)) // 输出: 14

// 使用更简洁的语法
println(applyFunction(_ * 3, 5)) // 输出: 15

优点

  1. 代码简洁:无需为简单的操作定义额外的命名函数。
  2. 灵活性:可以根据需要在调用时动态定义函数逻辑。
  3. 可读性:在某些情况下,使用匿名函数可以使代码更直观。

高阶函数的更多应用

高阶函数不仅可以接受匿名函数作为参数,还可以返回函数,支持更复杂的函数式编程模式。

示例

1
2
3
4
5
6
7
8
9
10
// 高阶函数,返回一个函数
def multiplier(factor: Int): Int => Int = {
x => x * factor
}

val timesTwo = multiplier(2)
println(timesTwo(5)) // 输出: 10

val timesThree = multiplier(3)
println(timesThree(5)) // 输出: 15

4. Scala 的辅助构造器(Auxiliary Constructors)

问题:辅助构造器是不是为了实例化默认参数的对象?例如:

1
2
3
4
5
6
7
8
9
class Point(val x: Int, val y: Int) {
def this(x: Int) = this(x, 0) // 辅助构造器,默认 y = 0
}

val p1 = new Point(5, 10)
println(s"Point1: (${p1.x}, ${p1.y})") // 输出: Point1: (5, 10)

val p2 = new Point(7)
println(s"Point2: (${p2.x}, ${p2.y})") // 输出: Point2: (7, 0)

回答

辅助构造器(Auxiliary Constructors) 在 Scala 中用于提供类的多种构造方式,类似于 Java 中的重载构造函数。它们不仅限于实例化带有默认参数的对象,但确实可以用于这种场景。

辅助构造器的作用

  1. 提供多种实例化方式

    • 允许类在不同的上下文中以不同的参数集进行实例化。
  2. 封装初始化逻辑

    • 可以在辅助构造器中添加额外的初始化步骤,确保对象的一致性。
  3. 实现默认参数

    • 可以通过辅助构造器提供默认值,实现类似于默认参数的效果。

示例详解

1
2
3
4
5
6
7
8
9
10
class Point(val x: Int, val y: Int) {
// 辅助构造器,默认 y = 0
def this(x: Int) = this(x, 0)
}

val p1 = new Point(5, 10)
println(s"Point1: (${p1.x}, ${p1.y})") // 输出: Point1: (5, 10)

val p2 = new Point(7)
println(s"Point2: (${p2.x}, ${p2.y})") // 输出: Point2: (7, 0)

在这个例子中:

  • 主构造器:接受两个参数 xy,用于初始化 Point 对象。
  • 辅助构造器:接受一个参数 x,并通过调用主构造器 this(x, 0) 来为 y 提供默认值 0

使用辅助构造器的场景

  1. 简化对象创建

    • 提供更简洁的实例化方式,尤其是当某些参数有常见的默认值时。
  2. 增强灵活性

    • 允许用户根据不同需求创建对象,而不必提供所有参数。

辅助构造器 vs 默认参数

Scala 还支持默认参数,可以在参数列表中为参数提供默认值,从而减少对辅助构造器的需求。

示例

1
2
3
4
5
6
7
class Point(val x: Int, val y: Int = 0)

val p1 = new Point(5, 10)
println(s"Point1: (${p1.x}, ${p1.y})") // 输出: Point1: (5, 10)

val p2 = new Point(7)
println(s"Point2: (${p2.x}, ${p2.y})") // 输出: Point2: (7, 0)

优点

  • 简洁性:无需定义辅助构造器,直接在参数列表中提供默认值。
  • 灵活性:用户可以选择传递或省略默认参数。

比较

  • 辅助构造器

    • 适用于更复杂的初始化逻辑。
    • 可以有多个辅助构造器,提供多种实例化方式。
  • 默认参数

    • 更适用于简单的默认值需求。
    • 语法更简洁,减少了代码量。

选择使用辅助构造器还是默认参数

  • 使用辅助构造器

    • 当对象初始化需要额外的逻辑步骤。
    • 当需要提供多种不同的构造方式,涉及不同数量或类型的参数。
  • 使用默认参数

    • 当仅需要为某些参数提供默认值,且初始化过程简单。
    • 推荐在大多数情况下优先使用默认参数,因为它们更简洁和易于维护。

5. Scala 的 object vs Java 中的静态类

问题:object 对象和 Java 中的静态类很像,可以在里面定义一些方法、属性,不用再继续实例化,对吗?

回答

object 在 Scala 中定义的是单例对象(Singleton Object),它与 Java 中的静态类static class)有一定的相似性,但也存在一些关键区别和更强大的功能。

Scala 中的 object

  • 单例模式object 定义的实体在整个程序运行期间只有一个实例,无需显式实例化。
  • 伴生对象object 可以与同名的 class 形成伴生对象,允许它们互相访问 private 成员。
  • 静态成员替代:Scala 没有 static 关键字,object 提供了存放静态方法和字段的替代方案。
  • 工厂方法和应用程序入口:常用于定义工厂方法(如 apply 方法)和作为程序的入口点(继承 App 特质)。

示例

1
2
3
4
5
6
7
object MathUtils {
def square(x: Int): Int = x * x
def cube(x: Int): Int = x * x * x
}

println(MathUtils.square(4)) // 输出: 16
println(MathUtils.cube(3)) // 输出: 27

Java 中的静态类

  • 静态嵌套类:Java 中的静态类通常指的是嵌套类使用 static 修饰的情况,允许在不依赖外部类实例的情况下创建嵌套类的实例。
  • 静态方法和字段:Java 使用 static 关键字来定义与类相关联而非实例相关联的方法和字段。
  • 单例模式实现:Java 没有内建的单例模式支持,通常通过私有构造函数和静态方法来实现。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MathUtils {
public static int square(int x) {
return x * x;
}

public static int cube(int x) {
return x * x * x;
}
}

// 使用静态方法
System.out.println(MathUtils.square(4)); // 输出: 16
System.out.println(MathUtils.cube(3)); // 输出: 27

主要区别

  1. 语法和概念

    • **Scala object**:定义一个完整的单例对象,可以包含方法、字段、甚至继承其他类或特质。与伴生类形成紧密的伴生关系。
    • Java 静态类:通常指静态嵌套类,用于在类内部定义独立的类。Java 没有直接的单例对象机制,静态方法和字段需要通过 static 关键字定义。
  2. 伴生对象

    • Scala 支持伴生对象,使得类和对象可以互相访问私有成员,增强了封装性和灵活性。
    • Java 不直接支持伴生对象,通常通过访问修饰符和嵌套类来实现类似功能。
  3. 单例实现

    • Scala 的 object 直接支持单例模式,无需额外代码。
    • Java 需要手动实现单例模式,通常通过私有构造函数和静态方法。
  4. 继承和多态

    • Scala 的 object 可以继承类和特质,实现多态和接口功能。
    • Java 的静态类(嵌套类)也可以继承,但由于静态类的性质,它们通常用于组织代码,而不是实现接口或多态。

总结

  • 相似点

    • 都可以用来定义与类相关联的静态方法和字段。
    • 都可以在不实例化对象的情况下调用其中的方法和访问字段。
  • 不同点

    • Scala 的 object 提供了内建的单例支持和伴生对象功能,语法更简洁且功能更强大。
    • Java 需要手动实现单例模式,并通过 static 关键字定义静态方法和字段,语法相对繁琐。

推荐使用方式

  • 在 Scala 中,使用 object 来定义工具类、工厂方法、应用程序入口点等,不需要手动管理单例实例。
  • 在 Java 中,使用静态方法和字段来实现工具类,必要时手动实现单例模式。

6. Scala 的 apply 方法及其自动调用

问题apply 方法好像是一个默认的方法,比如:

1
2
3
4
5
6
7
8
9
10
11
12
class Rectangle(val width: Double, val height: Double) {
def area(): Double = width * height
}

object Rectangle {
def apply(width: Double, height: Double): Rectangle = new Rectangle(width, height)

def unitSquare(): Rectangle = new Rectangle(1.0, 1.0)
}

val rect1 = Rectangle(5.0, 3.0) // 调用 apply 方法
println(rect1.area()) // 输出: 15.0

不显式调用它也会自动调用,对吗?

回答

是的,你的理解基本正确。在 Scala 中,apply 方法具有特殊的意义,允许你通过更简洁的语法调用它,而无需显式地使用方法名。这使得 object 内的 apply 方法可以像函数一样被调用,实现工厂模式和简化对象创建。

apply 方法的作用

  • 简化对象创建:通过定义 apply 方法,允许你像调用函数一样创建对象,无需使用 new 关键字。
  • 工厂方法:可以在 apply 方法中添加额外的逻辑,如参数验证、对象初始化等。
  • 与伴生对象结合:通常在伴生对象中定义 apply 方法,与类一起使用,提供便捷的实例化方式。

如何自动调用 apply 方法

当你在对象上使用函数调用语法(即在对象名后加括号并传递参数)时,Scala 会自动调用该对象的 apply 方法。

示例详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Rectangle(val width: Double, val height: Double) {
def area(): Double = width * height
}

object Rectangle {
def apply(width: Double, height: Double): Rectangle = new Rectangle(width, height)

def unitSquare(): Rectangle = new Rectangle(1.0, 1.0)
}

object TestApp extends App {
// 使用 apply 方法创建 Rectangle 实例,语法糖自动调用 apply
val rect1 = Rectangle(5.0, 3.0)
println(rect1.area()) // 输出: 15.0

// 也可以显式调用 apply 方法
val rect2 = Rectangle.apply(4.0, 2.5)
println(rect2.area()) // 输出: 10.0

// 使用伴生对象中的其他方法
val unitRect = Rectangle.unitSquare()
println(unitRect.area()) // 输出: 1.0
}

解释

  1. 定义类和伴生对象

    • Rectangle 类定义了矩形的 widthheight 属性,以及计算面积的方法 area
    • Rectangle 伴生对象定义了 apply 方法,用于创建 Rectangle 实例,以及 unitSquare 方法,用于创建一个单位正方形。
  2. 创建实例

    • Rectangle(5.0, 3.0):使用函数调用语法,Scala 自动调用 Rectangle.apply(5.0, 3.0),创建一个新的 Rectangle 实例。
    • Rectangle.apply(4.0, 2.5):显式调用 apply 方法,效果与上述相同。
    • Rectangle.unitSquare():调用伴生对象中的其他方法,创建一个单位正方形。

优势

  • 简洁性:减少了使用 new 关键字的需要,使代码更简洁易读。
  • 灵活性:可以在 apply 方法中添加自定义逻辑,如参数验证、对象缓存等。
  • 工厂模式支持:允许根据不同参数创建不同类型的实例,实现工厂模式。

自动调用的更多示例

示例 1:单参数 apply 方法

1
2
3
4
5
6
7
8
9
10
11
12
class Circle(val radius: Double) {
def area(): Double = math.Pi * radius * radius
}

object Circle {
def apply(radius: Double): Circle = new Circle(radius)
}

object TestApp extends App {
val circle = Circle(2.5) // 自动调用 Circle.apply(2.5)
println(circle.area()) // 输出: 19.634954084936208
}

示例 2:多重 apply 方法

你可以在伴生对象中定义多个 apply 方法,以支持不同的实例化方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person(val name: String, val age: Int) {
def greet(): Unit = {
println(s"Hello, my name is $name and I am $age years old.")
}
}

object Person {
// 默认年龄为 0
def apply(name: String): Person = new Person(name, 0)

// 指定姓名和年龄
def apply(name: String, age: Int): Person = new Person(name, age)
}

object TestApp extends App {
val person1 = Person("Alice") // 调用 apply(name: String)
person1.greet() // 输出: Hello, my name is Alice and I am 0 years old.

val person2 = Person("Bob", 25) // 调用 apply(name: String, age: Int)
person2.greet() // 输出: Hello, my name is Bob and I am 25 years old.
}

自动调用的工作机制

当你使用 ObjectName(args) 语法时,Scala 会隐式调用伴生对象中的 apply 方法。因此,不需要显式地调用 apply,而直接使用类似函数调用的语法。

内部机制

1
2
// Scala 编译器会将 Rectangle(5.0, 3.0) 转换为 Rectangle.apply(5.0, 3.0)
val rect1 = Rectangle(5.0, 3.0)

背后的逻辑

  1. 语法糖:Scala 的语法糖允许你通过 ObjectName(args) 语法自动调用 apply 方法。
  2. 编译器转换:编译器会将这种语法转换为显式的 apply 方法调用。
  3. 便利性:这种机制简化了对象创建过程,使代码更简洁和易读。

综合示例

为了更好地理解这些概念,我们来看一个综合的示例,涵盖了继承、Lambda 表达式、高阶函数、辅助构造器、objectapply 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 定义特质
trait Greeter {
def greet(name: String): Unit = {
println(s"Hello, $name!")
}
}

trait Logger {
def log(message: String): Unit = {
println(s"LOG: $message")
}
}

// 基类
class Animal {
def speak(): Unit = {
println("Animal makes a sound.")
}
}

// 子类,继承自 Animal,并混入 Greeter 和 Logger 特质
class Dog extends Animal with Greeter with Logger {
override def speak(): Unit = {
super.speak() // 调用父类 Animal 的 speak 方法
greet("Dog")
log("Dog has spoken.")
}

def sayHello(name: String): Unit = {
this.greet(name) // 使用 this 提高明确性
this.log(s"Said hello to $name.")
}
}

// 伴生对象,提供工厂方法
object Dog {
def apply(name: String): Dog = {
val dog = new Dog()
dog.sayHello(name)
dog
}
}

object TestApp extends App {
// 使用 apply 方法创建 Dog 实例
val myDog = Dog("Buddy")
myDog.speak()
// 输出:
// Hello, Buddy!
// LOG: Said hello to Buddy.
// Animal makes a sound.
// Hello, Dog!
// LOG: Dog has spoken.

// 使用高阶函数和匿名函数
def applyFunction(f: Int => Int, value: Int): Int = {
f(value)
}

val increment = (x: Int) => x + 1
println(applyFunction(increment, 10)) // 输出: 11

// 直接传递匿名函数
println(applyFunction(x => x * 2, 7)) // 输出: 14
}

解释

  1. 特质

    • Greeter:提供 greet 方法,用于打招呼。
    • Logger:提供 log 方法,用于记录日志。
  2. 类继承与混入特质

    • Animal:基类,定义了 speak 方法。
    • Dog:子类,继承自 Animal,并混入 GreeterLogger 特质。
      • 重写 speak 方法,调用父类方法并使用 greetlog 方法。
      • 定义 sayHello 方法,显式使用 this 调用 greetlog
  3. 伴生对象

    • Dog 对象定义了 apply 方法,用于创建 Dog 实例并调用 sayHello 方法。
  4. 应用程序入口

    • TestApp 对象继承自 App,程序启动时自动执行对象体内的代码。
    • 使用 Dog("Buddy") 语法自动调用 Dog.apply("Buddy") 方法,创建 Dog 实例并执行初始化逻辑。
    • 演示了高阶函数和匿名函数的使用。

运行结果

1
2
3
4
5
6
7
Hello, Buddy!
LOG: Said hello to Buddy.
Animal makes a sound.
Hello, Dog!
LOG: Dog has spoken.
11
14

总结

  • 子类调用父类方法

    • Scala 中可以使用 super 关键字调用父类的方法,类似于 Java 的 super.func1()
    • 在没有方法重写的情况下,可以直接调用父类的方法。
  • Lambda 表达式和匿名函数

    • Scala 的 Lambda 表达式语法更简洁,支持强大的类型推断和高阶函数。
    • C++ 的 Lambda 表达式功能强大,但语法更复杂,尤其在捕获和类型推断方面。
  • 高阶函数与匿名函数

    • 高阶函数可以接受匿名函数作为参数或返回函数,支持函数式编程的核心特性。
    • Scala 的高阶函数和匿名函数结合使用,使代码更灵活和简洁。
  • 辅助构造器

    • 用于提供类的多种实例化方式,不仅限于默认参数。
    • 与默认参数相比,辅助构造器适用于更复杂的初始化逻辑。
  • object vs Java 的静态类

    • Scala 的 object 定义单例对象,提供类似于静态方法和字段的功能,但功能更强大,支持伴生对象和工厂方法。
    • Java 需要手动实现单例模式,并使用 static 关键字定义静态成员,语法相对繁琐。
  • apply 方法

    • 允许通过简洁的语法调用对象的 apply 方法,自动实例化对象。
    • 当使用 ObjectName(args) 语法时,Scala 会自动调用 apply 方法,无需显式调用。

30 分钟速成 Scala
http://blog.luliang.online/2024/09/18/30分钟速成scala/
作者
Luyoung
发布于
2024年9月18日
许可协议