Clojure

Java 互操作

類別存取

類別名稱
類別名稱$巢狀類別名稱

表示類別名稱的符號會解析為類別實例。內部或巢狀類別會以 $ 與其外部類別分開。完全限定的類別名稱永遠有效。如果類別在命名空間中已 `import`,則可以使用它而無需限定。java.lang 中的所有類別都會自動匯入至每個命名空間。

String
-> java.lang.String
(defn date? [d] (instance? java.util.Date d))
-> #'user/date?
(.getEnclosingClass java.util.Map$Entry)
-> java.util.Map

成員存取

(.instanceMember instance args*)
(.instanceMember Classname args*)
(.-instanceField instance)
(Classname/staticMethod args*)
Classname/staticField

(.toUpperCase "fred")
-> "FRED"
(.getName String)
-> "java.lang.String"
(.-x (java.awt.Point. 1 2))
-> 1
(System/getProperty "java.vm.version")
-> "1.6.0_07-b06-57"
Math/PI
-> 3.141592653589793

上述提供了存取欄位或方法成員的首選慣用語法。instance member 表單適用於欄位和方法。instanceField 表單適用於欄位,而且如果同時存在欄位和同名的 0 參數方法,則必須使用此表單。它們全部會在巨集擴充時間擴充為對點運算子(如下所述)的呼叫。擴充如下:

(.instanceMember instance args*) ==> (. instance instanceMember args*)
(.instanceMember Classname args*) ==>
    (. (identity Classname) instanceMember args*)
(.-instanceField instance) ==> (. instance -instanceField)
(Classname/staticMethod args*) ==> (. Classname staticMethod args*)
Classname/staticField ==> (. Classname staticField)

點特殊表單

(. instance-expr member-symbol)
(. Classname-symbol member-symbol)
(. instance-expr -field-symbol)
(. instance-expr (method-symbol args*))(. instance-expr method-symbol args*)
(. Classname-symbol (method-symbol args*))(. Classname-symbol method-symbol args*)

特殊表單。

'.' 特殊表單是存取 Java 的基礎。它可以視為成員存取運算子,也可以解讀為「在範圍內」。

如果第一個運算元是解析為類別名稱的符號,則存取會被視為對命名類別的靜態成員存取。請注意,根據 JVM 規範,巢狀類別會命名為 EnclosingClass$NestedClass。否則,它會假設是實例成員,且第一個參數會經過評估以產生目標物件。

對於在類別實例上呼叫實例成員的特殊情況,第一個參數必須是評估為類別實例的表達式 - 請注意,頂端的首選表單會將 Classname 擴充為 (identity Classname)

如果第二個運算元是符號,且未提供任何 args,則會將它視為欄位存取 - 欄位名稱是符號的名稱,且表達式的值是欄位的值,除非存在同名的無參數 public 方法,這種情況下它會解析為對方法的呼叫。如果第二個運算元是以 - 開頭的符號,則成員符號只會解析為欄位存取(永遠不會解析為 0 元方法),且當這是意圖時應優先使用它。

如果第二個運算元是清單,或提供了 args,則會將它視為方法呼叫。清單的第一個元素必須是簡單符號,且方法名稱是符號的名稱。args(如果有的話)會從左至右評估,並傳遞給相符的方法,呼叫該方法,並傳回其值。如果方法有 void 回傳類型,則表達式的值會是 nil。請注意,在標準表單中,將方法名稱放在清單中並加上任何 args 是可選的,但對於建立在該表單上的巨集中收集 args 會很有用。

請注意,布林值回傳值會轉換成布林值,字元會變成字元,而數字基本型別會變成數字,除非它們立即被採取基本型別的方法使用。

本節最上方提供的成員存取形式,在巨集以外的所有情況下都優先使用。


(.. instance-expr member+)
(.. Classname-symbol member+)

member ⇒ fieldName-symbol 或 (instanceMethodName-symbol args*)

巨集。擴充成第一個引數的第一個成員的成員存取 (.),接著是結果中的下一個成員,依此類推。例如

