Data conversion

Java objects live in the same heap as jolt values, but they're different things: a jolt vector isn't a java.util.List, and a Java String[] isn't a vector. Champaca converts at the call boundary. On the way in, the target is the parameter type of the overload it picked (calling-java). On the way out, only strings and boxes convert, and everything else is left for you to copy.

Going in

jolt valueJava parameterWhat Java gets
integerint, long, float, doublethe number, widened if needed
integer in rangebyte, shortthe number, when no overload fits without narrowing
integerObject, Number, Integera boxed Integer if it fits in 32 bits, else a Long
integerLonga boxed Long
doubledouble, float, Objectthe number, or a boxed Double
stringString, CharSequence, Objecta java.lang.String
boolean, charboolean, char, or a boxthe value, boxed for a reference type
keywordString, Objectits name as a string, namespace included: :ns/kw is "ns/kw"
nilany reference typenull
fna single-method interfacean implementation of it
vectoran array typean array of that element type
vectorList, Collection, Iterablean ArrayList
vectorObjectan array, typed by its contents
mapany type a LinkedHashMap is: Map, HashMap, Objecta LinkedHashMap
setany type a LinkedHashSet is: Set, Collection, Objecta LinkedHashSet
other seqany type an ArrayList is: List, Collection, Objectan ArrayList
Java objectits class or a supertypeitself

Vectors and arrays

A vector given to an array parameter becomes a fresh array of that type, element by element:

(ldk/invoke-static "java.util.Arrays" "toString" [3 1 2])        ;=> "[3, 1, 2]"     toString(int[])
(ldk/invoke-static "java.util.Arrays" "toString" [1.5 2.5])      ;=> "[1.5, 2.5]"    toString(double[])
(ldk/->clj (ldk/invoke-static "java.util.Arrays" "copyOf" [3 1 2] 5))  ;=> [3 1 2 0 0]

A vector of integers also becomes a byte[] or short[] when the method has no overload that takes it without narrowing, as long as every element is in the type's range. That's how (ldk/invoke-static "java.nio.ByteBuffer" "wrap" [1 2 3]), (ldk/construct "java.lang.String" [104 105]) and a MessageDigest's update work.

A Java byte is signed, -128 to 127, so a byte written as 0xCA or 200 fits nothing. Write it as the negative number with the same bits (0xCA is -54, 0xFF is -1), or let Java parse hex with (ldk/invoke (ldk/invoke-static "java.util.HexFormat" "of") "parseHex" "cafebabe"). No value wraps silently, just as Clojure's byte refuses one out of range. ->clj reads a byte[] back as a vector of signed integers: [104 105], or [-54 -2 -70 -66] for that hex.

When the parameter is just Object, nothing says which array type to build, so the contents decide. All small integers make an int[], and one integer beyond 32 bits makes it a long[]. All doubles make a double[], all strings a String[] (keywords count as strings), and vectors of one kind a nested array. Anything else is an Object[]: characters, booleans, nils, an empty vector, or integers mixed with doubles.

Collections

Maps, sets and seqs become the java.util collection that keeps jolt's iteration order. The copy is deep: a nested map becomes a nested LinkedHashMap, and a vector inside a collection becomes an ArrayList (inside a collection there's no parameter type to ask for an array). Keywords become strings, keys included:

(let [m (ldk/construct "java.util.HashMap" {:a 1 "b" [1 2]})]
  [(ldk/to-string m) (ldk/->clj m)])
;=> ["{a=1, b=[1, 2]}" {"a" 1, "b" [1 2]}]

(ldk/invoke-static "java.util.Collections" "max" [3 9 4])   ;=> 9    max(Collection)

That makes the JSON libraries easy to feed. gson and Jackson both take a jolt map directly:

(def mapper (ldk/construct "com.fasterxml.jackson.databind.ObjectMapper"))
(ldk/invoke mapper "writeValueAsString" {:name "champaca" :tags ["a" "b"]})
;=> "{\"name\":\"champaca\",\"tags\":[\"a\",\"b\"]}"

That one needs the Jackson jars on the classpath (jars-and-modules).

Coming out

A result converts only if it's a string or a box: String, Integer, Long, Double, Boolean, Character and the rest come back as jolt values. Arrays, collections and every other object stay Java, so you can keep calling methods on them:

(ldk/invoke (ldk/invoke-static "java.util.Set" "of" "x") "contains" "x")   ;=> true

->clj

ldk/->clj copies Java data into jolt data, all the way down. An array or a List (any Collection that isn't a Set) becomes a vector, a Set a set, and a Map a map, with strings and boxes converted inside. Anything else comes back unchanged.

(ldk/->clj (ldk/invoke "a,b,c" "split" ","))                     ;=> ["a" "b" "c"]
(ldk/->clj (ldk/invoke-static "java.util.Map" "of" "k" 1))       ;=> {"k" 1}

Map keys stay strings unless you pass :key-fn, which ->clj applies to every string key at any depth. It leaves keys of other types alone, so keyword can't turn 1 into nil. If two keys map to the same key, ->clj throws instead of dropping one:

(ldk/->clj (ldk/invoke-static "java.util.Map" "of" "k" 1) {:key-fn keyword})   ;=> {:k 1}

->java

ldk/->java runs the inbound conversion by hand. Use it when you want the Java collection itself, to hold on to or to pass through an Object parameter as a list. Here a top-level vector becomes an ArrayList:

(ldk/to-string (ldk/->java {:name "champaca" :tags #{"tree"} :n [1 2]}))
;=> "{name=champaca, tags=[tree], n=[1, 2]}"

Conversions copy

Every conversion copies. A Java method that changes a collection you passed in changes the copy, and your jolt value stays as it was:

(let [v [1 2 3]
      l (ldk/->java v)]
  (ldk/invoke l "add" 4)
  [v (ldk/->clj l)])
;=> [[1 2 3] [1 2 3 4]]

That's usually what you want from immutable data, but a big collection is copied on every call that takes it. If you pass the same large map to Java many times, convert it once with ->java and pass the Java object instead.