Stories

Detail Return Return

Scala語言入門:初學者的基礎語法指南 - Stories Detail

本文已收錄至GitHub,推薦閲讀 👉 Java隨想錄

微信公眾號:Java隨想錄

原創不易,注重版權。轉載請註明原作者和原文鏈接

在計算機編程的世界裏,Scala是一個不可或缺的語言。

作為一種在Java虛擬機(JVM)上運行的靜態類型編程語言,Scala結合了面向對象和函數式編程的特性,使它既有強大的表達力又具備優秀的型態控制。

對於初學者來説,理解Scala的基本語法是掌握這門語言的關鍵步驟。本文將帶領大家逐步瞭解Scala的基礎知識,無論你是編程新手還是想要擴展技能集的專業開發者,都可以在這篇文章中找到有用的信息。

先分享Scala的官方網站:https://docs.scala-lang.org/。

大部分的學習資料都可以在這找到,語言支持切換中文,非常友好。

另外我們可以使用Scastie網站,在瀏覽器上直接運行Scala代碼進行調試:https://scastie.scala-lang.org/。

Scala & Java

Scala語言和Java語言有許多相似之處,但也有一些明顯的區別。

Scala語言來源於Java,它以Java虛擬機(JVM)為運行環境,Scala源碼 (.scala)會編譯成.class文件。這意味着Scala程序可以與Java程序互操作,並且可以利用JVM的優化和性能。

在語法上,Scala和Java有一些區別。

例如,在Scala中,一切皆為對象,而在Java中,基本類型、null、靜態方法等不是對象。在Scala中,成員變量/屬性必須顯示初始化,而在Java中可以不初始化。此外,在Scala中,異常處理採用Try-catch {case-case}-finally的方式,而在Java中採用Try-catch-catch-finally的方式。

Scala還有一些特有的概念,例如:惰性函數、伴生對象、特質、偏函數等。這些概念都為Scala語言提供了更多的靈活性和表達能力。使得Scala語言非常適合用來開發大數據處理框架。此外,Scala語言的語法糖也非常甜,可以用更少的代碼量來實現相同的功能。

Scala安裝

先從安裝Scala説起,Scala的安裝也很簡單。

  1. 首先Idea安裝 Scala插件。

  1. 項目結構裏點擊全局庫,添加 Scala SDK進行下載。

  1. 右鍵點擊添加到你要使用Scala的項目的項目庫,項目的庫裏就會多出Scala的SDK。

到這就結束了,然後我們就可以在項目裏使用Scala了。

新建一個Scala項目,運行Hello Wrold試一下。

數據類型

Scala中的數據類型可以分為兩大類:值類型(AnyVal)和引用類型(AnyRef)。這兩種類型都是 Any 類型的子類。

值類型包括9種基本數據類型,分別是 ByteShortIntLongFloatDoubleCharBooleanUnit。其中,前8種類型與Java中的基本數據類型相對應,而 Unit 類型表示無值,類似於Java中的 void

引用類型包括所有非值類型的數據類型,例如字符串、數組、列表等。它們都是 AnyRef 類型的子類。

在Scala的數據類型層級結構的底部,還有兩個特殊的數據類型: NothingNull。其中, Nothing 類型是所有類型的子類型,它沒有任何實例。而 Null 類型是所有引用類型的子類型,它只有一個實例: null

語法

主方法是一個程序的入口點。JVM要求一個名為main的主方法,接受一個字符串數組的參數。你可以如下所示來定義一個主方法。

object Main {
  def main(args: Array[String]): Unit =
    println("Hello, Scala developer!")
}

在Scala 2中,也可以通過創建一個擴展App類的對象來定義主程序。例如:

object Main extends App {
  println("Hello, Scala developer!")
}

需要注意的是,這種方法在Scala 3中不再推薦使用。它們被新的@main方法取代了,這是在Scala 3中生成可以從命令行調用的程序的推薦方法。App目前仍以有限的形式存在,但它不支持命令行參數,將來會被棄用。

val & var

在 Scala 中,valvar 都可以用來定義變量,但它們之間有一些重要的區別。

val 用於定義不可變變量,也就是説,一旦定義了一個 val 變量,它的值就不能再被改變。例如:

val x = 1
// x = 2 // 這會報錯,因為不能給 val 變量重新賦值

var 用於定義可變變量,它的值可以在定義後被改變。例如:

var y = 1
y = 2 // 這是合法的,因為 y 是一個 var 變量

val和var的類型可以被推斷,或者你也可以顯式地聲明類型,例如:

val x: Int = 1 + 1
var x: Int = 1 + 1

在實際編程中,我們應該儘量使用 val 來定義不可變變量,這樣可以提高代碼的可讀性和可維護性。只有在確實需要改變變量值的情況下,才應該使用 var 來定義可變變量。

泛型

在Scala 中,使用方括號 [] 來定義泛型類型。而在Java中是使用<>

例如,下面這段代碼:

object Main extends App {
  trait Animal {
    def speak: String
  }

  class Dog extends Animal {
    def speak = "Woof!"
  }

  class Cat extends Animal {
    def speak = "Meow!"
  }

  class Parrot extends Animal {
    def speak = "Squawk!"
  }

  class AnimalShelter[A <: Animal] {
    private var animals: List[A] = Nil

    def addAnimal(animal: A): Unit = {
      animals = animal :: animals
    }

    def getAnimal: A = {
      val animal = animals.head
      animals = animals.tail
      animal
    }
  }

  val dogShelter = new AnimalShelter[Dog]
  dogShelter.addAnimal(new Dog)
  val dog: Dog = dogShelter.getAnimal
  println(dog.speak)

  val catShelter = new AnimalShelter[Cat]
  catShelter.addAnimal(new Cat)
  val cat: Cat = catShelter.getAnimal
  println(cat.speak)

  val parrotShelter = new AnimalShelter[Parrot]
  parrotShelter.addAnimal(new Parrot)
  val parrot: Parrot = parrotShelter.getAnimal
  println(parrot.speak)
}

輸出:
Woof!
Meow!
Squawk!

這個示例中,我們定義了一個 Animal 特質和三個實現了該特質的類:DogCatParrot。然後我們定義了一個 AnimalShelter 類,它使用了泛型類型參數 A,並且限制了 A 必須是 Animal 的子類型。這樣我們就可以創建不同類型的動物收容所,比如 dogSheltercatShelterparrotShelter,並且在添加和獲取動物時保證類型安全。

包導入

import 語句用於導入其他包中的成員(類,特質,函數等)。 使用相同包的成員不需要 import 語句。 導入語句可以有選擇性:

import users._  // 導入包 users 中的所有成員
import users.User  // 導入類 User
import users.{User, UserPreferences}  // 僅導入選擇的成員
import users.{UserPreferences => UPrefs}  // 導入類並且設置別名

Scala 不同於 Java 的一點是 Scala 可以在任何地方使用導入:

def sqrtplus1(x: Int) = {
  import scala.math.sqrt
  sqrt(x) + 1.0
}

如果存在命名衝突並且你需要從項目的根目錄導入,請在包名稱前加上 _root_

package accounts

import _root_.users._

注意:包 scalajava.lang 以及 object Predef 是默認導入的。

包對象

在 Scala 中,包對象(Package Object)是一種特殊的對象,它與包同名,並且可以在包中定義一些公共的成員和方法,供包中的其他類和對象直接使用。包對象可以解決在包級別共享常量類型別名隱式轉換等問題。

在 Scala 中,可以使用 package 關鍵字定義一個包對象。包對象的文件名必須為 package.scala,並與包名一致。

下面是關於包對象的解釋和示例代碼:

// File: com/example/myapp/package.scala

package com.example

package object myapp {
  val appName: String = "MyApp"

  def printAppName(): Unit = {
    println(appName)
  }
}

在上述示例中,定義了一個包對象 myapp,位於包 com.example 下。在包對象中,我們定義了一個名為 appName 的常量和一個名為 printAppName 的方法。

這樣,我們就可以在包中的其他類和對象中直接使用 appNameprintAppName,而無需導入或限定符。

