Scala 3 — 书籍

面向 Java 开发人员的 Scala

语言

此页面通过分享每种语言的并排示例,对 Java 和 Scala 编程语言进行了比较。它面向了解 Java 并希望了解 Scala 的程序员,特别是通过了解 Scala 特性与 Java 的比较来了解 Scala。

概述

在进入示例之前,本第一部分提供了相对简短的介绍和对后续部分的总结。它从高层次介绍了 Java 和 Scala 之间的相似之处和不同之处,然后介绍了你在编写代码时每天都会遇到的不同之处。

高层次相似之处

从高层次来看,Scala 与 Java 具有以下相似之处

  • Scala 代码编译为 .class 文件,打包在 JAR 文件中,并在 JVM 上运行
  • 它是一种 面向对象编程 (OOP) 语言
  • 它是静态类型的
  • 这两种语言都支持 lambda 和 高阶函数
  • 它们都可以与 IntelliJ IDEA 和 Microsoft VS Code 等 IDE 一起使用
  • 项目可以使用 Gradle、Ant 和 Maven 等构建工具进行构建
  • 它拥有出色的库和框架,用于构建服务器端、网络密集型应用程序,包括 Web 服务器应用程序、微服务、机器学习等(请参阅 “Awesome Scala” 列表
  • Java 和 Scala 都可以使用 Scala 库
    • 它们可以使用 Akka 演员库 构建基于演员的并发系统,并使用 Apache Spark 构建数据密集型应用程序
    • 它们可以使用 Play Framework 开发服务器端应用程序
  • 你可以使用 GraalVM 将你的项目编译成本机可执行文件
  • Scala 可以无缝使用为 Java 开发的大量库

高级差异

同样在高级层面,Java 和 Scala 之间的差异是

  • Scala 具有简洁但可读的语法;我们称之为表达性
  • 尽管它是静态类型的,但 Scala 通常感觉像一门动态语言
  • Scala 是一门纯 OOP 语言,因此每个对象都是一个类的实例,而类似于 ++= 的符号看起来像运算符,实际上是方法;这意味着你可以创建自己的运算符
  • 除了是一门纯 OOP 语言之外,Scala 还是一门纯 FP 语言;事实上,它鼓励融合 OOP 和 FP,使用函数进行逻辑处理,使用对象进行模块化
  • Scala 有一整套不可变集合,包括 ListVector 以及不可变 MapSet 实现
  • Scala 中的一切都是表达式:类似于 if 语句、for 循环、match 表达式,甚至 try/catch 表达式的构造都具有返回值
  • Scala 惯用法默认支持不可变性:你应该使用不可变 (final) 变量和不可变集合
  • 惯用 Scala 代码不使用 null,因此不会出现 NullPointerException
  • Scala 生态系统在 sbt、Mill 和其他系统中还有其他 构建工具
  • 除了在 JVM 上运行之外,Scala.js 项目还允许你将 Scala 用作 JavaScript 替代品
  • Scala Native 项目添加了低级构造,允许你编写“系统”级代码,还可以编译成本机可执行文件

编程级别差异

最后,这些是你每天在编写代码时会看到的一些差异

  • Scala 的语法非常一致
  • 变量和参数定义为 val(不可变,如 Java 中的 final)或 var(可变)
  • 类型推断让你的代码感觉是动态类型的,并有助于保持你的代码简洁
  • 除了简单的 for 循环,Scala 还有强大的 for 推导,可根据你的算法产生结果
  • 模式匹配和 match 表达式将改变你编写代码的方式
  • 默认编写不可变代码会导致编写表达式而不是语句;随着时间的推移,你会发现编写表达式简化了你的代码(和你的测试)
  • 顶级定义让你可以将方法、字段和其他定义放在任何地方,这也导致了简洁、富有表现力的代码
  • 你可以通过将多个特质“混合”到类和对象中来创建混合(特质类似于 Java 8 及更高版本中的接口)
  • 默认情况下,类是封闭的,支持 Joshua Bloch 的Effective Java 惯用法,“设计和记录继承或禁止它”
  • Scala 的 上下文抽象术语推断提供了一系列功能
    • 扩展方法让你可以向封闭的类添加新功能
    • 给定实例让你可以定义术语,编译器可以在使用点合成这些术语,从而使你的代码不那么冗长,并且本质上让编译器为你编写代码
    • 多重相等让你可以将相等比较限制在编译时,仅限于有意义的比较
  • Scala 拥有最先进的、第三方、开源函数式编程库
  • Scala case 类就像 Java 14 中的记录;它们帮助你在编写 FP 代码时对数据建模,并内置对模式匹配和克隆等概念的支持
  • 借助按名称参数、中缀表示法、可选括号、扩展方法和 高阶函数 等特性,你可以创建自己的“控制结构”和 DSL
  • Scala 文件不必根据它们包含的类或特质来命名
  • 其他许多优点:伴随类和对象、宏、联合交集、数字文字、多个参数列表、参数的默认值、命名参数等

通过示例比较特性

在介绍之后,以下部分将并排比较 Java 和 Scala 编程语言特性。

OOP 样式类和方法

此部分提供了与 OOP 样式类和方法相关的特性的比较。

注释

//
/* ... */
/** ... */
//
/* ... */
/** ... */

OOP 样式类,主构造函数

Scala 不遵循 JavaBeans 标准,因此,这里不显示以 JavaBeans 样式编写的 Java 代码,而是显示等效于其后的 Scala 代码的 Java 代码。

class Person {
  public String firstName;
  public String lastName;
  public int age;
  public Person(
    String firstName,
    String lastName,
    int age
  ) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
  public String toString() {
    return String.format("%s %s is %d years old.", firstName, lastName, age);
  }
}
class Person (
  var firstName: String,
  var lastName: String,
  var age: Int
):  
  override def toString = s"$firstName $lastName is $age years old."

辅助构造函数

public class Person {
  public String firstName;
  public String lastName;
  public int age;

  // 主构造函数
  public Person(
    String firstName,
    String lastName,
    int age
  ) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }

  // 零参数构造函数
  public Person() {
    this("", "", 0);
  }

  // 单参数构造函数
  public Person(String firstName) {
    this(firstName, "", 0);
  }

  // 双参数构造函数
  public Person(
    String firstName,
    String lastName
  ) {
    this(firstName, lastName, 0);
  }

}
class Person (
  var firstName: String,
  var lastName: String,
  var age: Int
):
    // 零参数辅助构造函数
    def this() = this("", "", 0)

    // 单参数辅助构造函数
    def this(firstName: String) =
      this(firstName, "", 0)

    // 双参数辅助构造函数
    def this(
      firstName: String,
      lastName: String
    ) =
      this(firstName, lastName, 0)

