Clojure: 如何用符合Clojure风格的方式处理java.util.HashMap?

29

我有一个java.util.HashMap对象m(从Java代码中返回的值),我想得到一个新的映射,其中包含一个额外的键值对。

如果m是Clojure映射,我可以使用以下代码:

(assoc m "key" "value")

但是在 HashMap 上尝试这样做会得到以下结果:

java.lang.ClassCastException: java.util.HashMap cannot be cast to clojure.lang.Associative

seq 也没有成功:

(assoc (seq m) "key" "value")

出现java.lang.ClassCastException: clojure.lang.IteratorSeq 无法强制转换为 clojure.lang.Associative

我惟一成功的方法是使用HashMap自带的 put 方法,但它返回 void ,所以我需要显式地返回 m

(do (. m put "key" "value") m)

这不是惯用的Clojure代码,而且我修改了m而不是创建一个新的map。

如何以更符合Clojure风格的方式处理HashMap


2
我认为这是不可能的。Java的HashMap不是持久化数据结构,因此你要么修改它,要么克隆它并修改克隆版本。 - copumpkin
4个回答

35

Clojure可以使Java集合变成序列,所以您可以直接在java.util.HashMap上使用Clojure序列函数。

但是assoc需要一个clojure.lang.Associative类型的参数,因此您需要先将java.util.HashMap转换为该类型:

(assoc (zipmap (.keySet m) (.values m)) "key" "value")

编辑:更简单的解决方案:

(assoc (into {} m) "key" "value")

2
但是这样你就没有一个哈希表了,你只有一个Clojure映射,这似乎违背了他的目标。 - Runevault
1
所以,使用(assoc (into {} m) "key" "value")代替(assoc m "key" "value")。太好了!谢谢。 - foxdonut
3
我认为如果您再次需要一个HashMap,您只需创建一个新的即可。(java.util.HashMap. m) - Eric Normand
我最近在大数据上使用了这种方法,但对于嵌套映射等情况,我发现它非常慢。最终,我只是找到了一个Clojure库,这样我就不必再处理Java映射了。 - sandos

30
如果你要与 Java 代码进行交互,你可能不得不咬紧牙关,按照 Java 的方式使用“.put”。这并不一定是致命的错误;Clojure 给你像“do”和“.”这样的东西,特别是为了让你可以轻松地使用 Java 代码。
“assoc”只适用于 Clojure 数据结构,因为很多工作已经投入到使其非常廉价地创建具有轻微修改的新(不可变)副本中。Java HashMaps 不打算以相同的方式工作。您需要每次进行更改时都将它们克隆,这可能是昂贵的。
如果您真的想摆脱 Java 变异领域(例如,也许您保留这些 HashMap 很长时间,不希望在各个地方进行 Java 调用,或者您需要通过“print”和“read”对它们进行序列化,或者您想以 Clojure STM 方式线程安全地处理它们),则可以很容易地在 Java HashMaps 和 Clojure 哈希映射之间进行转换,因为 Clojure 数据结构实现了正确的 Java 接口,因此它们可以相互通信。
user> (java.util.HashMap. {:foo :bar})
#<HashMap {:foo=:bar}>

user> (into {} (java.util.HashMap. {:foo :bar}))
{:foo :bar}
如果你想要一个类似于do的东西,在你完成对它的操作后返回你正在使用的对象,你可以使用doto。实际上,Java HashMap在该函数的官方文档中被用作示例,这表明如果你谨慎地使用Java对象,那么使用它们并不是世界末日。
clojure.core/doto
([x & forms])
Macro
  Evaluates x then calls all of the methods and functions with the
  value of x supplied at the front of the given arguments.  The forms
  are evaluated in order.  Returns x.

  (doto (new java.util.HashMap) (.put "a" 1) (.put "b" 2))

一些可能的策略:

  1. 如果可以,将您的突变和副作用限制在单个函数中。如果您的函数在给定相同输入时始终返回相同值,则它可以在内部执行任何操作。有时,突变数组或映射是实现算法最有效或最简单的方式。只要不向整个世界“泄漏”副作用,你仍然会享受到函数式编程的好处。

  2. 如果您的对象需要长时间存在,或者需要与其他Clojure代码配合使用,请尽快将它们转换为Clojure数据结构,并在最后一刻将它们转换回Java HashMaps(在将它们反馈给Java时)。


感谢详细的解释。非常有用和有趣。我学到了一个关于在Java对象上调用void方法并在最后将它们返回的更优雅的方法,即使用“doto”而不是“do”。你会认为我应该意识到这一点,因为我正在阅读Stu的书;-) - foxdonut
顺便提一下,在Clojure 1.7(2015年)以及之前的一些版本中,java.util.HashMap现在甚至以Clojure的方式打印出来:(java.util.HashMap. {:a 1 :b 2})在回复中打印为"{:a 1, :b 2}",尽管它仍然是一个java.util.HashMap。例如,assoc无法在其上工作。 - Mars

5
在传统方式下,完全可以使用Java哈希映射。
(do (. m put "key" "value") m) 这种写法不符合Clojure的惯用写法,并且修改了m而不是创建一个新的映射。
你正在修改一个本来就意图被修改的数据结构。Java的哈希映射缺乏结构共享,这使得Clojure的映射可以高效地复制。通常的惯用方法是使用Java互操作函数以通常的Java方式处理Java结构,或者将它们清晰地转换为Clojure结构并以Clojure的函数式方式处理它们。当然,除非这样做会使生活更轻松,结果会产生更好的代码;那么一切都无所谓了。

2

以下是我使用哈希表编写的代码,当时我尝试比较Clojure版本和Java(从Clojure中使用)的内存特征。

(import '(java.util Hashtable))
(defn frequencies2 [coll]
    (let [mydict (new Hashtable)]
      (reduce (fn [counts x]
            (let [y (.toLowerCase x)]
              (if (.get mydict y)
            (.put mydict y (+ (.get mydict y) 1))
            (.put mydict y 1)))) coll) mydict))

这个函数是用来获取一个集合中每个不同元素(例如字符串中的单词)被重复使用的次数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接