Clojure 寄生於 Java 之中,汲取它的養分並試圖解放它的繁重。Java 有優秀的即時編譯 (Just-in-time compilation) 功能、垃圾資源回收 (Garbage collection)、HotSpot 虛擬機器與傑出的位元組程式碼。寄宿在其上的語言,不需自行實現便可以擁有強大的武器爲後援。
既然寄宿在 Java 之上,不能只理解 Clojure,而對 Java 視而不見。了解 Java 是認識平台、優秀的生態圈與工具,讓程式邁向卓越的方法。
本篇文章會從 Clojure 方呼叫 Java,以及 Java 方呼叫 Clojure 兩方面,爲各位介紹如何與 Java 共同合作。
Clojure 中載入 Java 套件庫的方法在先前的文章也有提到過,就是使用 import
載入 Java 函式庫:
(import java.util.Date)
(Date.)
;; => #inst "2017-12-31T15:51:54.105-00:00"
以上載入 java.util.Date
套件到目前的命名空間後,就可以順利使用 Date 類別。你也可以同時載入同個套件內的不同類別:
(import java.util.Date java.util.Calendar)
;; => java.uti.Calendar
也可以使用下面範例載入同個套件下的類別:
(import '(java.util Date Calendar)
'(java.net URI ServerSocket))
;; => java.net.ServerSocket
Clojure 使用 new
特殊形式 (Special form) 來創建 Java 類別的執行個體 (Instance),第一個參數是類別名稱,接著是該類別建構式的各個參數,返回值爲該類別的執行個體:
(def date (new java.util.Date))
date
;; => #inst "2017-12-31T17:10:35.227-00:00"
Clojure 提供了語法糖 (Syntactic Sugar),用來簡化頻繁輸入 new
,只要在類別名稱之後加上點 (.) 即可:
(String. "Hello")
;; => "Hello"
由於 Clojure 中的字串即是 Java 的字串類別 java.util.String,可以在 java.util.String 類別上使用的方法 (Method),都可以在 Clojure 字串使用,例如將文字改成大寫與小寫:
(. "Hello World" toUpperCase)
;; => “HELLO WORLD”
(. "Hello World" toLowerCase)
;; => “hello world”
使用方式就是在列表中的第一個位置放上點 (.),依序再放上執行個體 (Instance) 以及方法的名稱,如果還有參數則再依序放上方法的參數:
(. "Hello World" indexOf "ello")
;; => 1
Clojure 也爲此提供了語法糖,只要將方法名稱放在列表的第一個位置,並在方法名稱之前加上點 (.),之後列表的位置依序放入執行個體以及方法的參數即可:
(.toUpperCase "Hello World")
;; => “HELLO WORLD”
(.indexOf "Hello World" "ello")
;; => 1
靜態方法 (Static method) 以及靜態欄位 (Static field) 可以使用如上用點的方式呼叫,也可以使用語法糖 Class/Method 的方式:
(. java.lang.Math PI)
;; => 3.141592653589793
java.lang.Math/PI
;; => 3.141592653589793
還有可以簡化層層內嵌運算式的特殊形式,即是在運算式的第一個位置,放上兩個點符號 (.),用來將以下的範例簡化:
(. (. (. " Hello " trim) toUpperCase) length)
;; => 5
以上範例先將字串前後空白去除,再將字串轉成大寫,之後算出字串的長度。最裡頭的運算式求值之後取得新的執行個體,再傳遞給下一個運算式求值,Clojure 則提供了兩個點符號的特殊形式,將內嵌多層的運算式變得平坦,更易寫、易讀和易於理解:
(.. " Hello" trim toUpperCase length)
;; => 5
在前一章提到過的 reify
巨集,能夠用來繼承父類別,產生匿名類別:
(.listFiles (java.io.File. ".")
(reify java.io.FileFilter
(accept [this f]
(.isDirectory f))))
以上範例使用 java.io.File
其中的 listFiles
方法,它接受一個實作 FileFilter
的執行個體,透過此執行個體的 accept
方法來決定要保留哪些檔案。範例中使用 reify
實作了 FileFilter
中的 accept
方法。
除了 reify
之外,proxy
也提供產生匿名類別的功能,但是 reify
只能用來實作協議或是介面,無法用來擴充實際的型態;當你需要覆寫父類別的方法時,只能用 proxy
來達成。
proxy
接受的第一個參數是向量,其中包含了欲實作繼承的類別或介面的名稱,第二個參數爲內含建構式參數的向量,接下來則是覆寫的函式,大略像以下的形式:
(proxy [class-or-interface-name] [constructor-parameters]
(method-name-1 [method-parameters]
method-body)
(method-name-2 [method-parameters]
method-body)
由於 reify
以及 proxy
的函式爲閉包,可以訪問建立此閉包環境中的資訊,因此可以達成類似於執行個體屬性的功能:
(str (let [f "foo"]
(proxy [Object] []
(toString [] f))))
值得注意的是,使用 proxy
建立的匿名類別中的函式,不需要在參數中有明確指定 this
指向該執行個體,proxy
會隱式地建立 this
代表到時建立的執行個體;使用 reify
、deftype
、defrecord
則需要明確指定 this
參數。
reify
與 proxy
讓我們在動態時期建立匿名類型,然而也會有需要提供靜態具名類型的時候,尤其是 Clojure 端打算提供給 Java 端功能的時候。
產生具名類型的方式是透過 gen-class
巨集,它提供了許多修飾子來調整產生出來的類型相關屬性,除了指定類型名稱之外,其他都不是必要提供的。
gen-class
巨集會根據所給予的資訊產生對應的 Java 類別檔案 (.class),以下介紹 gen-class
以及一些修飾子:
(gen-class
:name tw.embracing-clojure.example ;; 類別名稱
:extends com.example.baseclass ;; 父類別名稱
:implements [com.example.IFace] ;; 欲實作的介面
:constructors {[String] [String]} ;; 建構式的返回值型態與參數型態
:init initialize ;; 指定當作建構式的函式
:state state ;; 指定類別中的公有最終屬性
:methods [[doSomething [Byte] String] ;; 類別的方法
[show [] String]]
)
(defn init [a b]
[[(str a b)] {ref {}}])
(defn doSomething [this b]
"do something")
(defn show [this]
"show")
-
:name
定義了欲產生的類別名稱,此處不可缺少。
-
:extends
欲擴充的父類別名稱。
-
:implements
如果有欲實作的介面,則寫於此處的向量之中。
-
:constructors
該類別建構式的返回值與參數的型態寫於此處。寫法爲以一個映射中包含向量爲索引鍵,以及內容值爲向量的每對資料,其中索引鍵向量中寫下返回值的型態,內容向量則寫下各參數的型態。
-
:init
指定用來當作建構式的函式名稱,該函式必須返回一個向量,該向量包含兩個元素,其中一個是傳給父類別建構式各參數值的向量,以及代表狀態的值,該狀態值爲原子 (Atom) 型態。
-
:state
在此處指定的名稱,將會在產生的類別中建立一個同名的執行個體屬性,爲不可變動 (final)。它的值必須要在
init
函式中指定。 -
:methods
此處指名了產生的類別中新增的方法,不需要將欲覆寫的父類別方法寫在這。
除了這裡介紹的修飾子之外,沒有提到的部分有興趣的讀者可以參考官方文件,或在 REPL 輸入 (doc gen-class)
即可取得詳細資訊。
之前的章節中提到過的 ns
巨集,除了可以建立命名空間之外,也具備產生具名類別的功能。其功能與提供的修飾子都跟 gen-class
一樣:
(ns com.example.clojure
(:gen-class
:methods [[show [] void]]))
在 ns
巨集裡的 gen-class
若沒有指定 :name
,則使用該命名空間當作類別名稱。
在 Java 的方法中,有可能會遇到參數需要傳遞物件的陣列,或是該方法爲不定引數,此時就需要一些與 Java 陣列有關的協作函式,可以創建或是修改陣列。
你可以使用 into-array
將序列轉成陣列:
(into-array [\x \y \z])
;; => #object["[Ljava.lang.Character;" 0x1a8aca92 "[Ljava.lang.Character;@1a8aca92"]
也可以使用 make-array
函式,明確指定該陣列的型態與容量:
(make-array Integer/TYPE 10)
;; => #object["[I" 0x44bcf69d "[I@44bcf69d"]
或是可以使用下列明確寫明型態的陣列創建函式:
-
boolean-array
-
byte-array
-
char-array
-
double-array
-
float-array
-
int-array
-
long-array
-
object-array
-
short-array
(long-array 10)
;; => #object["[J" 0x44e25f4d "[J@44e25f4d"]
(char-array 5)
;; => #object["[C" 0x176b9225 "[C@176b9225"]
(float-array 15)
;; => #object["[F" 0x19b993c9 "[F@19b993c9"]
#object["[J" 0x44e25f4d "[J@44e25f4d"]
代表是個原生 (Primitive) 的 long 型態陣列、#object["[C" 0x176b9225 "[C@176b9225"]
則是原生的 char 型態陣列,而 #object["[F" 0x19b993c9 "[F@19b993c9"]
則是原生的 float 型態陣列。
存取 Java 陣列使用 aget
與 aset
兩個函式:
(def my-array (into-array ["a" "b" "c" "d"]))
(aget my-array 2)
;; => "c"
(aset my-array 2 "C")
;; => "C"
參數分別爲陣列的索引值,與打算設定的新值。
doto
巨集將第一個參數傳遞給其後運算式當作第一個參數。它將以下層疊的運算式:
(let [array (java.util.ArrayList.)]
(.add array 11)
(.add array 3)
(.add array 7)
array)
;; => [11 3 7]
轉換成:
(doto (java.util.ArrayList.)
(.add 11)
(.add 3)
(.add 7))
;; => [11 3 7]
使用 doto
簡化了許多繁瑣的步驟。
因爲 Java 類型的方法無法在 Clojure 當作一級函式,所以與 map
等函式配合時,需要多一層匿名函式包裝:
(map #(.length %) ["Happy" "New" "Year"])
;; => (5 3 4)
使用 memfn
可以將執行個體的方法轉換成 Clojure 中的函式,與高階函式搭配使用:
(map (memfn length) ["Happy" "New" "Year"])
;; => (5 3 4)
例外是程式運行中發生非預期的問題時,產生出來的類型。根據產生的例外,做適當的處理或善後,是健壯穩定的程式必要的條件。Clojure 使用在 Java 中處理例外的關鍵字 try
、catch
、finally
,作爲例外處理的建構單元:
(defn divide-by [denom]
(try
(/ 1 denom)
(catch ArithmeticException e
(.printStackTrace e))
(finally
(println "Divided by" denom))))
以上範例在 try
運算式內計算以 1 除以參數,如果發生 ArithmeticException
例外(除以 0),則會印出該例外的除錯堆疊資訊,finally
則是不論 try
運算式是否產生例外,都會進入 finally
運算式內運行。
Clojure 程式發生不可預期的錯誤時,可以使用 throw
來拋出例外:
(throw (IllegalStateException. "Illegal state exeption"))
;; => IllegalStateException Illegal state exeption
值得注意的是,使用 throw
丟出的例外類型,必須是 java.lang.Throwable
或是它的子類別。
大多數時間都是從 Clojure 端呼叫 Java,也會有要從 Java 端呼叫 Clojure 的時候。使用時首先引入 clojure.java.api.Clojure
類別,它提供了找尋 Var 物件、解析程式與存取命名空間的方法。
找到欲存取的 Var 物件後,以其實作的 IFn 介面進行調用,即可呼叫該 Var 物件代表的函式:
import clojure.java.api.Clojure;
import clojure.lang.IFn;
IFn plus = Clojure.var("clojure.core", "+");
plus.invoke(1, 2);
或是引入 Clojure 的命名空間
IFn require = Clojure.var("clojure.core", "require");
require.invoke(Clojure.read("clojure.set"));
或者是引入 Clojure 的高階函式來使用:
IFn map = Clojure.var("clojure.core", "map");
IFn inc = Clojure.var("clojure.core", "inc");
map.invoke(inc, Clojure.read("[1 2 3]"));
從本篇文章中你了解到如何載入 Java 套件與類別,知道了讀取類別成員的方法,還有可以用來建立匿名與具名型別的巨集。還了解到如何將 Clojure 序列轉換成 Java 陣列的方式,以及例外處理的各個建構子。除此之外,還學會了如何由 Java 調用 Clojure 函式與資料的方式。
還不賴吧?今天就先到這裡,下一篇文章再見囉!