(.. System (getProperties) (get "os.name"))

擴充成

(. (. System (getProperties)) (get "os.name"))

但比較容易寫、讀和理解。另請參閱 -> 巨集,其用法類似

(-> (System/getProperties) (.get "os.name"))


(doto instance-expr (instanceMethodName-symbol args*)*)

巨集。評估 instance-expr,然後依序對結果物件呼叫所有方法/函式,並傳入提供的引數,最後回傳該物件。

(doto (new java.util.HashMap) (.put "a" 1) (.put "b" 2))
-> {a=1, b=2}

(Classname. args*)
(new Classname args*)

特殊表單。

如果有 args,會從左到右評估這些 args,並傳遞給 Classname 指定的類別的建構函式。會回傳建構的物件。

替代巨集語法

如所示,除了標準特殊形式 new 之外,Clojure 還支援包含 '.' 的符號的特殊巨集擴充。

(new Classname args*)

可以寫成

(Classname. args*) ;請注意尾端的點

後者會在巨集擴充時擴充成前者。


(instance? Class expr)

評估 expr 並測試它是否是類別的執行個體。回傳 true 或 false


(set! (. instance-expr instanceFieldName-symbol) expr)
(set! (. Classname-symbol staticFieldName-symbol) expr)

指定特殊形式。

當第一個運算元是欄位成員存取形式時,指定會指定到對應的欄位。如果是執行個體欄位,會評估執行個體 expr,然後評估 expr。

在所有情況下,都會回傳 expr 的值。

請注意 - 您無法指定函式參數或區域繫結。在 Clojure 中,只有 Java 欄位、Vars、Refs 和 Agents 是可變的


(memfn method-name arg-names*)

巨集。擴充成建立 fn 的程式碼,該 fn 預期會傳遞一個物件和任何 args,並在物件上呼叫指定的執行個體方法,傳遞 args。當您想要將 Java 方法視為一級 fn 時使用。

(map (memfn charAt i) ["fred" "ethel" "lucy"] [1 2 3])
-> (\r \h \y)

請注意,現在幾乎總是優先直接使用此方法,語法如下