end Person

默认情况下,类是封闭的

“计划继承或禁止继承。”

final class Person
类 Person

一个开放扩展的类

类 Person
开放类 Person

单行方法

public int add(int a, int b) {
  return a + b;
}
def add(a: Int, b: Int): Int = a + b

多行方法

public void walkThenRun() {
  System.out.println("walk");
  System.out.println("run");
}
def walkThenRun() =
  println("walk")
  println("run")

不可变字段

final int i = 1;
val i = 1

可变字段

int i = 1;
var i = 1;
var i = 1

接口、特征和继承

本节比较了 Java 接口和 Scala 特征,包括类如何扩展接口和特征。

接口/特征

public interface Marker {};
特征 Marker

简单接口

public interface Adder {
  public int add(int a, int b);
}
特征 Adder
  def add(a: Int, b: Int): Int

具有具体方法的接口

public interface Adder {
  int add(int a, int b);
  default int multiply(
    int a, int b
  ) {
    return a * b;
  }
}
特征 Adder
  def add(a: Int, b: Int): Int
  def multiply(a: Int, b: Int): Int =
    a * b

继承

类 Dog 扩展 Animal 实现 HasLegs、HasTail
类 Dog 扩展 Animal、HasLegs、HasTail

扩展多个接口

这些接口和特征具有具体实现的方法(默认方法)

接口 Adder {
  default int add(int a, int b) {
    return a + b;
  }
}

接口 Multiplier {
  default int multiply (
    int a,
    int b)
  {
    return a * b;
  }
}

public 类 JavaMath
实现 Adder、Multiplier {}

