Java語言從以前發展到現在也吸收了許多新東西,像是Java 8開始有Lambda和Stream讓寫起來可以更精簡有效率、Java 11開始增加var關鍵字讓編譯器自動型別推斷,加上Spring、Lombok等框架與套件,增添了更多方便功能,還能省略許多重複的程式碼。但Swift將許多功能直接加入語言本身,成為語言的一部分,像是Optional<T>直接用問號就可以宣告,List<E>Map<K, V>直接用中括號設值、取值,方便度也是更上一層樓。本篇文章提供給熟悉Java語言的人,以Java語言的角度來介紹Swift語言,為已經有程式語言基礎的人提供更快速了解Swift的介紹。

與Java之間的基本比較

類似Java的地方

  • 都是強型別語言,變數不會隱式轉換
  • 都是靜態型別語言,宣告變數時需要確定型別
  • 與Java 10之後一樣有的var關鍵字,宣告變數時可自動判斷型別
  • 使用let宣告的變數跟Java的final一樣是不可變常數,但有更多語法糖
  • 使用大括號標示不同程式區塊
  • 都有物件導向與垃圾回收機制
  • 都有類別(class)、繼承(extends)、覆寫(override)、泛形(generics)、列舉(enum)
  • 有類似介面(interface)的協定(protocol)
  • 都可以傳遞函數、匿名函數、閉包(closures)
  • 都有REPL互動模式,在Java 9開始使用jshell指令,在Swift使用swift指令

不同Java的地方

  • 語句/陳述(statement)的結尾不用分號
  • if、for、switch、do、while等關鍵字後面不需要使用括號包裹條件
  • switch case不用加上break,還多了where關鍵字
  • 宣告變數時有專屬關鍵字var
  • 宣告函數/方法時有專屬關鍵字func
  • 可以宣告巢狀函數/方法
  • 增加 defer 關鍵字,指定函數結束前必定執行的程式碼
  • 增加 try? 關鍵字,可以忽略例外處理直接得到Optional包裝的函數回傳值
  • 函數/方法同時有參數(parameter)名稱與引數(argument)名稱
  • 沒有int、double、boolean這種基本型態,都是類別與物件
  • 提供無符號數字
  • 數字的運算溢位時會出現Runtime error,除非使用&+&-等允許溢位的運算子
  • 不必完全基於類別設計,程式進入點不必是public class
  • 建立物件不用new關鍵字、除了建構子還有解構子
  • 繼承類別不用extends關鍵字,使用冒號
  • 除了類別還有struct關鍵字設計資料結構、使用括號建立元組(tuple)
  • 除了繼承,還有更方便的擴展extension功能,類似JavaScript的添加prototype
  • 增加subscript,類似JavaScript的yield
  • 支援自訂與覆寫運算子的功能,類似C++的功能
  • 有更方便的getter與setter,還增加willSet與didSet的攔截

快速比較

以下是快速比較的表格,用詞可能會不夠精確,但可以快速了解兩者的差別。

功能JavaSwift
程式進入點public class的main方法不限制
宣告變數直接寫型別,或是使用var使用var
宣告常數final加上型別使用let
宣告方法直接寫回傳型別+名稱+參數使用func
宣告類別使用class使用class
宣告可選型別var opt = new Optional<String>()var opt: String?
判斷Optional可用if (opt.isPresent()) {
var name = opt.get();
}
if let name = opt
Optional直接取值var name = opt.get()let name = opt!
繼承類別使用extends+父類名稱直接使用冒號+父類名稱
介面使用interface使用prototype
for基本用法for (int n = 0; n < count; n++)for let n in 0..<count
至少執行一次的迴圈do {
} while (條件);
repeat {
} while 條件
例外處理try {(會拋出錯誤的函數)
} catch(Exception e) {例外處理}
do { try(會拋出錯誤的函數)
} catch {例外處理}
不處理例外只能轉拋出去let value = try?(會拋出錯誤的函數)
函數結束前的處理try-catch之後寫在finally區塊隨時都可寫在defer關鍵字的區塊
陣列預設值new int[] {5, 5, 5}var l = [5, 5, 5]
不可變ListList.of(5, 5, 5)let l = [5, 5, 5]
不可變MapMap.of("a", 1, "b", 2)let m = ["a": 1, "b": 2]
弱引用使用WeakReference<T>類別使用week varunowned let
套件管理Maven、GradleSPM

比Java更方便的Swift特性

字串樣板

Java的字串可以使用String.format()來設立字串樣板,但用起來麻煩一點,因為樣板與參數是分開的。Swift跟Linux shell或新版JavaScript一樣,可以直接在字串內塞入變數。不同的是Linux shell與JavaScript使用$來表達變數,而Swift使用反斜線配括號,應該是因為反斜線在字串中本來就是用來表達特殊字元,例如\t\n\0\\這些特殊符號,當然\t這些在Swift裡面也是可以使用的。

let name = "Jack"
let say = "Hello, \(name)"
// Hello, Jack

字串比較

