Skip to content

Latest commit

 

History

History
369 lines (255 loc) · 12.9 KB

07.md

File metadata and controls

369 lines (255 loc) · 12.9 KB

與 Java 共舞

Clojure 寄生於 Java 之中,汲取它的養分並試圖解放它的繁重。Java 有優秀的即時編譯 (Just-in-time compilation) 功能、垃圾資源回收 (Garbage collection)、HotSpot 虛擬機器與傑出的位元組程式碼。寄宿在其上的語言,不需自行實現便可以擁有強大的武器爲後援。

既然寄宿在 Java 之上,不能只理解 Clojure,而對 Java 視而不見。了解 Java 是認識平台、優秀的生態圈與工具,讓程式邁向卓越的方法。

本篇文章會從 Clojure 方呼叫 Java,以及 Java 方呼叫 Clojure 兩方面,爲各位介紹如何與 Java 共同合作。

從 Clojure 呼叫 Java

載入 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 代表到時建立的執行個體;使用 reifydeftypedefrecord 則需要明確指定 this 參數。

具名類型

reifyproxy 讓我們在動態時期建立匿名類型,然而也會有需要提供靜態具名類型的時候,尤其是 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 的方法中,有可能會遇到參數需要傳遞物件的陣列,或是該方法爲不定引數,此時就需要一些與 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 陣列使用 agetaset 兩個函式:

(def my-array (into-array ["a" "b" "c" "d"]))
(aget my-array 2)
;; => "c"
(aset my-array 2 "C")
;; => "C"

參數分別爲陣列的索引值,與打算設定的新值。

有用的工具函式

doto

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 簡化了許多繁瑣的步驟。

memfn

因爲 Java 類型的方法無法在 Clojure 當作一級函式,所以與 map 等函式配合時,需要多一層匿名函式包裝:

(map #(.length %) ["Happy" "New" "Year"])
;; => (5 3 4)

使用 memfn 可以將執行個體的方法轉換成 Clojure 中的函式,與高階函式搭配使用:

(map (memfn length) ["Happy" "New" "Year"])
;; => (5 3 4)

處理例外

例外是程式運行中發生非預期的問題時,產生出來的類型。根據產生的例外,做適當的處理或善後,是健壯穩定的程式必要的條件。Clojure 使用在 Java 中處理例外的關鍵字 trycatchfinally,作爲例外處理的建構單元:

(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 或是它的子類別。

從 Java 呼叫 Clojure

大多數時間都是從 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 函式與資料的方式。

還不賴吧?今天就先到這裡,下一篇文章再見囉!