JavaMath jm = new JavaMath();
jm.add(1,1);
jm.multiply(2,2);
特征 Adder
  def add(a: Int, b: Int) = a + b

特征 Multiplier
  def multiply(a: Int, b: Int) = a * b

类 ScalaMath 扩展 Adder、Multiplier

val sm = new ScalaMath
sm.add(1,1)
sm.multiply(2,2)

混合

不适用
class DavidBanner

trait Angry
  def beAngry() =
    println("You won’t like me ...")

trait Big
  println("I’m big")

trait Green
  println("I’m green")

// 混合这些特征,因为 DavidBanner
// 已创建
val hulk = new DavidBanner with Big with Angry with Green

控制结构

本节比较了 Java 和 Scala 中的 控制结构

if 语句,单行

if (x == 1) { System.out.println(1); }
if x == 1 then println(x)

if 语句,多行

if (x == 1) {
  System.out.println("x is 1, as you can see:")
  System.out.println(x)
}
if x == 1 then
  println("x is 1, as you can see:")
  println(x)

if、else if、else

if (x < 0) {
  System.out.println("negative")
} else if (x == 0) {
  System.out.println("zero")
} else {
  System.out.println("positive")
}
if x < 0 then
  println("negative")
else if x == 0
  println("zero")
else
  println("positive")

if 作为方法体

public int min(int a, int b) {
  return (a < b) ? a : b;
}
def min(a: Int, b: Int): Int =
  if a < b then a else b

if 返回值

在 Java 中称为三元运算符

int minVal = (a < b) ? a : b;
val minValue = if a < b then a else b

while 循环

while (i < 3) {
  System.out.println(i);
  i++;
}
while i < 3 do
  println(i)
  i += 1

for 循环,单行

for (int i: ints) {
  System.out.println(i);
}
//首选
for i <- ints do println(i)

// 也可用
for (i <- ints) println(i)

for 循环,多行

for (int i: ints) {
  int x = i * 2;
  System.out.println(x);
}
for
  i <- ints
do
  val x = i * 2
  println(s"i = $i, x = $x")

for 循环,多个生成器

for (int i: ints1) {
  for (int j: chars) {
    for (int k: ints2) {
      System.out.printf("i = %d, j = %d, k = %d\n", i,j,k);
    }
  }
}
for
  i <- 1 到 2
  j <- 'a' 到 'b'
  k <- 1 到 10 步长 5
do
  println(s"i = $i, j = $j, k = $k")

带守卫 (if) 表达式的生成器

List ints =
  ArrayList(1,2,3,4,5,6,7,8,9,10);

for (int i: ints) {
  if (i % 2 == 0 && i < 5) {
    System.out.println(x);
  }
}
for
  i <- 1 到 10
  if i % 2 == 0
  if i < 5
do
  println(i)

for 推导

不适用
val list =
  for
    i <- 1 到 3
  yield
    i * 10
// list: Vector(10, 20, 30)

switch/match

String monthAsString = "";
switch(day) {
  case 1: monthAsString = "January";
          break;
  case 2: monthAsString = "February";
          break;
  default: monthAsString = "Other";
          break;
}
val monthAsString = day match
  case 1 => "January"
  case 2 => "February"
  case _ => "Other"

switch/match,每个 case 多个条件

String numAsString = "";
switch (i) {
  case 1: case 3
  case 5: case 7: case 9
    numAsString = "odd";
    break;
  case 2: case 4
  case 6: case 8: case 10
    numAsString = "even";
    break;
  default
    numAsString = "too big";
    break;
}
val numAsString = i match
  case 1 | 3 | 5 | 7 | 9 => "odd"
  case 2 | 4 | 6 | 8 | 10 => "even"
  case _ => "too big"

try/catch/finally

try {
  writeTextToFile(text);
} catch (IOException ioe) {
  println(ioe.getMessage())
} catch (NumberFormatException nfe) {
  println(nfe.getMessage())
} finally {
  println("Clean up resources here.")
}
try
  writeTextToFile(text)
catch
  case ioe: IOException =>
    println(ioe.getMessage)
  case nfe: NumberFormatException =>
    println(nfe.getMessage)
finally
  println("Clean up resources here.")

集合类

本节比较了 Java 和 Scala 中可用的集合类

不可变集合类