下面是一個使用包對象的示例代碼:

package com.example.myapp

object Main {
  def main(args: Array[String]): Unit = {
    println(myapp.appName)  // 直接訪問包對象中的常量
    myapp.printAppName()    // 直接調用包對象中的方法
  }
}

在上述示例中,我們在 Main 對象中直接訪問了包對象 myapp 中的常量 appName 和方法 printAppName。由於包對象與包同名且位於同一包中,因此可以直接使用它們。

特質

在Scala中,類是單繼承的,但是特質(trait)可以多繼承。

這意味着,一個類只能繼承一個父類,但可以繼承多個特質。這樣,從結果上看,就實現了多重繼承。

下面是一個例子:

trait A {
  def printA() = println("A")
}

trait B {
  def printB() = println("B")
}

class C extends A with B

object Main extends App {
  val c = new C
  c.printA()
  c.printB()
}

輸出:
A
B

例子中,定義了兩個特質 AB,它們分別有一個方法 printAprintB。然後我們定義了一個類 C,它繼承了特質 AB。這樣,類 C 就可以使用特質 AB 中定義的方法了。

特質也可以有默認的實現:

trait Greeter {
  def greet(name: String): Unit =
    println("Hello, " + name + "!")
}

你可以使用extends關鍵字來繼承特質,使用override關鍵字來覆蓋默認的實現。

class DefaultGreeter extends Greeter

class CustomizableGreeter(prefix: String, postfix: String) extends Greeter {
  override def greet(name: String): Unit = {
    println(prefix + name + postfix)
  }
}

val greeter = new DefaultGreeter()
greeter.greet("Scala developer") // Hello, Scala developer!

val customGreeter = new CustomizableGreeter("How are you, ", "?")
customGreeter.greet("Scala developer") // How are you, Scala developer?

凡是需要特質的地方,都可以由該特質的子類型來替換。

import scala.collection.mutable.ArrayBuffer

trait Pet {
  val name: String
}

class Cat(val name: String) extends Pet
class Dog(val name: String) extends Pet

val dog = new Dog("Harry")
val cat = new Cat("Sally")

val animals = ArrayBuffer.empty[Pet]
animals.append(dog)
animals.append(cat)
animals.foreach(pet => println(pet.name))  // Prints Harry Sally

在這裏 trait Pet 有一個抽象字段 namename 由Cat和Dog的構造函數中實現。最後一行,我們能調用pet.name的前提是它必須在特質Pet的子類型中得到了實現。

運算符

在 Scala 中,運算符是用於執行特定操作的符號或標記。Scala 具有豐富的運算符,並且允許用户自定義運算符,以及在自定義類中使用運算符。下面是關於定義和使用運算符的解釋和示例代碼:

在 Scala 中,可以使用 def 關鍵字定義自定義運算符。自定義運算符可以是任何由字母、數字或下劃線組成的標識符,以及一些特殊字符,例如 +-* 等。要定義一個運算符,可以在方法名前面加上一個操作符,然後在方法體中實現相應的邏輯。

下面是一個示例代碼:

class Vector2D(val x: Double, val y: Double) {
  def +(other: Vector2D): Vector2D = {
    new Vector2D(x + other.x, y + other.y)
  }
}

val v1 = new Vector2D(1.0, 2.0)
val v2 = new Vector2D(3.0, 4.0)
val sum = v1 + v2

println(sum.x) // 輸出:4.0
println(sum.y) // 輸出:6.0

在上述示例中,定義了一個 Vector2D 類,表示二維向量。我們通過 val 關鍵字定義了 xy 作為向量的座標。

然後,我們定義了一個自定義運算符 +,它接受另一個 Vector2D 對象作為參數,並返回一個新的 Vector2D 對象。在方法體內,我們實現了向量的加法操作。

在主程序中,我們創建了兩個 Vector2D 對象 v1v2。然後,我們使用自定義的運算符 + 來執行向量的加法,並將結果賦值給 sum

最後,我們打印出 sumxy 座標,驗證加法操作的結果。

我們可以像使用內置運算符一樣使用自定義運算符。它們可以用於相應類型的實例上,並按照定義的邏輯執行操作。

下面是一個示例代碼:

val num1 = 10
val num2 = 5

val sum = num1 + num2
val difference = num1 - num2
val product = num1 * num2

println(sum)        // 輸出:15
println(difference)  // 輸出:5
println(product)     // 輸出:50

在上述示例中,我們定義了兩個整數變量 num1num2。然後,我們使用內置的運算符 +-* 來執行加法、減法和乘法操作,並將結果分別賦值給 sumdifferenceproduct

傳名參數

傳名參數(Call-by-Name Parameters)是一種特殊的參數傳遞方式,它允許我們將表達式作為參數傳遞給函數,並在需要時進行求值。傳名參數使用 => 符號來定義,以表示傳遞的是一個表達式而不是具體的值。

傳名參數的特點是,在每次使用參數時都會重新求值表達式,而不是在調用函數時進行求值。這樣可以延遲表達式的求值,只在需要時才進行計算。傳名參數通常用於需要延遲計算、惰性求值或者需要按需執行的場景。

下面是一個示例代碼:

def callByName(param: => Int): Unit = {
  println("Inside callByName")
  println("Param 1: " + param)
  println("Param 2: " + param)
}

def randomNumber(): Int = {
  println("Generating random number")
  scala.util.Random.nextInt(100)
}

callByName(randomNumber())

輸出:
Inside callByName
Generating random number
Param 1: 53
Generating random number
Param 2: 87

在上述示例中,定義了一個名為 callByName 的函數,它接受一個傳名參數 param。在函數體內,我們打印出兩次參數的值。

另外,定義了一個名為 randomNumber 的函數,它用於生成隨機數。在該函數內部,我們打印出生成隨機數的消息,並使用 scala.util.Random.nextInt 方法生成一個介於 0 到 100 之間的隨機數。

在主程序中,我們調用 callByName 函數,並將 randomNumber() 作為傳名參數傳遞進去。

當程序執行時,會先打印出 "Inside callByName" 的消息,然後兩次調用 param,即 randomNumber()。在每次調用時,都會重新生成一個新的隨機數,並打印出相應的值。

這説明傳名參數在每次使用時都會重新求值表達式,而不是在調用函數時進行求值。這樣可以實現按需執行和延遲計算的效果。

implicit

implicit 關鍵字用於定義隱式轉換和隱式參數。它可以用來簡化代碼,讓編譯器自動執行一些操作。

下面是一些使用 implicit 關鍵字的示例:

  • 隱式轉換:可以使用 implicit 關鍵字定義隱式轉換函數,讓編譯器自動將一種類型的值轉換為另一種類型的值。
implicit def intToString(x: Int): String = x.toString

val x: String = 1
println(x) // 輸出 "1"

在這個例子中,定義了一個隱式轉換函數 intToString,它接受一個 Int 類型的參數,並返回它的字符串表示。由於這個函數被定義為 implicit,因此編譯器會在需要時自動調用它。

在主程序中,我們將一個 Int 類型的值賦值給一個 String 類型的變量。由於類型不匹配,編譯器會嘗試尋找一個隱式轉換函數來將 Int 類型的值轉換為 String 類型的值。在這個例子中,編譯器找到了我們定義的 intToString 函數,並自動調用它將 1 轉換為 "1"

  • 隱式參數:可以使用 implicit 關鍵字定義隱式參數,讓編譯器自動為方法提供參數值。
implicit val x: Int = 1

def foo(implicit x: Int): Unit = println(x)

foo // 輸出 1

在這個例子中,定義了一個隱式值 x 並賦值為 1。然後我們定義了一個方法 foo,它接受一個隱式參數 x

在主程序中,我們調用了方法 foo,但沒有顯式地傳入參數。由於方法 foo 接受一個隱式參數,因此編譯器會嘗試尋找一個隱式值來作為參數傳入。在這個例子中,編譯器找到了我們定義的隱式值 x 並將其作為參數傳入方法 foo

Object & Class

