Clojure程序设计
上QQ阅读APP看书,第一时间看更新

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是如何摆脱它们的。