有关如何创建不可变集合实例的示例。

序列

List strings = List.of("a", "b", "c");
val strings = List("a", "b", "c")
val strings = Vector("a", "b", "c")

集合

Set set = Set.of("a", "b", "c");
val set = Set("a", "b", "c")

映射

Map map = Map.of(
  "a", 1,
  "b", 2,
  "c", 3
);
val map = Map(
  "a" -> 1,
  "b" -> 2,
  "c" -> 3
)

可变集合类

Scala 具有可变集合类,例如其 scala.collection.mutable 包中的 ArrayBufferMapSet。将它们导入当前作用域后,它们将像刚才所示的不可变 ListVectorMapSet 示例一样创建。

Scala 还具有一个 Array 类,你可以将其视为对 Java array 原始类型的包装。Scala Array[A] 映射到 Java A[],因此你可以将此 Scala Array[String]

val a = Array("a", "b")

视为由这个 Java String[]

String[] a = {"a", "b"};

支持。但是,Scala Array 还具有 Scala 集合中预期的所有函数方法,包括 mapfilter

val nums = Array(1, 2, 3, 4, 5)
val doubledNums = nums.map(_ * 2)
val filteredNums = nums.filter(_ > 2)

由于 Scala Array 的表示方式与 Java array 相同,因此你可以在 Scala 代码中轻松使用返回数组的 Java 方法。

尽管讨论了 Array,但请记住,在 Scala 中通常有更适合 Array 的替代方案。数组对于与其他语言(Java、JavaScript)进行交互很有用,并且在编写需要从底层平台中挤出最大性能的低级代码时也可能很有用。但通常,当你需要使用序列时,Scala 的惯用法是优先使用不可变序列,如 VectorList,然后在真正需要可变序列时使用 ArrayBuffer

你还可以使用 Scala CollectionConverters 对象在 Java 和 Scala 集合类之间进行转换。在不同的包中有两个对象,一个用于从 Java 转换为 Scala,另一个用于从 Scala 转换为 Java。此表显示了可能的转换

Java Scala
java.util.Collection scala.collection.Iterable
java.util.List scala.collection.mutable.Buffer
java.util.Set scala.collection.mutable.Set
java.util.Map scala.collection.mutable.Map
java.util.concurrent.ConcurrentMap scala.collection.mutable.ConcurrentMap
java.util.Dictionary scala.collection.mutable.Map

集合类上的方法

由于能够将 Java 集合视为流,因此 Java 和 Scala 现在拥有许多相同的常见函数方法

  • map
  • filter
  • forEach/foreach
  • findFirst/find
  • reduce

如果你习惯在 Java 中使用 lambda 表达式来使用这些方法,那么你就会发现很容易在 Scala 的 集合类 上使用相同的方法。

Scala 还有许多其他 集合方法,包括 headtaildroptakedistinctflatten 等等。起初你可能会疑惑为什么有这么多方法,但在使用 Scala 后你会意识到,由于 这些方法,你几乎不需要再编写自定义 for 循环了。

(这也意味着你也很少需要阅读自定义 for 循环。由于开发人员往往将十分之一的时间花在阅读代码上,而不是编写代码上,因此这一点很重要。)

元组

Java 元组的创建方式如下

Pair<String, Integer> pair =
  new Pair<String, Integer>("Eleven", 11);

Triplet<String, Integer, Double> triplet =
  Triplet.with("Eleven", 11, 11.0);
Quartet<String, Integer, Double,Person> triplet =
  Quartet.with("Eleven", 11, 11.0, new Person("Eleven"));

其他 Java 元组名称包括 Quintet、Sextet、Septet、Octet、Ennead、Decade。

Scala 中任何大小的元组都可以通过将值放在括号内创建,如下所示

val a = ("eleven")
val b = ("eleven", 11)
val c = ("eleven", 11, 11.0)
val d = ("eleven", 11, 11.0, Person("Eleven"))

枚举

本节比较了 Java 和 Scala 中的枚举。

基本枚举

enum Color {
  RED, GREEN, BLUE
}
enum Color
  case Red, Green, Blue

参数化枚举

enum Color {
  Red(0xFF0000),
  Green(0x00FF00),
  Blue(0x0000FF);

