- Clojure程序设计
- (美)Stuart Halloway Aaron Bedra
- 795字
- 2020-06-26 14:08:19
2.5 调用Java
Clojure提供了简单、直接的语法用来调用Java代码,包括:创建对象、调用方法、访问静态方法和字段。此外,Clojure提供的语法糖,使得用Clojure调用Java时,甚至比用Java自己来调用还要简洁!
并非所有的Java类型都是生来平等:基本类型与数组就大不一样。对于这些Java的特例,Clojure 同样允许你直接访问它们。最后,Clojure 还提供了一组便利的函数用来处理一些常见任务,不要小看了它们,对于Java而言,处理那些任务可是相当笨拙的。
2.5.1 访问构造函数、方法和字段
在许多同Java交互的场景中,第一步都要创建Java对象。为此,Clojure的new应运而生。
(new classname)
来创建一个Random对象试试看。
(new java.util.Random) -><Random java.util.Random@667cbde6>
REPL在打印这个新的Random实例时,只是简单的调用了它的toString()方法。为了随后能使用这个 Random实例,你还需要把它保存到某个地方。现在,我们先简单的用def把它保存到一个Clojure变量中。
(def rnd (new java.util.Random)) -> #'user/rnd
现在,你可以使用Clojure的句点(.)这个特殊形式来调用rnd的方法。
(. class-or-instance member-symbol & args) (. class-or-instance (member-symbol & args))
例如,下述代码调用了nextInt()方法的无参数版本。
(. rnd nextInt) -> -791474443
Random还有另外一个nextInt(),它接受一个参数。你只要把参数追加至列表中,就能调用这个单参数的版本了。
(. rnd nextInt 10) -> 8
在之前的调用中,句点被用来访问实例方法。但实际上它对各种类成员都有效:无论是字段还是方法,静态的或是的实例。下面你会看到如何使用句点来获得pi的值。
(. Math PI) -> 3.141592653589793
注意,Math没有采用全限定名。因为没有必要那么做,Clojure会自动导入java.lang。但java.util就没有这么幸运了,为了避免到哪儿都要输入冗长的java.util.Random,你可以用import明确的将其导入。
(import [& import-lists])
; import-list => (package-symbol & class-name-symbols)
import 接受数量可变的列表作为参数,每个列表的第一项是要导入的包名称,其余部分则是导入的项的名称。因为执行了下面这个 import,接下来就可以使用非全限定的名称来访问Random、Locale和MessageFormat了。
(import '(java.util Random Locale) '(java.text MessageFormat)) -> java.text.MessageFormat Random -> java.util.Random Locale -> java.util.Locale MessageFormat -> java.text.MessageFormat
至此,你几乎已经掌握了通过Clojure调用Java所需的一切知识。借助它们,你现在可以做到以下几点。
● 导入类名
● 创建实例
● 访问字段
● 调用方法
然而,这种程度的语法还不足以令人感到特别兴奋。充其量,这也就是“采用另外一种方式来打括号的Java”罢了。好戏还在后头,第9章中大有乾坤。
2.5.2 Javadoc
尽管通过Clojure访问Java非常容易,但仅凭记忆来掌握Java的海量细节,无疑是一个巨大的挑战。因此,Clojure提供了一个javadoc函数,它能使你的生活轻松许多。当你在REPL中探索时,尽情体验它为你带来的愉悦吧。
(javadoc java.net.URL) ->
2.6 流程控制
Clojure中用于流程控制的形式数量极少。本节中,你会遇到if、do和loop/recur。人们后来发现,有了它们,几乎就别无所求了。
2.6.1 分支结构与if
Clojure的if会对其第一个参数进行求值。倘若结果逻辑为真,就返回对第二个参数求值的结果。
src/examples/exploring.clj (defn is-small? [number] (if (< number 100) "yes")) (is-small? 50) -> "yes"
如果传给if的第一个参数逻辑为假,is-small?会返回nil。
(is-small? 50000) -> nil
如果你希望定义“else”部分定义一个结果,将其作为if的第三个参数即可。
src/examples/exploring.clj (defn is-small? [number] (if (< number 100) "yes" "no")) (is-small? 50000) -> "no"
流程控制宏when和when-not建立在if的基础之上,参见第7.2.3小节“when与when-not”。
2.6.2 用do引入副作用
Clojure的if,其每个分支只能有一个形式。如果你想在某个分支中多做几件事情,该如何是好?例如,你可能想要记录被选中的究竟是哪条分支。do可以接受任意数量的形式,对这些形式逐个求值,并返回最后一个形式的求值结果。
在if中,你可以使用do来打印一条日志语句。
src/examples/exploring.clj (defn is-small? [number] (if (< number 100) "yes" (do (println "Saw a big number" number) "no")))
结果如下。
(is-small? 200) | Saw a big number 200 -> "no"
这是一个出现了副作用的例子。对计算 is-small?的返回值来说,println 没有任何贡献。相反,它触及了函数外部的世界,并确确实实“做了某些事情”。
为了能对纯函数和副作用进行融合,许多编程语言不惜采用极其古怪的方式。但Clojure不这样。在Clojure中,哪儿有副作用一眼就能看出来。do就是用来申明“注意,接下来会有副作用”的方式之一。因为除了最后一个形式,do会把所有其他形式的返回值都给忽略掉,所以,这也就意味着对于那些被忽略掉的形式来说,必须具有某种副作用才有其存在的意义。
2.6.3 循环与loop/recur
在Clojure中,loop就是流程控制的瑞士军刀。
(loop [bindings *] exprs*)
loop与let的工作方式颇为相似,首先建立绑定bindings,然后对expres求值。不同的地方是,loop设置了一个循环点(recursion point),随后这个循环点将成为特殊形式recur返回目标。
(recur exprs*)
早先由Loop建立的绑定,会被recur重新绑定为新的值,并且控制程序流程返回到loop的顶端。例如,下面的loop/recur会返回一组倒计数。
src/examples/exploring.clj (loop [result [] x 5] (if (zero? x) result (recur (conj result x) (dec x)))) -> [5 4 3 2 1]
首次进入时,loop 把 result 和一个空向量进行了绑定,并把 x 绑定为 5。由于 x不为零,recur随后对名称x和result再次进行了绑定。
● result被绑定至一个新的向量,该向量是通过连接早先的result和x得到的。
● x则被绑定为前一个x的递减结果。
接下来程序流回到了loop的顶端。由于这一次x仍然不为零,循环继续,再次对result加以累积并递减x。最终,x递减为零,if终结了循环并返回result。
去掉 loop,你就能让 recur 递归至函数的起始位置。这就使得编写那种把整个主体都用作隐式循环的函数变得极为容易。
src/examples/exploring.clj (defn countdown [result x] (if (zero? x) result (recur (conj result x) (dec x)))) (countdown [] 5) -> [5 4 3 2 1]
尽管recur结构非常强大。但你用到它的机会并不多,因为许多常用的循环已经作为Clojure序列库的一部分直接提供了。
例如,倒计数也可以用下面的任意一种方式来表示。
(into [] (take 5 (iterate dec 5))) -> [5 4 3 2 1] (into [] (drop-last (reverse (range 6)))) -> [5 4 3 2 1] (vec (reverse (rest (range 6)))) -> [5 4 3 2 1]
现在还不到深究它们的时候,目前只要知道,相比直接使用recur,它们是更加常用的替代方式。第 3.2 节“使用序列库”中,有关于此处用到的这些序列库函数的讨论。另一方面,Clojure无法进行自动尾部调用优化(tail-call optimization,TCO)。然而,对recur的调用则会得到优化。第4章“函数式编程”中定义了何为尾部调用优化,并且对递归和尾部调用优化的细节进行了探索。
至此,你已经领略了相当多的语言特性,但那种能变化的“变量”却始终没有出现。有些事物的确会发生变化,第5章“状态”将会向你展示Clojure如何处理可以改变的“引用”。但大多数传统语言中的变量,既无必要,同时还相当危险。让我们来看看Clojure是如何摆脱它们的。