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的攔截
快速比較
以下是快速比較的表格,用詞可能會不夠精確,但可以快速了解兩者的差別。
功能 | Java | Swift |
---|---|---|
程式進入點 | 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] |
不可變List | List.of(5, 5, 5) | let l = [5, 5, 5] |
不可變Map | Map.of("a", 1, "b", 2) | let m = ["a": 1, "b": 2] |
弱引用 | 使用WeakReference<T> 類別 | 使用week var 、unowned let |
套件管理 | Maven、Gradle | SPM |
比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的整數分為有符號與無符號整數,指定整數的空間大小不是使用byte
、short
、int
、long
,而是Int8
、Int16
、Int32
、Int64
,以及無負號的UInt8
、UInt16
、UInt32
、UInt64
。平常可以直接使用Int
與Uint
兩個整數型態資料,根據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的我看到這用法覺得很新鮮😄。