  private int rgb;

  Color(int rgb) {
    this.rgb = rgb;
  }
}
enum Color(val rgb: Int)
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)

用户定义的枚举成员

enum Planet {
  MERCURY (3.303e+23, 2.4397e6),
  VENUS   (4.869e+24, 6.0518e6),
  EARTH   (5.976e+24, 6.37814e6);
  // more planets ...

  private final double mass;
  private final double radius;

  Planet(double mass, double radius) {
    this.mass = mass;
    this.radius = radius;
  }

  public static final double G =
    6.67300E-11;

  private double mass() {
    return mass;
  }

  private double radius() {
    return radius;
  }

  double surfaceGravity() {
    return G * mass /
      (radius * radius);
  }

  double surfaceWeight(
    double otherMass
  ) {
    return otherMass *
      surfaceGravity();
  }

}
enum Planet(
  mass: Double,
  radius: Double
):
  case Mercury extends
    Planet(3.303e+23, 2.4397e6)
  case Venus extends
    Planet(4.869e+24, 6.0518e6)
  case Earth extends
    Planet(5.976e+24, 6.37814e6)
    // more planets ...

  private final val G = 6.67300E-11

  def surfaceGravity =
    G * mass / (radius * radius)

  def surfaceWeight(otherMass: Double)
    = otherMass * surfaceGravity

异常和错误处理

本节介绍了 Java 和 Scala 中异常处理之间的差异。

Java 使用检查异常

Java 使用检查异常,因此在 Java 代码中,您历来编写 try/catch/finally 块,以及方法上的 throws 子句

public int makeInt(String s)
throws NumberFormatException {
  // code here to convert a String to an int
}

Scala 不使用检查异常

Scala 惯用语是使用这种检查异常。在使用可能引发异常的代码时,可以使用 try/catch/finally 块来捕获引发异常的代码中的异常,但从那里继续执行的方式不同。

解释这一点的最佳方法是,Scala 代码由返回值的表达式组成。因此,最终会将代码编写为一系列代数表达式

val a = f(x)
val b = g(a,z)
val c = h(b,y)

这很好,这只是代数。创建方程式来解决小问题,然后组合方程式来解决大问题。

而且非常重要的是——正如你从代数课程中所记得的——代数表达式不会短路——它们不会引发会炸毁一系列方程式的异常。

因此,在 Scala 中,我们的方法不会引发异常。相反,它们返回 Option 等类型。例如,此 makeInt 方法捕获可能的异常并返回 Option

def makeInt(s: String): Option[Int] =
  try
    Some(s.toInt)
  catch
    case e: NumberFormatException => None

Scala Option 类似于 Java Optional 类。如所示,如果字符串到整数的转换成功,则 Int 会在 Some 值中返回,如果失败,则会返回 None 值。 SomeNoneOption 的子类型,因此该方法声明返回 Option[Int] 类型。

当你有 Option 值(例如 makeInt 返回的值)时,有许多方法可以处理它,具体取决于你的需求。此代码显示了一种可能的方法

makeInt(aString) match
  case Some(i) => println(s"Int i = $i")
  case None => println(s"Could not convert $aString to an Int.")

Option 通常在 Scala 中使用,并且内置于标准库中的许多类中。其他类似的类集,如 Try/Success/Failure 和 Either/Left/Right,提供了更大的灵活性。

有关处理 Scala 中的错误和异常的更多信息,请参阅 函数式错误处理 部分。

Scala 独有的概念

这总结了 Java 和 Scala 语言的比较。

Scala 中还有其他概念,目前在 Java 11 中没有同等概念。这包括

  • 与 Scala 的 上下文抽象 相关的所有内容
  • 一些 Scala 方法特性
    • 多个参数列表
    • 默认参数值
    • 调用方法时使用命名参数
  • 案例类(类似于 Java 14 中的“记录”)、案例对象以及伴随类和对象(请参阅 领域建模 章节)
  • 创建自己的控制结构和 DSL 的能力
  • 顶级定义
  • 模式匹配
  • match 表达式的高级特性
  • 类型 lambda
  • 特征参数
  • 不透明类型别名
  • 多重相等
  • 类型类
  • 中缀方法
  • 宏和元编程

此页面的贡献者