Clojure

Java 交互操作

类访问

Classname
Classname$NestedClassName

表示类名的符号将解析为 Class 实例。内部或嵌套类使用 $ 与其外部类隔开。完全限定的类名始终有效。如果在命名空间中 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

上面给出了访问字段或方法成员的首选惯用法。实例成员形式适用于字段和方法。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)

Dot 特殊形式

(. 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 的基础。可以将其视为成员访问运算符,也可以理解为“在...的范围内”。

如果第一个操作数是解析为类名的符号,则访问被认为是针对命名类的静态成员。请注意,嵌套类的命名方式为 EnclosingClass$NestedClass,符合 JVM 规范。否则,它将被认为是实例成员,第一个参数将被求值以生成目标对象。

对于在 Class 实例上调用实例成员的特殊情况,第一个参数必须是一个表达式,该表达式将求值为类实例 - 请注意,顶部的首选形式将 Classname 展开为 (identity Classname)

如果第二个操作数是一个符号,并且没有提供参数,则它被视为字段访问 - 字段的名称是符号的名称,表达式的值是字段的值,除非存在同名的无参数公共方法,在这种情况下,它将解析为对该方法的调用。如果第二个操作数是一个以 - 开头的符号,则成员符号将仅解析为字段访问(从不解析为 0 元方法),在需要时应优先使用它。

如果第二个操作数是一个列表,或者提供了参数,则它被视为方法调用。列表的第一个元素必须是一个简单的符号,方法的名称是符号的名称。如果有任何参数,它们将从左到右求值,并传递给匹配的方法,然后调用该方法,并返回其值。如果方法的返回值类型为 void,则表达式的值为 nil。请注意,在规范形式中,将方法名放在带参数的列表中是可选的,但在基于该形式构建的宏中,它可能很有用,方便收集参数。

请注意,布尔返回值将转换为布尔值,字符将转换为字符,数值基本类型将转换为数字,除非它们立即被一个接受基本类型的函数消耗。

在本节开头给出的成员访问形式是优先使用的,除了在宏中以外。


(.. 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*)

特殊形式。

如果有任何参数,它们将从左到右求值,并传递给 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)

赋值特殊形式。

当第一个操作数是字段成员访问形式时,赋值将针对相应的字段。如果是实例字段,则会先求值 instance expr,然后再求值 expr。

在所有情况下,都会返回 expr 的值。

注意 - 您无法对函数参数或局部绑定进行赋值。在 Clojure 中,只有 Java 字段、Var、Ref 和 Agent 是可变的


(memfn method-name arg-names*)

宏。展开为创建函数的代码,该函数期望被传递一个对象和任何参数,并对该对象调用命名实例方法,传递参数。当您想将 Java 方法视为一等函数时使用。