在Scala中,classobject 都可以用來定義類型,但它們之間有一些重要的區別。class 定義了一個類,它可以被實例化。每次使用 new 關鍵字創建一個類的實例時,都會創建一個新的對象。

class MyClass(x: Int) {
  def printX(): Unit = println(x)
}

val a = new MyClass(1)
val b = new MyClass(2)
a.printX() // 輸出 1
b.printX() // 輸出 2

構造器可以通過提供一個默認值來擁有可選參數:

class Point(var x: Int = 0, var y: Int = 0)

val origin = new Point  // x and y are both set to 0
val point1 = new Point(1)
println(point1.x)  // prints 1

在這個版本的Point類中,xy擁有默認值0所以沒有必傳參數。然而,因為構造器是從左往右讀取參數,所以如果僅僅要傳個y的值,你需要帶名傳參。

class Point(var x: Int = 0, var y: Int = 0)
val point2 = new Point(y=2)
println(point2.y)  // prints 2

object 定義了一個單例對象。它不能被實例化,也不需要使用 new 關鍵字創建。在程序中,一個 object 只有一個實例。此外,object 中定義的成員都是靜態的,這意味着它們可以在不創建實例的情況下直接訪問。而 class 中定義的成員只能在創建實例後訪問。

object MyObject {
  val x = 1
  def printX(): Unit = println(x)
}

MyObject.printX() // 輸出 1

另外,在Scala中,如果一個 object 的名稱與一個 class 的名稱相同,那麼這個 object 被稱為這個 class 的伴生對象。伴生對象和類可以相互訪問彼此的私有成員:

class MyClass(x: Int) {
  private val secret = 42
  def printCompanionSecret(): Unit = println(MyClass.companionSecret)
}

object MyClass {
  private val companionSecret = 24
  def printSecret(c: MyClass): Unit = println(c.secret)
}

val a = new MyClass(1)
a.printCompanionSecret() // 輸出 24
MyClass.printSecret(a) // 輸出 42

在這個例子中,定義了一個類 MyClass 和它的伴生對象 MyClass。類 MyClass 中定義了一個私有成員變量 secret 和一個方法 printCompanionSecret,用於打印伴生對象中的私有成員變量 companionSecret。而伴生對象 MyClass 中定義了一個私有成員變量 companionSecret 和一個方法 printSecret,用於打印類 MyClass 的實例中的私有成員變量 secret

在主程序中,創建了一個類 MyClass 的實例 a,並調用了它的 printCompanionSecret 方法。然後我們調用了伴生對象 MyClassprintSecret 方法,並將實例 a 作為參數傳入。

這就是Scala中類和伴生對象之間互相訪問私有成員的基本用法。

樣例類

樣例類(case class)是一種特殊的類,常用於描述不可變的值對象(Value Object)

它們非常適合用於不可變的數據。定義一個樣例類非常簡單,只需在類定義前加上case關鍵字即可。例如,下面是一個簡單的樣例類定義:

case class Person(var name: String, var age: Int)

創建樣例類的實例時,不需要使用new關鍵字,直接使用類名即可。例如,下面是一個創建樣例類實例並修改其成員變量的示例:

object Test01 {
  case class Person(var name: String, var age: Int)

  def main(args: Array[String]): Unit = {
    val z = Person("張三", 20)
    z.age = 23
    println(s"z = $z")
  }
}

_(下劃線)

在Scala中,下劃線 _ 是一個特殊的符號,它可以用在許多不同的地方,具有不同的含義。

  • 作為通配符:下劃線可以用作通配符,表示匹配任意值。例如,在模式匹配中,可以使用下劃線來表示匹配任意值。
x match {
  case 1 => "one"
  case 2 => "two"
  case _ => "other"
}
  • 作為忽略符:下劃線也可以用來忽略不需要的值。例如,在解構賦值時,可以使用下劃線來忽略不需要的值。
val (x, _, z) = (1, 2, 3)
  • 作為函數參數佔位符:下劃線還可以用作函數參數的佔位符,表示一個匿名函數的參數。例如,在調用高階函數時,可以使用下劃線來簡化匿名函數的定義。
val list = List(1, 2, 3)
list.map(_ * 2)
  • 將方法轉換為函數:在方法名稱後加一個下劃線,會將其轉化為偏應用函數(partially applied function),就能直接賦值了。
def add(x: Int, y: Int) = x + y
val f = add _

這只是下劃線在Scala中的一些常見用法。由於下劃線在不同的上下文中具有不同的含義,因此在使用時需要根據具體情況進行判斷。

println

println 函數用於向標準輸出打印一行文本。它可以接受多種不同類型的參數,並將它們轉換為字符串進行輸出。

下面是一些常見的使用 println 函數進行輸出的方式:

  • 輸出字符串:直接將字符串作為參數傳入 println 函數,它會將字符串原樣輸出。
println("Hello, world!")
  • 輸出變量:將變量作為參數傳入 println 函數,它會將變量的值轉換為字符串並輸出。
val x = 1
println(x)
  • 輸出表達式:將表達式作為參數傳入 println 函數,它會計算表達式的值並將其轉換為字符串輸出。
val x = 1
val y = 2
println(x + y)
  • 使用字符串插值:可以使用字符串插值來格式化輸出。在字符串前加上 s 前綴,然後在字符串中使用 ${expression} 的形式來插入表達式的值。
val name = "Alice"
val age = 18
println(s"My name is $name and I am $age years old.")

這些是 println 函數的一些常見用法。你可以根據需要使用不同的方式來格式化輸出。

集合

在Scala中,集合有三大類:序列Seq、集Set、映射Map,所有的集合都擴展自Iterable,所以Scala中的集合都可以使用 foreach方法。在Scala中集合有可變(mutable)和不可變(immutable)兩種類型。

List

如我們可以使用如下方式定義一個List,其他集合類型的定義方式也差不多。

object Main {
  def main(args: Array[String]): Unit = {
    // 定義一個空的字符串列表
    var emptyList: List[String] = List()
    // 定義一個具有數據的列表
    var intList = List(1, 2, 3, 4, 5, 6)
    // 定義空列表
    var emptyList2 = Nil
    // 使用::運算符連接元素
    var numList = 1 :: (2 :: (3 :: Nil))
    println(emptyList)
    println(intList)
    println(emptyList2)
    println(numList)
  }
}

輸出:
List()
List(1, 2, 3, 4, 5, 6)
List()
List(1, 2, 3)

下面是一些List的常用方法:

val list = List(1, 2, 3, 4)

// 獲取列表的長度
val length = list.length

// 獲取列表的第一個元素
val first = list.head

// 獲取列表的最後一個元素
val last = list.last

// 獲取列表除第一個元素外剩餘的元素
val tail = list.tail

// 獲取列表除最後一個元素外剩餘的元素
val init = list.init

// 反轉列表
val reversed = list.reverse

// 在列表頭部添加元素
val newList1 = 0 +: list

// 在列表尾部添加元素
val newList2 = list :+ 5

// 連接兩個列表
val list1 = List(1, 2)
val list2 = List(3, 4)
val concatenatedList = list1 ++ list2

// 檢查列表是否為空
val isEmpty = list.isEmpty

// 檢查列表是否包含某個元素
val containsElement = list.contains(1)

// 過濾列表中的元素
val filteredList = list.filter(_ > 2)

// 映射列表中的元素
val mappedList = list.map(_ * 2)

// 摺疊列表中的元素(從左到右)
val sum1 = list.foldLeft(0)(_ + _)

// 摺疊列表中的元素(從右到左)
val sum2 = list.foldRight(0)(_ + _)

// 拉鍊操作
val names = List("Alice", "Bob", "Charlie")
val ages = List(25, 32, 29)
val zipped = names.zip(ages) // List(("Alice", 25), ("Bob", 32), ("Charlie", 29))

// 拉鍊操作後解壓縮
val (unzippedNames, unzippedAges) = zipped.unzip // (List("Alice", "Bob", "Charlie"), List(25, 32, 29))

更多方法不再贅述,網上很容易查閲到相關文章。