在Java內==比較的是兩個變數儲存的數字,如果是int這種基本型態,就是等於比較兩個數字。如果變數儲存的是字串等物件,那依然直接比較兩個數字,這個數字代表的是物件的參考位置。因此 "Jack" == "Jack" 在Java中會返回true,因為兩個Jack是同一個物件;可是 new String("Jack") == "Jack" 就不會返回true了,因為等號左邊的參數運算後會產生一個新的物件,成為兩個看似一樣卻獨立的物件。所以也有人說Java語言永遠是傳值的,但這又是另一個故事了😄。

在Swift中,運算子可以重載(Override),針對不同型別的比較可以有不同表現。運算子==在遇到字串與字串的比較時,會轉換成實際比較兩個字串的實際內容是否一致。想要明確比較是否為同一個物件,可以使用===,但是這個運算子無法用在String上。

let name = "Jack"
let str1 = "Ja" + "ck"
let str2 = String(name)
print(name == str1)  // true
print(name == str2)  // true
print(str1 == str2)  // true

可選變數 Optional<T>

在Java內要宣告一個Optional的變數,需要類似下面這樣的宣告。

// 宣告
var optionName = new Optional<String>();

// 流程控制與取值
if(optionName.isPresent()) {
    var name = optionName.get();
    System.out.println(name);
}

// 取不到值的時候的預設值
var name = optionName.orElse("預設名稱");

但在Swift裡面,Optional成為語言本身的一部分,所以可以有更方便的使用方法。

// 宣告
var optionName: String?

// 流程控制與取值
if let name = optionName {
    print(name)
}

// 取不到值的時候的預設值
let name = optionName ?? "預設名稱"

// 不透過if let,使用驚嘆號直接取值,如果沒有值程式會出錯
print(optionName!)

對應Map的字典(Dictionary)

Java的Map對比Swift的字典,可以儲存由鍵(Key)與值(Value)對應的資料,跟Java一樣必須指定鍵與值的型別,可以交由編譯器自動型別推斷。使用var宣告的字典是可增修內容的,但使用let宣告的字典會不可變更內容,這點與Java的final關鍵字不同,final只是規定變數對應的物件不可以更改,Swift的let同時還約束了物件內的資料也不可更改。在Java必須使用Collections.unmodifiableMap()或是Map.of()才能建立不可變更資料的Map。

// 宣告Map
var ages = new MashMap<String, Integer>();
ages.put("Jack", 18);
ages.put("Rose", 21);
ages.put("Kitty", 17);

// 走訪Map
for(var name: ages.keys()) {
    var age = ages.get(name);
    System.out.printf("%s is %d years old%n", name, age);
}

ages.clear();  // 清空Map
// 以下三種方式都可以建立<String, Int>的字典
// 如果用 let 而不是 var 宣告字典,會成為不可更改的字典,增修資料會拋出錯誤
var dic1: Dictionary<String, Int> = [:]
var dic2: [String: Int] = [:]
var ages = ["Jack": 18, "Rose": 21] // 從初始值自動型別推斷
ages["Kitty"] = 17  // 增加資料,類似Java的陣列操作方式

// 走訪字典印出文字
for (name, age) in ages {
    print("\(name) is \(age) years old")
}

ages = [:]  // 清空字典

對外只能讀取的類別成員(Class Field)

在Java的Class中,如果希望有一個Field對外只能讀取,在類別內才能設定,那麼需要寫一個private setter與一個public getter函數,例如以下這樣。

public class Cat {
    private String name;

    public String getName() {
        return name;
    }
}

// 外部使用 cat.getName() 讀取值

雖然只在類別內使用時private setter這個方法可以不寫,但還是需要寫一個public getter才能公開name給外部。而在Swift的Class中設定private可以只針對setter的部分,讓外部可以讀取name,卻不能設定name,很方便。

class Cat {
    private(set) var name: String
}

// 外部使用 cat.name 讀取值

比POJO好用的結構(struct)

POJO全稱為Plain Old Java Object,一個通常只拿來包裝資料類別,然後有一堆getter與setter。寫起來很麻煩,但透過Eclipse IDE的自動建立函數快速完成,或是透過Lombok直接透過一個 @Data Annotation還可以快更多。但Swift提供struct來包裝資料可以更加方便。

以下範例會使用Swift建立一個Cat結構,然後建立兩個cat實例。可以發現使用struct定義內容,不用有getter、setter與建構子(Constructor),產生實例時可以自由決定要初始化哪些數值,類似Lombok提供的@Builder Annotation。

// 定義一個Cat結構
struct Cat {
    var name: String?
    var age: Int?
}

// 產生兩個Instance
var cat1 = Cat(name: "Kitty")
var cat2 = Cat(name: "Kitty", age: 3)

要注意上面name與age的型態都有加上問號,代表他其實是Optional<String>與Optional<Int>,必須要是Optional才能才初始化的時候保持nil不賦值。因此遇到一定不能為空的Field時,可以不加上問號來確保開發者有賦值。

自動完成Equals方法

