首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《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/lenStr1: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.String1:9 user=> (.getClass 2.3)java.lang.Double1:10 user=> (.getClass [1 2 3])clojure.lang.PersistentVector1:11 user=> (.getClass /'(1 2 3))clojure.lang.PersistentList1: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.LinkedList1:42 user=> (.getClass (.iterator (ArrayList.)))java.util.ArrayList$Itr1: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的差别很大。