Map

object Main {
  def main(args: Array[String]): Unit = {
    // 定義一個空的映射
    val emptyMap = Map()
    // 定義一個具有數據的映射
    val intMap = Map("key1" -> 1, "key2" -> 2)
    // 使用元組定義一個映射
    val tupleMap = Map(("key1", 1), ("key2", 2))
    println(emptyMap)
    println(intMap)
    println(tupleMap)
  }
}

輸出:
Map()
Map(key1 -> 1, key2 -> 2)
Map(key1 -> 1, key2 -> 2)

下面是map常用的一些方法:

val map = Map("key1" -> 1, "key2" -> 2)

// 獲取映射的大小
val size = map.size

// 獲取映射中的所有鍵
val keys = map.keys

// 獲取映射中的所有值
val values = map.values

// 檢查映射是否為空
val isEmpty = map.isEmpty

// 檢查映射是否包含某個鍵
val containsKey = map.contains("key1")

// 獲取映射中某個鍵對應的值
val value = map("key1")

// 獲取映射中某個鍵對應的值,如果不存在則返回默認值
val valueOrDefault = map.getOrElse("key3", 0)

// 過濾映射中的元素
val filteredMap = map.filter { case (k, v) => v > 1 }

// 映射映射中的元素
val mappedMap = map.map { case (k, v) => (k, v * 2) }

// 遍歷映射中的元素
map.foreach { case (k, v) => println(s"key: $k, value: $v") }

這裏的case關鍵字起到匹配的作用。

Range

Range屬於序列(Seq)這一類集合的子集。它表示一個整數序列,可以用來遍歷一個整數區間內的所有整數。例如,1 to 5表示一個從1到5的整數序列,包括1和5。

Range常見於for循環中,如下可定義一個Range:

// 定義一個從1到5的整數序列,包括1和5
val range1 = 1 to 5

// 定義一個從1到5的整數序列,包括1但不包括5
val range2 = 1 until 5

// 定義一個從1到10的整數序列,步長為2
val range3 = 1 to 10 by 2

// 定義一個從10到1的整數序列,步長為-1
val range4 = 10 to 1 by -1

如果我們想把Range轉為List,我們可以這樣做:

val range = 1 to 5
val list = range.toList

Range繼承自Seq,因此它擁有Seq的所有常用方法,例如lengthheadlasttailinitreverseisEmptycontainsfiltermapfoldLeftfoldRight等。它還擁有一些特殊的方法,例如:

val range = 1 to 10 by 2

// 獲取序列的起始值
val start = range.start

// 獲取序列的結束值
val end = range.end

// 獲取序列的步長
val step = range.step

// 獲取一個包括結束值的新序列
val inclusiveRange = range.inclusive

迭代器

迭代器(Iterator)是一種用於遍歷集合中元素的工具。它提供了一種方法來訪問集合中的元素,而不需要暴露集合的內部結構。在 Scala 中,你可以使用 iterator 方法來獲取一個集合的迭代器。

object Main {
  def main(args: Array[String]): Unit = {

    val list = List(1, 2, 3)
    val iterator = list.iterator

    // 1. 使用 hasNext 方法來檢查迭代器中是否還有元素
    val hasMoreElements = iterator.hasNext
    println(s"Has more elements: $hasMoreElements")

    // 2. 使用 next 方法來獲取迭代器中的下一個元素
    val nextElement = iterator.next()
    println(s"Next element: $nextElement")

    // 注意:上面的代碼已經將迭代器移動到了第二個元素,因此下面的代碼將從第二個元素開始執行

    // 3. 使用 size 方法來獲取迭代器中元素的個數
    val size = iterator.size
    println(s"Size: $size")

    val size1 = iterator.size
    println(s"Size1: $size1")

    // 注意:上面的代碼已經將迭代器移動到了末尾,因此下面的代碼將不再有效

    // 4. 使用 contains 方法來檢查迭代器中是否包含某個元素
    val containsElement = iterator.contains(2)
    println(s"Contains element: $containsElement")
  }
}

輸出:
Has more elements: true
Next element: 1
Size: 2
Size1: 0
Contains element: false

特別注意:迭代器是一次性的,所以在使用完畢後就不能再次使用。因此,在上面的代碼中,我們在調用 next 方法後就不能再使用其他方法來訪問迭代器中的元素了。所以 size1輸出為0。

Tuple

Tuple從集合中抽出來講述是因為Tuple不屬於集合。它是一種用來將多個值組合在一起的數據結構。一個Tuple可以包含不同類型的元素,每個元素都有一個固定的位置。Scala 中的元組包含一系列類:Tuple2,Tuple3等,直到 Tuple22。

示例如下:

object Main {
  def main(args: Array[String]): Unit = {
    // 定義一個包含兩個元素的Tuple
    val tuple1 = (1, "hello")
    println(tuple1)

    // 定義一個包含三個元素的Tuple
    val tuple2 = (1, "hello", true)
    println(tuple2)

    // 定義一個包含多個不同類型元素的Tuple
    val tuple3 = (1, "hello", true, 3.14)
    println(tuple3)

    // 訪問Tuple中的元素
    val firstElement = tuple3._1
    val secondElement = tuple3._2
    println(s"first element: $firstElement, second element: $secondElement")
  }
}

輸出:
(1,hello)
(1,hello,true)
(1,hello,true,3.14)
first element: 1, second element: hello

下面是一些Tuple的常用方法:

object Main {
  def main(args: Array[String]): Unit = {
    val tuple = (1, "hello")
    // 交換二元組的元素
    // 輸出:(hello,1)
    val swapped = tuple.swap

    // 使用 copy 方法來創建一個新的 Tuple,其中某些元素被替換為新值
    //輸出:(1,world)
    val newTuple = tuple.copy(_2 = "world")

    // 遍歷元素
    // 輸出: 1 hello
    tuple.productIterator.foreach(println)

    // 轉換為字符串
    // 輸出: (1,hello)
    val stringRepresentation = tuple.toString

    // 使用 Tuple.productArity 方法來獲取 Tuple 中元素的個數
    // 輸出: 2
    val arity = tuple.productArity

    // 使用 Tuple.productElement 方法來訪問 Tuple 中的元素
    // 輸出: 1
    val firstElement = tuple.productElement(0)
  }
}

提取器對象

提取器對象是一個包含有 unapply 方法的單例對象。apply 方法就像一個構造器,接受參數然後創建一個實例對象,反之 unapply 方法接受一個實例對象然後返回最初創建它所用的參數。提取器常用在模式匹配和偏函數中。

下面是一個使用提取器對象(Extractor Object)的 Scala 代碼示例:

object Email {
  def apply(user: String, domain: String): String = s"$user@$domain"
  
  def unapply(email: String): Option[(String, String)] = {
    val parts = email.split("@")
    if (parts.length == 2) Some(parts(0), parts(1))
    else None
  }
}

// 測試
val address = "john.doe@example.com"
address match {
  case Email(user, domain) => println(s"User: $user, Domain: $domain")
  case _ => println("Invalid email address")
}

在上述示例中,定義了一個名為Email的提取器對象。提取器對象具有兩個方法:applyunapply

apply方法接收用户名和域名作為參數,並返回一個完整的電子郵件地址。在這個示例中,我們簡單地將用户名和域名拼接成電子郵件地址的字符串。

unapply方法接收一個電子郵件地址作為參數,並返回一個Option類型的元組。在這個示例中,我們使用split方法將電子郵件地址分割為用户名和域名兩部分,並通過Some將它們封裝到一個Option中返回。如果分割後的部分不是兩部分,即電子郵件地址不符合預期的格式,我們返回None

在測試部分,我們創建了一個電子郵件地址字符串address。然後,我們使用match表達式將address與提取器對象Email進行匹配。如果匹配成功,我們提取出用户名和域名,並打印出對應的信息。如果匹配失敗,即電子郵件地址無效,我們打印出相應的錯誤信息。

流程判斷

while & if

