Scala 3 — 書籍

Scala 給 Java 開發人員

語言

此頁面透過分享每種語言的並列範例,提供 Java 和 Scala 程式語言之間的比較。它適用於了解 Java 並想學習 Scala 的程式設計師,特別是透過了解 Scala 功能與 Java 的比較。

概觀

在進入範例之前,此第一部分提供後續部分的相對簡短介紹和摘要。它以高階方式呈現 Java 和 Scala 之間的相似性和差異,然後介紹您在撰寫程式碼時每天會遇到的差異。

高階相似性

在高階層面上,Scala 與 Java 共享這些相似性

  • Scala 程式碼編譯成 *class* 檔案,封裝在 JAR 檔案中,並在 JVM 上執行
  • 它是一種 物件導向程式設計 (OOP) 語言
  • 它是靜態型別
  • 兩種語言都支援 lambda 和 高階函式
  • 它們都可以與 IntelliJ IDEA 和 Microsoft VS Code 等 IDE 搭配使用
  • 專案可以使用 Gradle、Ant 和 Maven 等建置工具建置
  • 它擁有絕佳的函式庫和架構,用於建置伺服器端、網路密集型應用程式,包括網路伺服器應用程式、微服務、機器學習等等(請參閱 “Awesome Scala” 清單
  • Java 和 Scala 都可以使用 Scala 函式庫
    • 它們可以使用 Akka actors 函式庫 來建置基於 actor 的並行系統,並使用 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 的 脈絡抽象詞彙推論 提供了一系列功能
    • 擴充方法 讓你可以在封閉的類別中新增新的功能
    • Given 實例 讓你定義編譯器可以在using 點進行綜合的詞彙,這讓你的程式碼更簡潔,而且基本上讓編譯器為你撰寫程式碼
    • 多重相等性 讓你限制相等性比較(在編譯時)僅限於有意義的比較
  • Scala 有最先進的、第三方、開源函數式程式庫
  • Scala case 類別就像 Java 14 中的記錄;它們可協助您在撰寫 FP 程式碼時建模資料,並內建支援模式比對和複製等概念
  • 拜依名稱參數、中綴表示法、可選括號、擴充方法和高階函數等功能所賜,您可以建立自己的「控制結構」和 DSL
  • Scala 檔案不必根據它們包含的類別或特質來命名
  • 許多其他好處:伴隨類別和物件、巨集、聯集交集、數字文字、多個參數清單、參數的預設值、命名參數等

功能與範例比較

有了這個引言,以下各節將提供 Java 和 Scala 程式語言功能的並排比較。

OOP 風格的類別和方法

本節提供與 OOP 風格類別和方法相關的功能比較。

註解

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

OOP 風格類別、主要建構函式

Scala 沒有遵循 JavaBeans 標準,因此我們在此顯示等同於後續 Scala 程式碼的 Java 程式碼,而不是顯示以 JavaBeans 風格撰寫的 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
class Person

開放擴充的類別

class Person
open class 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("執行");
}
def walkThenRun() =
  println("走")
  println("跑")

不可變欄位

final int i = 1;
val i = 1

可變欄位

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

介面、特質和繼承

本節比較 Java 介面和 Scala 特質,包括類別如何擴充介面和特質。

介面/特質

public interface Marker {};
trait Marker

簡單介面

public interface Adder {
  public int add(int a, int b);
}
trait 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;
  }
}
trait Adder
  def add(a: Int, b: Int): Int
  def multiply(a: Int, b: Int): Int =
    a * b

繼承

class Dog extends Animal implements HasLegs, HasTail
class Dog extends Animal, HasLegs, HasTail

擴充多個介面

這些介面和特質具有具體的、已實作的方法(預設方法)

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

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

public class JavaMath
implements Adder, Multiplier {}

JavaMath jm = new JavaMath();
jm.add(1,1);
jm.multiply(2,2);
trait 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)

Mixins

不適用
類別 DavidBanner

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

特質 Big
  println("I’m big")

特質 Green
  println("I’m green")

// 在建立 DavidBanner 時混合特質
// is created
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 to 2
  j <- 'a' to 'b'
  k <- 1 to 10 by 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 to 10
  if i % 2 == 0
  if i < 5
do
  println(i)

for 理解

不適用
val list =
  for
    i <- 1 至 3
  產生
    i * 10
// 清單:向量 (10, 20, 30)

切換/比對

字串 monthAsString = "";
switch(day) {
  情況 1:monthAsString = "一月";
          中斷;
  情況 2:monthAsString = "二月";
          中斷;
  預設:monthAsString = "其他";
          中斷;
}
val monthAsString = day 比對
  情況 1 => "一月"
  情況 2 => "二月"
  情況 _ => "其他"

切換/比對,每個情況有多個條件

字串 numAsString = "";
switch (i) {
  情況 1:情況 3
  情況 5:情況 7:情況 9
    numAsString = "奇數";
    中斷;
  情況 2:情況 4
  情況 6:情況 8:情況 10
    numAsString = "偶數";
    中斷;
  預設
    numAsString = "太大";
    中斷;
}
val numAsString = i 比對
  情況 1 | 3 | 5 | 7 | 9 => "奇數"
  情況 2 | 4 | 6 | 8 | 10 => "偶數"
  情況 _ => "太大"

try/catch/finally

try {
  writeTextToFile(text);
} catch (IOException ioe) {
  println(ioe.getMessage())
} catch (NumberFormatException nfe) {
  println(nfe.getMessage())
} finally {
  println("在此清理資源。")
}
try
  writeTextToFile(text)
catch
  情況 ioe:IOException =>
    println(ioe.getMessage)
  情況 nfe:NumberFormatException =>
    println(nfe.getMessage)
finally
  println("在此清理資源。")

集合類別

此區段比較 Java 和 Scala 中提供的 集合類別

不可變集合類別

建立不可變集合實例的範例。

序列

清單字串 = 清單.of("a", "b", "c");
val 字串 = 清單("a", "b", "c")
val 字串 = 向量("a", "b", "c")

集合

設定集合 = 設定.of("a", "b", "c");
val 集合 = 設定("a", "b", "c")

地圖

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

可變集合類別

Scala 具有可變集合類別,例如 ArrayBufferMapSet,位於其 scala.collection.mutable 套件中。在將它們 匯入 當前範圍後,它們的建立方式就像剛剛顯示的不可變 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),
  金星   (4.869e+24, 6.0518e6),
  地球   (5.976e+24, 6.37814e6);
  // 更多行星 ...

  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)
    // 更多行星 ...

  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
  • 特質參數
  • 不透明類型別名
  • 多重宇宙等號
  • 類型類別
  • 中綴方法
  • 巨集和元程式設計

此頁面的貢獻者