Warning! This blog's new home now is here.
Clojure is a language designed to be hosted, this means that it utilizes all the power of the hosting platform without trying to hide or abstract it. This design choice has several consequences:
However, Clojure emphasizes functional programming and immutability, and this has to be taken into account when interoperating with code written in another language that works on lossy mutable state.
Usually there is no problem in using Clojure data structures in Java code since all data types in Clojure implements the relevant Java interfaces like java.util.List
, java.util.Map
, etc.
Let's see the classes and interfaces hierarchies for the main data structures of Clojure:
clojure.lang.PersistentList
, interfaces: clojure.lang.ISeq
, clojure.lang.Sequential
, java.util.List
, java.io.Serializable
, clojure.lang.IHashEq
clojure.lang.ASeq
, interfaces: clojure.lang.IObj
, java.io.Serializable
clojure.lang.Obj
java.lang.Object
clojure.lang.PersistentVector
, interfaces: clojure.lang.IPersistentVector
, java.lang.Iterable
, java.util.List
, java.util.RandomAccess
, java.lang.Comparable
, java.io.Serializable
, clojure.lang.IHashEq
clojure.lang.APersistentVector
, interfaces: clojure.lang.IFn
clojure.lang.AFn
java.lang.Object
clojure.lang.PersistentArrayMap
, interfaces: clojure.lang.IPersistentMap
, java.util.Map
, java.lang.Iterable
, java.io.Serializable
, clojure.lang.MapEquivalence
, clojure.lang.IHashEq
clojure.lang.APersistentMap
, interfaces: clojure.lang.IFn
clojure.lang.AFn
java.lang.Object
clojure.lang.PersistentHashSet
, interfaces: clojure.lang.IPersistentSet
, java.util.Collection
, java.util.Set
, java.io.Serializable
, clojure.lang.IHashEq
clojure.lang.APersistentSet
, interfaces: clojure.lang.IFn
clojure.lang.AFn
java.lang.Object
So you can easily write libraries in Clojure that can be used from Java without problems. Clojure data structures outside the Clojure contex usually behave like regular Java collections, maps and sets.
Usually, the other way around is straightforward too. But there are some corner cases that you have to be aware of when writing libraries that can be used from Java.
Idiomatic Clojure code often works on data structures through the seq(uence) abstraction. In fact a lot of higher-order functions in clojure.core just call seq
on their collections arguments. But there are corner cases, and we'll look at one of them that I discovered recently when writing the assert-json library.
Let's build a regular map:
(def m {:a 1 :b 2})
(map class m) ;= (clojure.lang.MapEntry clojure.lang.MapEntry)
(first (first m)) ;= :a
(second (first m)) ;= 1
Ok, so map
when iterating on a... map, works on a sequence of clojure.lang.MapEntry
. And first
and second
functions can be used on these MapEntry
to extract respectively the key and value of the map entry. Easy.
Now let's try it with a java.util.HashMap
:
(def hm (doto (java.util.HashMap.)
(.put "a" 1)
(.put "b" 2))
(map class hm) ;= (java.util.HashMap$Entry java.util.HashMap$Entry)
(first (first hm)) ; java.lang.IllegalArgumentException: Don't know how to create ISeq from: java.util.HashMap$Entry
; RT.java:494 clojure.lang.RT.seqFrom
; RT.java:475 clojure.lang.RT.seq
; RT.java:567 clojure.lang.RT.first
; core.clj:55 clojure.core/first
What's the problem here? In the stacktrace we see that first
calls seq
on its argument, and seq
on a java.util.HashMap$Entry
fails miserably. Why? Let's read the documentation of this function:
(doc seq) ;-------------------------
;clojure.core/seq
;([coll])
;Returns a seq on the collection. If the collection is
; empty, returns nil. (seq nil) returns nil. seq also works on
; Strings, native Java arrays (of reference types) and any objects
; that implement Iterable.
Well, since seq
uence is a sequential list abstraction, every seq
able data structure must be Iterable
(or either a String or an array of reference types). If you look again in the Using Clojure data from Java section, you'll see that Clojure maps implements the java.lang.Iterable
interface that in this case iterates on clojure.lang.MapEntry
instances. These MapEntry
objects live in this hierarchy:
clojure.lang.MapEntry
, interfaces: clojure.lang.IMapEntry
clojure.lang.AMapEntry
, interfaces: clojure.lang.IPersistentVector
, java.lang.Iterable
, java.util.List
, java.util.RandomAccess
, java.lang.Comparable
, java.io.Serializable
, clojure.lang.IHashEq
clojure.lang.APersistentVector
, interfaces: clojure.lang.IFn
clojure.lang.AFn
java.lang.Object
And so MapEntry
objects are Iterable
too. That's why first
on map entries works.
On the Java side? We already know that HashMap
s are Iterable
on java.util.HashMap$Entry
instances. But these HashMap$Entry
are Iterable
too? Probably not, let's see the class and interfaces hirarchy:
java.util.HashMap$Entry
java.lang.Object
Uhm, no they are not.
The problem here is that to extract key and value from a map entry I used the wrong functions. first
and second
are more appropriate on generic sequences, instead for this specific task there are more suitable (and interoperable) functions: key
and val
.
(def m {:a 1 :b 2})
(key (first m)) ;= :a
(val (first m)) ;= 1
(def hm (doto (java.util.HashMap.)
(.put "a" 1)
(.put "b" 2))
(key (first hm)) ;= "a"
(val (first hm)) ;= 1
Clojure and Java are usually nicely interoperable, but pay attention to use the appropriate functions in Clojure to manipulate data structures when it's possible that your code will be used from Java.