object Main {
  def main(args: Array[String]): Unit = {
    println("----while----")
    var i = 0
    while (i < 5) {
      println(i)
      i += 1
    }

    println("----if----")
    val x = 3
    if (x > 0) {
      println("x大於0")
    } else {
      println("x小於0")
    }
  }
}

輸出:
----while----
0
1
2
3
4
----if----
x大於0

Scala中的while和if跟Java中的方法幾乎沒有區別。

for

object Main {
  def main(args: Array[String]): Unit = {
  println("----for循環----")
    for (i <- 1 to 5) {
      println(i)
    }
  }
}

輸出:
----for循環----
1
2
3
4
5

for循環跟Java略微有點區別。其中i <- 1 to 5是Scala中for循環的一種常見形式。它表示遍歷一個序列,序列中的元素依次為1、2、3、4、5。

多重for循環簡寫

Scala中對於多重for循環可以進行簡寫,例如我們要用Java寫多重for循環是下面這樣:

public class Main {
  public static void main(String[] args) {
    // 多重for循環
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        System.out.println(i + " " + j);
      }
    }
  }
}

而用Scala我們可以直接簡寫為下面這樣:

object Main {
  def main(args: Array[String]): Unit = {
    // 多重for循環
    for (i <- 0 until 3; j <- 0 until 3) {
      println(i + " " + j)
    }
  }
}

輸出:
0 0
0 1
0 2
1 0
1 1
1 2
2 0
2 1
2 2

可以看出scala的for循環語法更加的精簡。代碼行數更少。

yield

在for循環的過程中我們可以使用 yield來對for循環的元素進行 操作收集:

object Main {
  def main(args: Array[String]): Unit = {
    val numbers = for (i <- 1 to 5) yield i * 2
    println(numbers)
  }
}

輸出:
Vector(2, 4, 6, 8, 10)

模式匹配(pattern matching)

在Scala語言中,沒有switchcase關鍵字。相反,我們可以使用模式匹配(pattern matching)來實現類似於switch語句的功能。它是Java中的switch語句的升級版,同樣可以用於替代一系列的 if/else 語句。下面是一個簡單的例子,它展示瞭如何使用模式匹配來實現類似於switch語句的功能:

object Main {
  def main(args: Array[String]): Unit = {
    def matchTest(x: Any): String = x match {
      case 1 => "one"
      case "two" => "two"
      case y: Int => "scala.Int"
      case _ => "many"
    }

    println(matchTest(1))
    println(matchTest("two"))
    println(matchTest(3))
    println(matchTest("test"))
  }
}

輸出:
one
two
scala.Int
many

在上面的例子中,定義了一個名為matchTest的函數,它接受一個類型為Any的參數x。在函數體中,我們使用了一個模式匹配表達式來匹配參數x的值。

在模式匹配表達式中,我們定義了四個case子句。第一個case子句匹配值為1的情況;第二個case子句匹配值為"two"的情況;第三個case子句匹配類型為Int的情況;最後一個case子句匹配所有其他情況。

樣例類(case classes)的匹配

樣例類非常適合用於模式匹配。

abstract class Notification

case class Email(sender: String, title: String, body: String) extends Notification

case class SMS(caller: String, message: String) extends Notification

def showNotification(notification: Notification): String = {
  notification match {
    case Email(sender, title, _) =>
      s"You got an email from $sender with title: $title"
    case SMS(number, message) =>
      s"You got an SMS from $number! Message: $message"
  }
}

val someSms = SMS("12345", "Are you there?")
val someEmail = Email("John Doe", "Meeting", "Are we still meeting tomorrow?")

println(showNotification(someSms))
println(showNotification(someEmail))

這段代碼定義了一個抽象類 Notification,以及兩個擴展自 Notification 的樣例類 EmailSMS。然後定義了一個函數 showNotification,它接受一個 Notification 類型的參數,並使用模式匹配來檢查傳入的通知是 Email 還是 SMS,並相應地生成一條消息。

最後,我們創建了兩個實例:一個 SMS 和一個 Email,並使用 showNotification 函數來顯示它們的消息。

模式守衞(Pattern guards)

為了讓匹配更加具體,可以使用模式守衞,也就是在模式後面加上if <boolean expression>

def checkNumberType(number: Int): String = number match {
  case n if n > 0 && n % 2 == 0 => "Positive even number"
  case n if n > 0 && n % 2 != 0 => "Positive odd number"
  case n if n < 0 && n % 2 == 0 => "Negative even number"
  case n if n < 0 && n % 2 != 0 => "Negative odd number"
  case _ => "Zero"
}

// 測試
println(checkNumberType(10))    // 輸出: Positive even number
println(checkNumberType(15))    // 輸出: Positive odd number
println(checkNumberType(-4))    // 輸出: Negative even number
println(checkNumberType(-9))    // 輸出: Negative odd number
println(checkNumberType(0))     // 輸出: Zero

在上述示例中,我們定義了一個名為checkNumberType的方法,它接收一個整數參數number並返回一個描述數字類型的字符串。

通過使用模式守衞,我們可以對number進行多個條件的匹配,並根據條件來返回相應的結果。在每個case語句中,我們使用模式守衞來進一步過濾匹配的數字。

例如,case n if n > 0 && n % 2 == 0 表示當 number 大於 0 且為偶數時執行該分支。類似地,其他的 case 語句也使用了模式守衞來進行更精確的匹配。

在測試部分,我們調用了checkNumberType方法並傳入不同的整數進行測試。根據不同的輸入,方法將返回相應的字符串描述數字類型。

僅匹配類型

當不同類型對象需要調用不同方法時,僅匹配類型的模式非常有用

def processValue(value: Any): String = value match {
  case str: String => s"Received a String: $str"
  case num: Int => s"Received an Int: $num"
  case lst: List[_] => s"Received a List: $lst"
  case _: Double => "Received a Double"
  case _ => "Unknown value"
}

// 測試
println(processValue("Hello"))                // 輸出: Received a String: Hello
println(processValue(10))                     // 輸出: Received an Int: 10
println(processValue(List(1, 2, 3)))           // 輸出: Received a List: List(1, 2, 3)
println(processValue(3.14))                    // 輸出: Received a Double
println(processValue(true))                    // 輸出: Unknown value

在上述示例中,定義了一個名為processValue的方法,它接收一個任意類型的參數value,並返回一個描述值類型的字符串。

通過使用類型模式匹配,我們可以根據不同的值類型來執行相應的邏輯。在每個case語句中,我們使用類型模式匹配來匹配特定類型的值。

例如,case str: String 表示當 value 的類型為 String 時執行該分支,並將其綁定到變量 str。類似地,其他的 case 語句也使用了類型模式匹配來匹配不同的值類型。

在測試部分,我們調用了processValue方法並傳入不同類型的值進行測試。根據值的類型,方法將返回相應的描述字符串。

Scala的模式匹配是我覺得非常實用和靈活的一個功能,比Java的switch語句更加強大和靈活。Scala的模式匹配可以匹配不同類型的值,包括數字、字符串、列表、元組等。而Java的switch語句只能匹配整數、枚舉和字符串類型的值。

密封類

特質(trait)和類(class)可以用sealed標記為密封的,這意味着其所有子類都必須與之定義在相同文件中,從而保證所有子類型都是已知的。密封類限制了可擴展的子類類型,並在模式匹配中確保所有可能的類型都被處理,提高了代碼的安全性和可靠性。

下面是一個使用密封類(sealed class)和模式匹配的 Scala 代碼示例:

sealed abstract class Shape

case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Square(side: Double) extends Shape

def calculateArea(shape: Shape): Double = shape match {
  case Circle(radius) => math.Pi * radius * radius
  case Rectangle(width, height) => width * height
  case Square(side) => side * side
}

// 測試
val circle = Circle(5.0)
val rectangle = Rectangle(3.0, 4.0)
val square = Square(2.5)

println(s"Area of circle: ${calculateArea(circle)}")         // 輸出: Area of circle: 78.53981633974483
println(s"Area of rectangle: ${calculateArea(rectangle)}")   // 輸出: Area of rectangle: 12.0
println(s"Area of square: ${calculateArea(square)}")         // 輸出: Area of square: 6.25