(map #(.charAt %1 %2) ["fred" "ethel" "lucy"] [1 2 3])
-> (\r \h \y)

(bean obj)

取得 Java 物件,並根據其 JavaBean 屬性回傳映射抽象的唯讀實作。

(bean java.awt.Color/black)
-> {:RGB -16777216, :alpha 255, :blue 0, :class java.awt.Color,
    :colorSpace #object[java.awt.color.ICC_ColorSpace 0x5cb42b "java.awt.color.ICC_ColorSpace@5cb42b"],
    :green 0, :red 0, :transparency 1}

Clojure 函式庫函式中對 Java 的支援

Clojure 函式庫函式中的許多函式已定義 Java 類型的物件語意。contains? 和 get 可用於 Java 地圖、陣列、字串,後兩者使用整數鍵。count 可用於 Java 字串、集合和陣列。nth 可用於 Java 字串、清單和陣列。seq 可用於 Java 參考陣列、可迭代物件和字串。由於函式庫的其他大部分都是建構在這些函式上,因此在 Clojure 演算法中使用 Java 物件有很大的支援。

實作介面和擴充類別

Clojure 支援動態建立物件,這些物件實作一個或多個介面和/或使用 proxy 巨集來擴充類別。產生的物件屬於匿名類別。您也可以使用 gen-class 產生靜態命名類別和 .class 檔案。從 Clojure 1.2 開始,reify 也可用於實作介面。

Java 注解可以透過 gen-class 和 Clojure 類型建構上的 metadata 附加到類別、建構函式和方法,請參閱 資料類型參考 以取得範例。


(proxy [class-and-interfaces] [args] fs+)

class-and-interfaces - 類別名稱的向量
args - 傳遞給超類別建構函式的引數向量(可能為空)。
f ⇒ (name [params*] body) 或 (name ([params*] body) ([params+] body) …​)

巨集

擴展為建立代理類別實例的程式碼,透過呼叫提供的 fns 來實作指定的類別/介面。如果只提供一個類別,則必須是第一個。如果未提供,預設為 Object。介面名稱必須是有效的介面類型。如果未提供類別方法的方法 fn,則會呼叫超類別方法。如果未提供介面方法的方法 fn,則在呼叫時會擲出 UnsupportedOperationException。方法 fns 是封閉函式,而且可以擷取呼叫代理的環境。每個方法 fn 都會接收一個額外的隱式第一個引數,該引數會繫結到 this。請注意,雖然方法 fns 可以提供來覆寫受保護的方法,但它們無法存取其他受保護的成員,也無法存取 super,因為這些功能無法代理。

陣列

Clojure 支援建立、讀取和修改 Java 陣列。建議您將陣列的使用限制在與需要它們作為引數或將它們用作回傳值的 Java 函式庫互通上。

請注意,許多其他 Clojure 函式會使用陣列,例如透過 seq 函式庫。這裡列出的函式存在於陣列的初始建立,或支援陣列的變異或效能較高的操作。

可變長引數方法

Java 可變長引數方法將尾隨的可變長引數參數視為陣列。它們可以透過傳遞明確的陣列來取代可變長引數,從 Clojure 呼叫。

根據可變長引數類型,使用特定於類型的陣列建構函式來取得基本類型或 into-array 來建立特定類型的陣列。請參閱 常見問題 以取得範例。

從現有集合建立陣列: aclone amap to-array to-array-2d into-array
多維陣列支援: aget aset to-array-2d make-array
類型特定陣列建構函式: boolean-array byte-array char-array double-array float-array int-array long-array object-array short-array
基本型陣列轉換: booleans bytes chars doubles floats ints longs shorts
變更陣列: aset
處理現有陣列: aget alength amap areduce

類型提示

Clojure 支援使用類型提示,以協助編譯器避免在效能至上的程式碼區域中使用反射。通常,應避免使用類型提示,直到出現已知的效能瓶頸。類型提示是放置在符號或表達式上的 元資料標籤,由編譯器使用。它們可以放置在函式參數、let 繫結名稱、var 名稱(定義時)和表達式上

(defn len [x]
  (.length x))

(defn len2 [^String x]
  (.length x))

user=> (time (reduce + (map len (repeat 1000000 "asdf"))))
"Elapsed time: 3007.198 msecs"
4000000
user=> (time (reduce + (map len2 (repeat 1000000 "asdf"))))
"Elapsed time: 308.045 msecs"
4000000

在識別碼或表達式上放置類型提示後,編譯器會嘗試在編譯時解析對其上方法的任何呼叫。此外,編譯器會追蹤任何回傳值的用途,並推斷其用途的類型,因此只需要極少量的提示即可獲得完全在編譯時解析的呼叫系列。請注意,靜態欄位或靜態方法的回傳值不需要類型提示,因為編譯器始終擁有該類型資訊。

有一個 *warn-on-reflection* 旗標(預設為 false),當編譯器無法解析為直接呼叫時,它會警告您

(set! *warn-on-reflection* true)
-> true

(defn foo [s] (.charAt s 1))
-> Reflection warning, line: 2 - call to charAt can't be resolved.
-> #user/foo

(defn foo [^String s] (.charAt s 1))
-> #user/foo

對於函式回傳值,類型提示可以放置在參數向量之前

(defn hinted-single ^String [])

-> #user/hinted-single

(defn hinted
  (^String [])
  (^Integer [a])
  (^java.util.List [a & args]))

-> #user/hinted

別名

Clojure 為沒有典型 Java 類別名稱表示法的基本型 Java 類型和陣列提供別名。根據 Java 欄位描述子 規格來表示類型。例如,位元組陣列(byte-array [])的類型為 "[B"。

  • int - 基本型 int

  • ints - int 陣列

  • long - 基本型 long

  • longs - long 陣列

  • float - 基本型 float

  • floats - float 陣列

  • double - 基本型 double

  • doubles - double 陣列

  • void - 無回傳值

  • short - 基本型 short

  • shorts - short 陣列

  • boolean - 基本型 boolean

  • booleans - boolean 陣列

  • byte - 基本型 byte

  • bytes - byte 陣列

  • char - 基本型字元

  • chars - 字元陣列

  • objects - 物件陣列

支援 Java 基本型

Clojure 支援在區域環境中高效率地處理和運算 Java 基本型別。支援所有 Java 基本型別:int、float、long、double、boolean、char、short 和 byte。

  • let/loop-bound 區域變數可以是基本型別,具有推論的、可能是其 init-form 的基本型別。

  • 重新繫結基本型區域變數的recur 形式會在不封裝的情況下執行,並針對相同的基本型別進行類型檢查。

  • 算術運算 (+,-,*,/,inc,dec,<,<=,>,>= 等) 會針對語意相同的基本型別進行重載。

  • aget / aset 會針對基本型陣列進行重載

  • aclonealength 針對基本型陣列的函式

  • 基本型陣列的建構函式:float-arrayint-array 等。

  • 基本型陣列的類型提示 - ^ints、^floats 等。

  • 強制轉換運算 intfloat 等會在使用者可以接受基本型別時產生基本型別

  • num 強制轉換函式會封裝基本型別以強制進行一般算術運算

  • 陣列轉型函式 ints longs 等,會產生 int[]、long[] 等。

  • 一組「未檢查」的運算,用於執行效能最佳,但可能有風險的整數 (int/long) 運算:unchecked-multiply unchecked-dec unchecked-inc unchecked-negate unchecked-add unchecked-subtract unchecked-remainder unchecked-divide

  • 一個動態變數用於自動將安全操作與未檢查操作交換:*unchecked-math*

  • amapareduce 巨集用於以函數式(即非破壞性)處理一個或多個陣列,以分別產生一個新陣列或聚合值。

與其撰寫這個 Java

static public float asum(float[] xs){
  float ret = 0;
  for(int i = 0; i < xs.length; i++)
    ret += xs[i];
  return ret;
}

您可以撰寫這個 Clojure

(defn asum [^floats xs]
  (areduce xs i ret (float 0)
    (+ ret (aget xs i))))

而且產生的程式碼速度完全相同(使用 java -server 執行時)。

這方面最好的部分是您在初始編碼中不必執行任何特殊操作。通常這些最佳化是不需要的。如果一段程式碼成為瓶頸,您可以透過微小的裝飾來加速它

(defn foo [n]
  (loop [i 0]
    (if (< i n)
      (recur (inc i))
      i)))

(time (foo 100000))
"Elapsed time: 0.391 msecs"
100000

(defn foo2 [n]
  (let [n (int n)]
    (loop [i (int 0)]
      (if (< i n)
        (recur (inc i))
        i))))

(time (foo2 100000))
"Elapsed time: 0.084 msecs"
100000

函數對基本參數和回傳類型支援有限:longdouble 的類型提示(僅限這些)會產生基本類型超載。請注意,此功能僅限於 arity 不大於 4 的函數。

因此,定義為

(defn foo ^long [^long n])

的函數會取得並回傳基本類型 long 的值(使用方塊化參數的呼叫,而任何物件都會導致強制轉換和委派至基本類型超載)。

強制轉換

有時需要具備特定基本類型的值。只要有可能進行強制轉換,這些強制轉換函數就會產生所指示類型的值:bigdec bigint boolean byte char double float int long num short

一些最佳化提示

  • 所有參數都以物件傳遞給 Clojure fns,因此在 fn 引數中放置任意原始型別提示沒有意義(除了原始陣列型別提示,以及如上所述的 long 和 double)。相反,使用所示的 let 技術將引數置於原始區域變數中,如果它們需要參與主體中的原始算術運算。

  • (let [foo (int bar)] …​) 是取得原始區域變數的正確方式。請勿使用 ^Integer 等。

  • 除非您要進行截斷運算,否則不要急於進行未檢查的數學運算。HotSpot 在最佳化溢位檢查方面做得很好,它會產生例外狀況,而不是靜默截斷。在一個典型的範例中,速度差異約為 5% - 非常值得。此外,閱讀您程式碼的人不知道您是否使用未檢查的截斷或效能 - 最好將其保留給前者,並在後者中加上註解。

  • 通常沒有必要嘗試最佳化外部迴圈,事實上,它可能會傷害您,因為您會將事物表示為原始型別,而這些原始型別必須重新封裝才能成為內部呼叫的引數。唯一的例外是反射警告 - 您必須在任何經常被呼叫的程式碼中清除它們。

  • 幾乎每次有人提出他們嘗試使用提示最佳化的內容時,較快的版本提示都遠少於原始版本。如果提示最終沒有改善情況 - 請將其取出。

  • 許多人似乎假設只有未檢查的 ops 執行原始算術運算 - 事實並非如此。當引數是原始區域變數時,常規 + 和 * 等會執行原始數學運算,並進行溢位檢查 - 快速安全。

  • 因此,執行快速數學運算最簡單的方法是讓運算子保持原樣,並確保來源文字和區域變數為原始型別。對原始型別進行算術運算會產生原始型別。如果您有一個迴圈(如果您需要最佳化,您可能會有一個迴圈),請先確保迴圈區域變數為原始型別 - 然後,如果您意外產生一個封裝的中間結果,您將在遞迴時收到錯誤。不要透過強制轉換中間結果來解決該錯誤,而是找出哪個引數或區域變數不是原始型別。

簡單 XML 支援

隨附發布的內容包括簡單的 XML 支援,可在 src/clj/clojure/xml.clj 檔案中找到。此檔案中的所有名稱都位於 clojure.xml 名稱空間中。


(parse source)

分析並載入 source,它可以是命名 URI 的檔案、InputStream 或字串。傳回 clojure.xml/element struct-map 的樹狀結構,其具有 :tag、:attrs 和 :content 鍵。以及存取函式 tag、attrs 和 content。

(clojure.xml/parse "/Users/rich/dev/clojure/build.xml")
-> {:tag :project, :attrs {:name "clojure", :default "jar"}, :content [{:tag :description, ...

從 Java 呼叫 Clojure

clojure.java.api 套件提供一個最小的介面,可從其他 JVM 語言引導 Clojure 存取。它透過提供以下功能來執行此操作

  1. 使用 Clojure 的名稱空間來尋找任意變數,傳回變數的 clojure.lang.IFn 介面。

  2. 使用 Clojure 的 edn 讀取器讀取資料的便利方法 read

IFn 提供對 Clojure API 的完整存取權。您還可以在將其來源或編譯後的表單新增至類別路徑後,存取以 Clojure 編寫的任何其他函式庫。

Clojure 的公開 Java API 包含下列類別和介面

所有其他 Java 類別都應視為實作細節,應用程式應避免依賴它們。

要查詢並呼叫 Clojure 函式

IFn plus = Clojure.var("clojure.core", "+");
plus.invoke(1, 2);

clojure.core 中的函式會自動載入。其他名稱空間可透過 require 載入

IFn require = Clojure.var("clojure.core", "require");
require.invoke(Clojure.read("clojure.set"));

IFn 可以傳遞給高階函式,例如以下範例將 inc 傳遞給 map

IFn map = Clojure.var("clojure.core", "map");
IFn inc = Clojure.var("clojure.core", "inc");
map.invoke(inc, Clojure.read("[1 2 3]"));

Clojure 中的大部分 IFn 都指的是函式。然而,少數 IFn 指的是非函式資料值。若要存取這些資料值,請使用 deref,而不是呼叫函式

IFn printLength = Clojure.var("clojure.core", "*print-length*");
IFn deref = Clojure.var("clojure.core", "deref");
deref.invoke(printLength);