讀古今文學網 > Java程序員修煉之道 > 10.5 Clojure與Java的互操作 >

10.5 Clojure與Java的互操作

Clojure從一開始就設計為JVM語言,並且不會對程序員完全隱藏JVM特性。這些特殊的設計在幾個地方都有體現。比如在類型系統層面,Clojure的列表和向量都實現了Java集合類庫中的標準接口List。另外,Clojure使用Java的類庫非常容易,反之亦然。

這意味著Clojure程序員可以使用Java中豐富的類庫和工具,以及JVM的性能和其他特性。這一節會涉及這種互操作性的幾方面內容,特別是:

  • 從Clojure中調用Java;
  • Java如何見到Clojure函數的類型;
  • Clojure代理;
  • 用REPL做探索性編程;
  • 從Java中調用Clojure。

我們先看看從Clojure中如何訪問Java方法,開始它們的集成探索之旅吧。

10.5.1 從Clojure中調用Java

看一下這段在REPL中進行計算的Clojure代碼:

1:16 user=> (defn lenStr [y] (.length (.toString y)))
#\'user/lenStr
1:17 user=> (schwartz [\"bab\" \"aa\" \"dgfwg\" \"droopy\"] lenStr)
(\"aa\" \"bab\" \"dgfwg\" \"droopy\")
1:18 user=>
  

這段代碼用Schwartzian轉換對一個字符串向量排序,排序標準是字符串的長度。其中用到了形式(.toString)(.length),這都是Java方法,它們是在Clojure對像上調用的。符號開始部分的句號.表示運行時應該在下一個參數上調用該名稱的方法,底層是用(.)宏實現的。

所有用(def)或它的變體定義的Clojure值都被放在clojure.lang.Var實例中,它可以承載任何java.lang.Object,所以任何可以在java.lang.Object調用的方法都可以在Clojure值上調用。另外一些跟Java交互的形式是用來調用靜態方法的

(System/getProperty \"java.vm.version\")
  

(此處是調用System.getProperty)和用於訪問靜態公共變量(比如常量)的

Boolean/TRUE
  

在後面兩個例子中已經用到了Clojure命名空間的概念。跟Java包的概念類似,並且常用的Java包都有對應的映射縮寫形式,比如前面那些。

Clojure調用的本質

Clojure中的函數調用實際上是JVM的方法調用。JVM不能保證像類Lisp語言(特別是Scheme)通常做的那樣優化掉尾遞歸。JVM上一些其他的Lisp方言覺得它們需要真正的尾遞歸,因此不準備把Lisp函數調用跟JVM方法調用完全等同起來。而Clojure完全以JVM為平台,甚至不惜違背通常的Lisp實踐。

如果你想創建一個新的Java對像實例並在Clojure中操作它,用(new)形式就可以輕鬆做到。它還有個備選的縮寫形式,在類名之後跟一個句號,可以歸結為(.)宏的另一個用法:

(import \'(java.util.concurrent CountDownLatch LinkedBlockingQueue))
(def cdl (new CountDownLatch 2))
(def lbq (LinkedBlockingQueue.))
  

這裡還用了(import)形式,只用一行就可以導入一個包的很多Java類。

我們在前面提過,Clojure的類型系統有些地方跟Java是一致的,我們來看看其中的細節。

10.5.2 Clojure值的Java類型

從REPL中很容易看到某些Clojure值的Java類型:

1:8 user=> (.getClass \"foo\")
java.lang.String
1:9 user=> (.getClass 2.3)
java.lang.Double
1:10 user=> (.getClass [1 2 3])
clojure.lang.PersistentVector
1:11 user=> (.getClass \'(1 2 3))
clojure.lang.PersistentList
1:12 user=> (.getClass (fn  \"Hello world!\"))
user$eval110$fn__111
  

首先要看到所有Clojure值都是對象,JVM的原始類型默認情況下是不對外的(儘管從性能角度來看有辦法得到原始類型)。如你所料,字符串和數字值直接映射到對應的Java引用類型上去了(java.lang.Stringjava.lang.Double等)。

匿名的\"Hello world!\"函數的名字表明它是一個動態生成類的實例。這個類會實現clojure.lang.IFn接口,Clojure用該接口表明這個值是個函數,你可以把它當做java.util.concurrent裡的Callable接口。

序列會實現clojure.lang.ISeq接口。它們通常是抽像類ASeq或懶實現LazySeq的具體子類。

我們已經看過幾種值的類型了,但這些值是怎麼保存的呢?就像我們在本章一開始提到的,(def)把符號綁到一個值上,這樣會創建一個var。這些varclojure.lang.Var類型(它所實現的接口中也有IFn)的對象。

10.5.3 使用Clojure代理

Clojure有一個強大的宏(proxy),你可以用它創建擴展Java類(或實現接口)的Clojure對象。比如代碼清單10-7重新實現了之前的一個例子(代碼清單4-13),由於Clojure語法更加緊湊,所以這個例子的核心代碼只有一點。

代碼清單10-7 重溫調度執行者

(import \'(java.util.concurrent Executors LinkedBlockingQueue TimeUnit))
(def stpe (Executors/newScheduledThreadPool 2)) ; //STPE工廠方法
(def lbq (LinkedBlockingQueue.))

(def msgRdr (proxy [Runnable]   ; //定義匿名的Runnable實現
  (run  (.toString (.poll lbq)))
))

(def rdrHndl(.scheduleAtFixedRate stpe msgRdr 10 10 TimeUnit/MILLISECONDS)) 
  

(proxy)的一般形式是:

(proxy [<超類/接口>] [<args>] <命名函數的實現>+)
  

第一個向量參數是這個代理類應該實現的接口。如果這個代理還要擴展Java類(如果可以的話,當然,只能擴展一個Java類),這個類名必須是向量中的第一個元素。

第二個向量參數包含傳給超類構造方法的參數。這個向量經常是空的,並且如果(proxy)形式只是實現Java接口的話,那它肯定是空的。

這兩個參數之後是一個或多個表示單個方法實現的形式,按接口的要求或超類指定的實現。

(proxy)形式可以做出任何Java接口的簡單實現。這促成了一種吸引人的可能性:用Clojure REPL作為實驗Java和JVM代碼的擴展遊戲床。

10.5.4 用REPL做探索式編程

探索式編程的核心思想是減少要編寫的代碼量,因為Clojure的語法和REPL提供的實時互動環境,REPL不僅是探索Clojure編程的理想環境,也是學習Java類庫的極佳選擇。

我們來看一下Java列表實現。它們都有返回Iterator類型對象的iterator方法。但Iterator是個接口,所以你可能對真正的實現類型感到好奇。用REPL很容易找出答案:

1:41 user=> (import \'(java.util ArrayList LinkedList))
java.util.LinkedList
1:42 user=> (.getClass (.iterator (ArrayList.)))
java.util.ArrayList$Itr
1:43 user=> (.getClass (.iterator (LinkedList.)))
java.util.LinkedList$ListItr
  

(import)形式從java.util包中導入了兩個類。然後在REPL內用Java的getClass方法。可以看到迭代器實際上是內部類提供的。也許你不應該對此感到吃驚,因為我們在10.4節討論過,迭代器和它們的集合綁定很緊密,所以它們也許需要瞭解這些集合的內部實現細節。

在前面這個例子中值得注意的是,我們一個Clojure結構也沒用,只用了一點語法。我們操作的所有東西實際上都是Java結構。儘管如此,我們還是假設你想用不同的方式,在Java程序裡用Clojure。下一節將會向你展示如何實現這一目的。

10.5.5 在Java中使用Clojure

Clojure的類型系統跟Java高度一致。Clojure數據結構全是真正的Java集合,都實現了對應接口的所有必需部分。因為接口的可選部分一般都跟修改數據結構有關,而Clojure數據結構不可變,所以一般都沒實現。

類型系統的一致性使得在Java程序裡使用Clojure數據結構成為可能。Clojure自身的性質加強了這種可行性——它是採用調用機制的JVM編譯型語言。這最大限度地減少了運行時的問題,意味著從Clojure中得到的類幾乎跟其他任何Java類一樣。解釋型語言跟Java的互操作會更加困難,並且通常需要最基本的非Java語言運行時支持。

下面這個例子展示了Clojure的seq結構如何用在一個普通的Java字符串中。要運行這段代碼,需要把clojure.jar放在classpath上:

ISeq seq = StringSeq.create(\"foobar\");
while (seq != null) {
    Object first = seq.first;
    System.out.println(\"Seq: \"+ seq +\" ; first: \"+ first);
    seq = seq.next;
}
  

上面的代碼使用了StringSeq類中的工廠方法create。它給出了字符串中字符序列的seq視圖。firstnext方法返回新值,而不是修改已有的seq,就跟我們在10.4節討論的一樣。

截止目前我們只是在處理單線程的Clojure代碼。下一節我們要談論Clojure中的並發。特別是Clojure對狀態和可變性的處理方式,這使得它的並發模型跟Java的差別很大。