在上述示例中,我們定義了一個密封類Shape,它是一個抽象類,不能直接實例化。然後,我們通過擴展Shape類創建了CircleRectangleSquare這三個子類。

calculateArea方法中,我們使用模式匹配對傳入的shape進行匹配,並根據不同的Shape子類執行相應的邏輯。在每個case語句中,我們根據具體的形狀類型提取相應的屬性,並計算出面積。

在測試部分,我們創建了一個Circle對象、一個Rectangle對象和一個Square對象,並分別調用calculateArea方法計算它們的面積。

嵌套方法

當在Scala中定義一個方法時,我們可以選擇將其嵌套在另一個方法內部。這樣的嵌套方法只在外部方法的作用域內可見,而對於外部方法以外的代碼是不可見的。這可以幫助我們組織和封裝代碼,提高代碼的可讀性和可維護性。

def calculateDiscountedPrice(originalPrice: Double, discountPercentage: Double): Double = {
  def applyDiscount(price: Double, discount: Double): Double = {
    val discountedPrice = price - (price * discount)
    discountedPrice
  }

  def validateDiscount(discount: Double): Double = {
    val maxDiscount = 0.8 // 最大折扣為80%
    if (discount > maxDiscount) {
      maxDiscount
    } else {
      discount
    }
  }

  val validatedDiscount = validateDiscount(discountPercentage)
  val finalPrice = applyDiscount(originalPrice, validatedDiscount)
  finalPrice
}

// 調用外部方法
val price = calculateDiscountedPrice(100.0, 0.9)
println(s"The final price is: $price")

在上述示例中,定義了一個外部方法calculateDiscountedPrice,它接收原始價格originalPrice和折扣百分比discountPercentage作為參數,並返回最終價格。

calculateDiscountedPrice方法的內部,我們定義了兩個嵌套方法:applyDiscountvalidateDiscountapplyDiscount方法用於計算折扣後的價格,它接收價格和折扣作為參數,並返回折扣後的價格。validateDiscount方法用於驗證折扣百分比是否超過最大折扣限制,並返回一個有效的折扣百分比。

在外部方法中,我們首先調用validateDiscount方法來獲取有效的折扣百分比,然後將其與原始價格一起傳遞給applyDiscount方法,計算最終價格。最後,我們打印出最終價格。

正則表達式模型

正則表達式是用來找出數據中的指定模式(或缺少該模式)的字符串。.r方法可使任意字符串變成一個正則表達式。

object Main extends App {
  val emailPattern = "([a-zA-Z0-9_.+-]+)@([a-zA-Z0-9-]+)\\.([a-zA-Z0-9-.]+)".r

  def validateEmail(email: String): Boolean = email match {
    case emailPattern(username, domain, extension) =>
      println(s"Valid email address: $email")
      true
    case _ =>
      println(s"Invalid email address: $email")
      false
  }

  // 測試
  validateEmail("john.doe@example.com")        // 輸出: Valid email address: john.doe@example.com
  validateEmail("jane.doe@invalid")            // 輸出: Invalid email address: jane.doe@invalid
}

在上述示例中,我們首先創建了一個名為emailPattern的正則表達式對象,用於匹配電子郵件地址的模式。

然後,定義了一個名為validateEmail的方法,它接收一個字符串類型的電子郵件地址作為參數,並使用正則表達式模式匹配來驗證電子郵件地址的有效性。

在模式匹配的case語句中,我們使用emailPattern對傳入的電子郵件地址進行匹配,並將匹配結果中的用户名、域名和擴展提取到相應的變量中。如果匹配成功,我們打印出驗證通過的消息,並返回true表示電子郵件地址有效。如果沒有匹配成功,則打印出驗證失敗的消息,並返回false表示電子郵件地址無效。

在測試部分,我們調用validateEmail方法分別傳入一個有效的電子郵件地址和一個無效的電子郵件地址進行測試。根據匹配結果,我們打印出相應的驗證消息。

型變

在 Scala 中,協變(covariance)和逆變(contravariance)是用來描述類型參數在子類型關係中的行為的概念。協變和逆變是用來指定泛型類型參數的子類型關係的方式,以確保類型安全性。

協變

協變(Covariance): 協變表示類型參數在子類型關係中具有相同的方向。如果一個泛型類的類型參數是協變的,那麼子類型的關係將保持不變,即父類型可以被替換為子類型。在 Scala 中,可以使用 + 符號來表示協變。

下面是一個使用協變的示例代碼,使用 + 符號表示類型參數 A 是協變的:

class Animal
class Dog extends Animal

class Cage[+A]

val dogCage: Cage[Dog] = new Cage[Dog]
val animalCage: Cage[Animal] = dogCage

在上述示例中,我們定義了一個協變類 Cage[+A],它接受一個類型參數 A,並使用 + 符號來表示 A 是協變的。我們創建了一個 dogCage,它是一個 Cage[Dog] 類型的實例。然後,我們將 dogCage 賦值給一個類型為 Cage[Animal] 的變量 animalCage,這是合法的,因為 Cage[+A] 的協變性允許我們將子類型的 Cage 賦值給父類型的 Cage

逆變

逆變(Contravariance): 逆變表示類型參數在子類型關係中具有相反的方向。如果一個泛型類的類型參數是逆變的,那麼子類型的關係將反轉,即父類型可以替換為子類型。在 Scala 中,可以使用 - 符號來表示逆變。

下面是一個使用逆變的示例代碼,使用 - 符號表示類型參數 A 是逆變的:

class Animal
class Dog extends Animal

class Cage[-A]

val animalCage: Cage[Animal] = new Cage[Animal]
val dogCage: Cage[Dog] = animalCage

在上述示例中,定義了一個逆變類 Cage[-A],它接受一個類型參數 A,並使用 - 符號來表示 A 是逆變的。我們創建了一個 animalCage,它是一個 Cage[Animal] 類型的實例。然後,我們將 animalCage 賦值給一個類型為 Cage[Dog] 的變量 dogCage,這是合法的,因為 Cage[-A] 的逆變性允許我們將父類型的 Cage 賦值給子類型的 Cage
通過協變和逆變,我們可以在 Scala 中實現更靈活的類型關係,並確保類型安全性。這在處理泛型集合或函數參數時特別有用。下面是一個更具體的示例:

abstract class Animal {
  def name: String
}

class Dog(val name: String) extends Animal {
  def bark(): Unit = println("Woof!")
}

class Cat(val name: String) extends Animal {
  def meow(): Unit = println("Meow!")
}

class Cage[+A](val animal: A) {
  def showAnimal(): Unit = println(animal.name)
}

def printAnimalNames(cage: Cage[Animal]): Unit = {
  cage.showAnimal()
}

val dog: Dog = new Dog("Fido")
val cat: Cat = new Cat("Whiskers")

val dogCage: Cage[Dog] = new Cage[Dog](dog)
val catCage: Cage[Cat] = new Cage[Cat](cat)

printAnimalNames(dogCage) // 輸出:Fido
printAnimalNames(catCage) // 輸出:Whiskers

在上述示例中,定義了一個抽象類 Animal,以及它的兩個子類 DogCatDogCat 類都實現了 name 方法。

然後,定義了一個協變類 Cage[+A],它接受一個類型參數 A,並使用協變符號 + 表示 A 是協變的。Cage 類有一個名為 animal 的屬性,它的類型是 A,也就是動物的類型。我們定義了一個名為 showAnimal() 的方法,它打印出 animal 的名稱。

接下來,定義了一個名為 printAnimalNames() 的函數,它接受一個類型為 Cage[Animal] 的參數,並打印出其中動物的名稱。

我們創建了一個 Dog 類型的對象 dog 和一個 Cat 類型的對象 cat。然後,我們分別創建了一個 Cage[Dog] 類型的 dogCage 和一個 Cage[Cat] 類型的 catCage