延續上面「比POJO好用的結構(struct)」,繼續講struct的比較方法。在Java中兩個物件資料是否一致的比較,需要自行完成boolean equals(Object object)這個方法,不然他就只能比較兩個物件是否為同一個實例。在Eclipse中可以讓IDE自動寫上equals方法,使用Lombok可以更方便的透過@EqualsAndHashCode(包含在@Data內),在Swift中使用原生的Equatable就可以自動完成物件比較。

// 定義一個Cat結構
struct Cat: Equatable { // 注意這行多了Equatable
    var name: String?
    var age: Int?
}

// 產生兩個Instance
var cat1 = Cat(name: "Jack")
var cat2 = Cat(name: "Jack", age: 3)

// 測試兩者是否相等
print(cat1 == cat2) // false

// 修改其中一個數值再確認一次
cat1.age = 3
print(cat1 == cat2) // true

還記得本篇文章上面有提到運算子可以有多種功能嗎?因此在Swift中使用cat1 == cat2就等同在Java中使用cat1.equals(cat2),這點也是直觀易讀。

常見操作的比較

數字資料

Swift的整數分為有符號與無符號整數,指定整數的空間大小不是使用byteshortintlong,而是Int8Int16Int32Int64,以及無負號的UInt8UInt16UInt32UInt64。平常可以直接使用IntUint兩個整數型態資料,根據Swift說明,Int在32位元系統就等同Int32,在64位元系統就等同Int64,會自動選擇效率好又大的那個。在程式內寫數字的時候,可以使用底線來進行數字分位,類似平常寫數字會使用的逗號一樣,這點跟Java一樣。順道一提,在歐洲大部分地方的數字分位是使用小數點,而表達小數使用的是逗號喔!

// 以下全部都是1280
let a = 1280
let b = 1_280
let c = +1280
let d = +1_280
let e = +001_280
let f = +0_0_1_2_8_0 // 雖然編譯會過,但沒人會這樣寫啦XD

宣告變數的方式

在Java內宣告變數是型別 名稱這樣的方式,在Swift中,宣告變數的語句是var 名稱: 型別。而常數的宣告final 型別 名稱在Swift中是let 名稱: 型別

int num;
int[] nums = new int[] {5, 5, 5};
String str = "Hello world";
final String str2 = "不可更改變數";
var name = "Jack";    // Java 11開始支援的自動判斷型別的區域變數宣告方式
var num: Int
var nums = [5, 5, 5]   // 自動判斷為 Int[] 型別
var str = "Hello world"
let str2 = "不可更改變數"
var name = "Jack"      // 自動判斷為 String 型別

宣告函數/方法(Method)

下面以設計一個計算平方值的函數為例,介紹如何宣告Swift的函數。需要注意的是,Swift裡面要指定參數名稱與引數名稱,名稱相同時可以只寫一次,若要跟Java一樣呼叫函數時依照順序自動對應參數,需要在宣告函數的時候將引數名稱設為底線_

// 宣告方式
public double square(double num) {
    return num * num;
}
// 使用方式
square(20);
// 宣告方式 1
func square(num: Double) -> Double {
    return num * num
}
square(num: 20)  // 需要標註 num

// 宣告方式 2
func square(num n: Double) -> Double {
    return n * n
}
square(num: 20)  // 外部一樣使用 num,但函數內部使用 n

// 宣告方式 3
func square(_ num: Double) -> Double {  // 注意底線
    return num * num
}
square(20)       // 不需標註 num,跟Java一樣引數依照順序對應到函數裡

匿名函數/閉包(Closure)

從Java 8開始增加了Lambda,讓開發者實作Functional interface產生的物件,使用起來就像是一個可以單獨保存與傳遞的函數,同時又能綁定實作Functional interface當下的外部資料,讓開發者增加許多的靈活度。在Swift也有匿名函數與閉包的功能。

// 實作提供Sort演算法進行比較的函數
Comparator<Integer> comp = (a, b) -> {
    return a - b;
}

// 簡易寫法
Comparator<Integer> comp = (a, b) -> a - b;

// 匿名使用
Collections.sort(list, (a, b) -> a - b);
// 先不考慮泛形,建立一個比較方法,Swift的sort需要函數回傳布林值
let foo = {
    (a: Int, b: Int) -> Bool in
    return a < b
}
list.sort(by: foo)

// 匿名使用
list.sort(by: {
    (a, b) -> Bool in
    return a < b
})

// 簡化版,省略return
list.sort(by: {
    (a, b) -> Bool in a < b
})

// 更簡化版,省略引數名稱定義,以$0、$1代替
list.sort(by: { $0 < $1 })

// 最簡化版,直接傳入方法
list.sort(by: <)

最後只傳入一個小於符號就可以運作,因為小於在Swift內也等同於一個函數,由兩個參數與與一個Bool回傳值組成,符合sort裡面by需要的函數類型。因此就像是將foo傳給sort一樣,小於也可以直接傳給sort使用。這個在C++與Python應該有類似的用法,但習慣Java與JavaScript的我看到這用法覺得很新鮮😄。