(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 对象,并返回一个只读的 map 抽象实现,该实现基于其 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 Map、数组、String 上工作,后两者使用整数键。count 在 Java String、Collection 和数组上工作。nth 在 Java String、List 和数组上工作。seq 在 Java 引用数组、Iterable 和 String 上工作。由于库的大部分内容都是基于这些函数构建的,因此对在 Clojure 算法中使用 Java 对象有很大支持。

实现接口和扩展类

Clojure 支持使用 proxy 宏动态创建实现一个或多个接口和/或扩展类的对象。生成的都是匿名类。您还可以使用 gen-class 生成静态命名的类和 .class 文件。从 Clojure 1.2 开始,reify 也可用于实现接口。

Java 注解可以通过 gen-class 和 Clojure 类型构造上的 元数据 附加到类、构造函数和方法上,请参阅 数据类型参考 以了解示例。


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

class-and-interfaces - 类名的向量
args - 传递给超类构造函数的(可能为空的)向量。
f ⇒ (name [params*] body) 或 (name ([params*] body) ([params+] body) …​)

展开为创建代理类的实例的代码,该代理类通过调用提供的函数来实现命名的类/接口。如果提供,单个类必须排在最前面。如果没有提供,则默认值为 Object。接口名称必须是有效的接口类型。如果未提供方法函数来覆盖类方法,则将调用超类方法。如果未提供方法函数来覆盖接口方法,则在调用它时将抛出 UnsupportedOperationException。方法函数是闭包,可以捕获调用 proxy 时所在的环境。每个方法函数都接受一个额外的隐式第一个参数,该参数绑定到 this。请注意,虽然可以提供方法函数来覆盖受保护的方法,但它们无法访问受保护成员或 super,因为这些功能无法代理。

数组

Clojure 支持创建、读取和修改 Java 数组。建议将数组的使用限制在与需要它们作为参数或将它们用作返回值的 Java 库进行交互。

请注意,许多其他 Clojure 函数都可以使用数组,例如通过 seq 库。这里列出的函数是为了创建数组、或支持数组的变异或更高性能的操作而存在的。

变长参数方法

Java 变长参数方法将尾部变长参数参数视为数组。可以通过将显式数组传递给 vargs 来从 Clojure 中调用它们。

根据 varargs 类型,使用特定类型的数组构造函数来构建基本类型数组,或使用 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 - void 返回值

  • short - 基本 short

  • shorts - short 数组

  • boolean - 基本 boolean

  • booleans - boolean 数组

  • byte - 基本 byte

  • bytes - 字节数组

  • char - 基本字符

  • chars - 字符数组

  • objects - 对象数组

对 Java 基本类型的支持

Clojure 支持在本地上下文中对 Java 基本类型进行高性能操作和算术运算。所有 Java 基本类型都受到支持:int、float、long、double、boolean、char、short 和 byte。

  • let/loop 绑定的局部变量可以是基本类型,其推断的类型可能是其初始化表达式的基本类型。

  • 重新绑定基本类型局部变量的 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

  • 一个动态 var,用于自动将安全操作替换为未经检查的操作: *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 的类型提示(仅限这些)会生成基本类型参数的重载。请注意,此功能仅限于参数个数不超过 4 的函数。

因此,定义为以下形式的函数:

(defn foo ^long [^long n])

既接收也返回基本类型 long 的值(使用装箱参数的调用,实际上任何对象都会导致强制转换并委托给基本类型参数的重载)。

强制转换

有时需要具有特定基本类型的 value。这些强制转换函数会生成指示类型的 value,只要这种强制转换是可能的: bigdec bigint boolean byte char double float int long num short

一些优化提示

  • 所有参数都作为对象传递给 Clojure 函数,因此将任意基本类型提示放在函数参数上毫无意义(除了基本类型数组提示,以及前面提到的 long 和 double)。相反,使用所示的 let 技术将参数放入基本类型局部变量中,如果它们需要参与主体中的基本类型算术运算。

  • (let [foo (int bar)] …​) 是获得基本类型局部变量的正确方法。不要使用 ^Integer 等。

  • 除非需要截断操作,否则不要急于使用未经检查的数学运算。HotSpot 在优化溢出检查方面做得很好,这将产生异常而不是静默截断。在典型的示例中,这在速度上大约有 5% 的差异,非常值得。此外,阅读代码的人不知道是否使用未经检查的运算进行截断或性能提升 - 最好将其保留用于前者,并对后者进行注释。

  • 通常没有必要尝试优化外循环,实际上这样做可能会适得其反,因为您将用基本类型表示事物,而这些事物需要重新装箱才能成为内部调用的参数。唯一的例外是反射警告 - 您必须消除任何频繁调用的代码中的反射警告。

  • 几乎每次有人展示他们试图用提示进行优化的内容时,更快版本都会比原始版本具有更少的提示。如果提示最终没有改善情况 - 请将其删除。

  • 许多人似乎认为只有 unchecked- 操作执行基本类型算术运算 - 并非如此。当参数是基本类型局部变量时,常规 + 和 * 等会使用溢出检查执行基本类型数学运算 - 快速且安全。

  • 因此,进行快速数学运算的最简单方法是让运算符保持原样,并确保源文字和局部变量是基本类型。对基本类型的算术运算会生成基本类型。如果有一个循环(如果需要优化,很可能会有),请确保循环局部变量首先是基本类型 - 然后,如果您不小心生成了装箱的中间结果,您将在 recur 上得到一个错误。不要通过强制转换中间结果来解决该错误,而是找出哪个参数或局部变量不是基本类型。

简单 XML 支持

发行版包含简单的 XML 支持,位于 src/clj/clojure/xml.clj 文件中。此文件中的所有名称都在 clojure.xml 命名空间中。


(parse source)

解析并加载源,源可以是 File、InputStream 或命名 URI 的 String。返回 clojure.xml/element 结构映射的树,该结构映射具有键 :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

The clojure.java.api 包提供了一个最小的接口,用于从其他 JVM 语言引导 Clojure 访问。它是通过提供以下功能来实现的:

  1. 使用 Clojure 的命名空间来定位任意 var,并返回 var 的 clojure.lang.IFn 接口。

  2. 一个方便的 read 方法,用于使用 Clojure 的 edn 读取器读取数据。

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);