最後,我們分別調用 printAnimalNames() 函數,並傳入 dogCagecatCage。由於 Cage 類是協變的,所以可以將 Cage[Dog]Cage[Cat] 賦值給 Cage[Animal] 類型的參數,而不會產生類型錯誤。

類型限界

在 Scala 中,類型上界(Upper Bounds)和類型下界(Lower Bounds)是用於限制泛型類型參數的範圍的概念。它們允許我們在泛型類或泛型函數中指定類型參數必須滿足某種條件。下面是關於類型上界和類型下界的解釋和示例代碼:

類型上界

類型上界(Upper Bounds): 類型上界用於指定泛型類型參數必須是某個類型或其子類型。我們使用 <: 符號來定義類型上界。例如,A <: B 表示類型參數 A 必須是類型 B 或其子類型。

下面是一個使用類型上界的示例代碼:

abstract class Animal {
  def name: String
}

class Dog(val name: String) extends Animal {
  def bark(): Unit = println("Woof!")
}

class Cage[A <: Animal](val animal: A) {
  def showAnimal(): Unit = println(animal.name)
}

val dog: Dog = new Dog("Fido")
val cage: Cage[Animal] = new Cage[Dog](dog)

cage.showAnimal() // 輸出:Fido

在上述示例中,定義了一個抽象類 Animal,以及它的子類 DogDog 類繼承自 Animal 類,並實現了 name 方法。

然後,定義了一個泛型類 Cage[A <: Animal],它接受一個類型參數 A,並使用類型上界 A <: Animal 來確保 AAnimal 類型或其子類型。Cage 類有一個名為 animal 的屬性,它的類型是 A。我們定義了一個名為 showAnimal() 的方法,它打印出 animal 的名稱。

創建了一個 Dog 類型的對象 dog。然後,我們創建了一個 Cage[Animal] 類型的 cage,並將 dog 對象作為參數傳遞給它。

最後,調用 cageshowAnimal() 方法,它成功打印出了 Dog 對象的名稱。

類型下界

類型下界(Lower Bounds): 類型下界用於指定泛型類型參數必須是某個類型或其父類型。我們使用 > 符號來定義類型下界。例如,A >: B 表示類型參數 A 必須是類型 B 或其父類型。

下面是一個使用類型下界的示例代碼:

class Animal {
  def sound(): Unit = println("Animal sound")
}

class Dog extends Animal {
  override def sound(): Unit = println("Dog barking")
}

class Cat extends Animal {
  override def sound(): Unit = println("Cat meowing")
}

def makeSound[A >: Dog](animal: A): Unit = {
  animal.sound()
}

val dog: Dog = new Dog
val cat: Cat = new Cat

makeSound(dog) // 輸出:Dog barking
makeSound(cat) // 輸出:Animal sound

在上述示例中,定義了一個基類 Animal,以及兩個子類 DogCat。這些類都有一個 sound() 方法,用於輸出不同的動物聲音。

接下來,定義了一個泛型函數 makeSound[A >: Dog](animal: A),其中類型參數 A 的下界被定義為 Dog,即 A >: Dog。這意味着 A 必須是 Dog 類型或其父類型。

makeSound() 函數內部,我們調用傳入的 animal 對象的 sound() 方法。

然後,創建了一個 Dog 對象 dog 和一個 Cat 對象 cat

最後,分別調用 makeSound() 函數,並將 dogcat 作為參數傳遞進去。由於類型下界被定義為 Dog,所以 dog 參數符合條件,而 cat 參數被隱式地向上轉型為 Animal,也滿足條件。因此,調用 makeSound() 函數時,輸出了不同的聲音。

通過類型上界和類型下界,我們可以對泛型類型參數的範圍進行限制,以確保類型的約束和類型安全性。這使得我們能夠編寫更靈活、可複用且類型安全的代碼。

內部類

在 Scala 中,內部類是一個定義在另一個類內部的類。內部類可以訪問外部類的成員,並具有更緊密的關聯性。下面是一個關於 Scala 中內部類的解釋和示例代碼:

在 Scala 中,內部類可以分為兩種類型:成員內部類(Member Inner Class)局部內部類(Local Inner Class)

成員內部類:成員內部類是定義在外部類的作用域內,並可以直接訪問外部類的成員(包括私有成員)。成員內部類可以使用外部類的實例來創建和訪問。

下面是一個示例代碼:

class Outer {
  private val outerField: Int = 10

  class Inner {
    def printOuterField(): Unit = {
      println(s"Outer field value: $outerField")
    }
  }
}

val outer: Outer = new Outer
val inner: outer.Inner = new outer.Inner
inner.printOuterField() // 輸出:Outer field value: 10

在上述示例中,定義了一個外部類 Outer,它包含一個私有成員 outerField。內部類 Inner 定義在 Outer 的作用域內,並可以訪問外部類的成員。

在主程序中,創建了外部類的實例 outer。然後,我們使用 outer.Inner 來創建內部類的實例 inner。注意,我們需要使用外部類的實例來創建內部類的實例。

最後,調用內部類 innerprintOuterField() 方法,它成功訪問並打印了外部類的私有成員 outerField

局部內部類: 局部內部類是定義在方法或代碼塊內部的類。局部內部類的作用域僅限於所在方法或代碼塊內部,無法從外部訪問。

下面是一個示例代碼:

def outerMethod(): Unit = {
  val outerField: Int = 10

  class Inner {
    def printOuterField(): Unit = {
      println(s"Outer field value: $outerField")
    }
  }

  val inner: Inner = new Inner
  inner.printOuterField() // 輸出:Outer field value: 10
}

outerMethod()

在上述示例中,定義了一個外部方法 outerMethod。在方法內部,我們定義了一個局部變量 outerField 和一個局部內部類 Inner

在方法內部,創建了內部類 Inner 的實例 inner。注意,內部類的作用域僅限於方法內部。

最後,調用內部類 innerprintOuterField() 方法,它成功訪問並打印了外部變量 outerField

通過使用內部類,我們可以在 Scala 中實現更緊密的關聯性和封裝性,同時允許內部類訪問外部類的成員。內部類在某些場景下可以提供更清晰和組織良好的。

複合類型

在 Scala 中,複合類型(Compound Types)允許我們定義一個類型,它同時具有多個特質(Traits)或類的特性。複合類型可以用於限制一個對象的類型,以便它同時具備多個特性。下面是關於複合類型的解釋和示例代碼:

複合類型使用 with 關鍵字將多個特質或類組合在一起,形成一個新的類型。

下面是一個示例代碼:

trait Flyable {
  def fly(): Unit
}

trait Swimmable {
  def swim(): Unit
}

class Bird extends Flyable {
  override def fly(): Unit = println("Flying...")
}

class Fish extends Swimmable {
  override def swim(): Unit = println("Swimming...")
}

def action(obj: Flyable with Swimmable): Unit = {
  obj.fly()
  obj.swim()
}

val bird: Bird = new Bird
val fish: Fish = new Fish

action(bird) // 輸出:Flying...
action(fish) // 輸出:Swimming...

在上述示例中,定義了兩個特質 FlyableSwimmable,分別表示可飛行和可游泳的特性。然後,我們定義了兩個類 BirdFish,分別實現了相應的特質。

接下來,定義了一個方法 action,它接受一個類型為 Flyable with Swimmable 的參數。這表示參數必須同時具備 FlyableSwimmable 的特性。

在主程序中,創建了一個 Bird 對象 bird 和一個 Fish 對象 fish

最後,分別調用 action 方法,並將 birdfish 作為參數傳遞進去。由於它們都同時具備 FlyableSwimmable 的特性,所以可以成功調用 fly()swim() 方法。

通過使用複合類型,可以在 Scala 中定義一個類型,它同時具備多個特質或類的特性,從而實現更靈活和精確的類型約束。這有助於編寫更可靠和可複用的代碼。

多態方法

在 Scala 中,多態方法(Polymorphic Methods)允許我們定義可以接受多種類型參數的方法。這意味着同一個方法可以根據傳入參數的類型執行不同的邏輯。下面是關於多態方法的解釋和示例代碼:

多態方法使用類型參數來定義方法的參數類型,並使用泛型來表示可以接受多種類型參數。在方法內部,可以根據類型參數的實際類型執行不同的邏輯。

下面是一個示例代碼:

def printType[T](value: T): Unit = {
  value match {
    case s: String => println("String: " + s)
    case i: Int => println("Int: " + i)
    case d: Double => println("Double: " + d)
    case _ => println("Unknown type")
  }
}

printType("Hello") // 輸出:String: Hello
printType(123) // 輸出:Int: 123
printType(3.14) // 輸出:Double: 3.14
printType(true) // 輸出:Unknown type

在上述示例中,定義了一個多態方法 printType,它接受一個類型參數 T。根據傳入參數的類型,我們使用模式匹配來判斷其實際類型,並執行相應的邏輯。

在方法內部,使用 match 表達式對傳入的參數 value 進行模式匹配。對於不同的類型,我們分別輸出相應的類型信息。

在主程序中,多次調用 printType 方法,並傳入不同類型的參數。根據傳入的參數類型,方法會執行相應的邏輯並輸出對應的類型信息。

函數

Scala中一個簡單的函數定義如下,我們可以在Scala中使用JDK的類:

import java.util.Date
object Main {
  def main(args: Array[String]): Unit = {
    printCurrentDate() // 輸出當前日期和時間
  }
  def printCurrentDate(): Unit = {
    val currentDate = new Date()
    println(currentDate.toString)
  }
}

函數默認值

在 Scala 中,可以為函數參數指定默認值。這樣,當調用函數時如果沒有提供參數值,將使用默認值。下面是一個簡單的示例:

object Main {
  def main(args: Array[String]): Unit = {
    greet() // 輸出 "Hello, World!"
    greet("Alice") // 輸出 "Hello, Alice!"
  }

  def greet(name: String = "World"): Unit = {
    println(s"Hello, $name!")
  }
}

高階函數

高階函數是指使用其他函數作為參數、或者返回一個函數作為結果的函數。在Scala中函數是“一等公民”,所以允許定義高階函數。這裏的術語可能有點讓人困惑,我們約定,使用函數值作為參數,或者返回值為函數值的“函數”和“方法”,均稱之為“高階函數”。

def applyFuncToList(list: List[Int], f: Int => Int): List[Int] = {
  list.map(f)
}

val numbers = List(1, 2, 3, 4)
val double = (x: Int) => x * 2
val doubledNumbers = applyFuncToList(numbers, double) // List(2, 4, 6, 8)

在這個例子中,applyFuncToList 函數接受一個整數列表和一個函數 f,該函數將一個整數作為輸入並返回一個整數。然後,applyFuncToList 函數使用 map 方法將函數 f 應用於列表中的每個元素。在上面的代碼中,我們定義了一個 double 函數,它將輸入乘以2,並將其傳遞給 applyFuncToList 函數以對數字列表中的每個元素進行加倍。

匿名函數

在 Scala 中,匿名函數是一種沒有名稱的函數,可以用來創建簡潔的函數字面量。它們通常用於傳遞給高階函數,或作為局部函數使用。

例如,下面是一個簡單的匿名函數,它接受兩個整數參數並返回它們的和:

object Main {
  def main(args: Array[String]): Unit = {
    val add = (x: Int, y: Int) => x + y
    println(add(1, 2))  //輸出: 3
  }
}

偏應用函數

簡單來説,偏應用函數就是一種只對輸入值的某個子集進行處理的函數。它只會對符合特定條件的輸入值進行處理,而對於不符合條件的輸入值則會拋出異常。

舉個例子:

object Main {
  def main(args: Array[String]): Unit = {

    println(divide.isDefinedAt(0)) // false
    println(divideSafe.isDefinedAt(0)) // true

    println(divide(1)) // 42
    println(divideSafe(1)) // Some(42)

    // println(divide(0)) // 拋出異常
    println(divideSafe(0)) // None
  }

  val divide: PartialFunction[Int, Int] = {
    case d: Int if d != 0 => 42 / d
  }

  val divideSafe: PartialFunction[Int, Option[Int]] = {
    case d: Int if d != 0 => Some(42 / d)
    case _ => None
  }

}

這個例子中,divide 是一個偏應用函數,它只定義了對非零整數的除法運算。如果我們嘗試用 divide 函數去除以零,它會拋出一個異常。其中isDefinedAt 是一個方法,它用於檢查偏應用函數是否在給定的輸入值上定義。如果偏應用函數在給定的輸入值上定義,那麼 isDefinedAt 方法會返回 true,否則返回 false

為了避免這種情況,我們可以使用 divideSafe 函數,它返回一個 Option 類型的結果。如果除數為零,它會返回 None 而不是拋出異常。

柯里化函數

柯里化(Currying)是一種將多參數函數轉換為一系列單參數函數的技術。我們可以使用柯里化來定義函數,例如:

def add(a: Int)(b: Int): Int = a + b

這個 add 函數接受兩個參數 ab,並返回它們的和。由於它是一個柯里化函數,所以我們可以將它看作是一個接受單個參數 a 的函數,它返回一個接受單個參數 b 的函數。

我們可以這樣調用這個函數:

val result = add(1)(2) // 3

或者這樣:

val addOne = add(1) _
val result = addOne(2) // 3

在上面的例子中,我們首先調用 add 函數並傳入第一個參數 1,然後我們得到一個新的函數 addOne,它接受一個參數並返回它與 1 的和。最後,我們調用 addOne 函數並傳入參數 2,得到結果 3。

柯里化函數可以幫助我們實現參數複用和延遲執行等功能。

柯里化函數的好處之一是它可以讓我們給一個函數傳遞較少的參數,得到一個已經記住了某些固定參數的新函數。這樣,我們就可以在不同的地方使用這個新函數,而不需要每次都傳遞相同的參數²。

此外,柯里化函數還可以幫助我們實現函數的延遲計算。當我們傳遞部分參數時,它會返回一個新的函數,可以在新的函數中繼續傳遞後面的參數。這樣,我們就可以根據需要來決定何時執行這個函數。

惰性函數

可以使用 lazy 關鍵字定義惰性函數。惰性函數的執行會被推遲,直到我們首次對其取值時才會執行。

下面是一個簡單的例子,展示瞭如何定義和使用惰性函數:

def sum(x: Int, y: Int): Int = {
  println("sum函數被執行了...")
  x + y
}

lazy val res: Int = sum(1, 2)

println("-----")
println(res)

在這個例子中,我們定義了一個函數 sum,它接受兩個參數並返回它們的和。然後我們定義了一個惰性值 res 並將其賦值為 sum(1, 2)

在主程序中,我們首先打印了一行分隔符。然後我們打印了變量 res 的值。由於 res 是一個惰性值,因此在打印它之前,函數 sum 並沒有被執行。只有當我們首次對 res 取值時,函數 sum 才會被執行。

這就是Scala中惰性函數的基本用法。你可以使用 lazy 關鍵字定義惰性函數,讓函數的執行被推遲。

總結

在總結之處,我希望強調Scala的美學和實用性。它是一種同時支持函數式編程和麪向對象編程的語言,Scala的語法設計使其對初學者非常友好,同時也為更深入地探索編程提供了空間。

學習Scala不僅能夠幫助你提高編程效率,還能開闊你的編程視野。當你熟練掌握Scala後,你將發現一個全新的、充滿無限可能的編程世界正在向你敞開。今天,我們只是輕輕掀開了Scala的神秘面紗,未來等待你去挖掘的還有更多。

請繼續探索和嘗試,讓自己真正理解並掌握Scala的精髓。持續學習,不斷思考,享受編程的樂趣。

最後,希望這篇文章能給你帶來收穫和思考。


感謝閲讀,如果本篇文章有任何錯誤和建議,歡迎給我留言指正。

老鐵們,關注我的微信公眾號「Java 隨想錄」,專注分享Java技術乾貨,文章持續更新,可以關注公眾號第一時間閲讀。

一起交流學習,期待與你共同進步!

Add a new Comments

Some HTML is okay.