~ / track C / clojure ecosystem
JVM interop from Clojure
IntermediateClojure runs on the JVM and treats every Java class, method, and field as
first-class. You don't write FFI bindings; you call (.method object args) directly. This is one of Clojure's superpowers — the whole Maven
universe is available — but it's also where functional principles end and
imperative reality begins. The skill is keeping interop contained at the
boundary so the rest of your code stays functional.
The five interop forms
| Form | Meaning |
|---|---|
(Integer/parseInt "42") | Static method call |
Integer/MAX_VALUE | Static field access |
(.method obj args) | Instance method call |
(.-field obj) | Instance field access |
(StringBuilder. "hi") | Constructor (note the trailing .) |
Real example: a StringBuilder (mutable, fast) wrapped behind a Clojure
function:
The StringBuilder is mutable — inside the function. The outside sees a
pure names → String API. That's the pattern: borrow imperative Java for
the hot path, expose a functional surface to the rest of your code.
Chaining: .. and doto
.. chains method calls / field accesses:
(.. (System/getProperties) (get "user.dir"))
doto invokes a series of methods on the same object — useful when setting
up Java configuration objects:
doto returns the object after applying all the side-effecting calls, so it
composes with let/->.
Type hints: cheap performance wins
When the JVM can't infer the type, it falls back to reflection, which is
slow. A type hint (^Type) tells the compiler the type and emits a
direct call:
(defn upper [^String s] (.toUpperCase s))
Without the ^String hint, (.toUpperCase s) reflects at runtime. With it,
the call compiles to a direct bytecode invoke. Common hints: ^String,
^long, ^double, ^bytes, and array types like ^"[B".
To find missing hints, run with *warn-on-reflection* enabled.
Implementing Java interfaces
Two main tools:
reify— a one-off, anonymous Java object implementing one or more interfaces. Use when you need to pass aRunnable,Callable, listener, etc.
proxy— likereifybut for extending a concrete class (less common; preferred only when reify can't).deftype/defrecord— named types implementing interfaces / protocols (see Protocols and multimethods).
Containing the imperative
A few habits that keep interop from polluting the rest of your code:
- Wrap once. A function like
(query-user db id)that hides the JDBC call. The rest of your code never touchesConnection,ResultSet, orPreparedStatement. - Convert at the boundary. Java collections → Clojure data on the way in
(
(into {} m),(into [] coll)); Clojure → Java on the way out only if required. - Pure core, Java shell. Same principle as Functional core, imperative shell: push interop to the edges.
- Type hint hot paths. Save reflection for non-performance-sensitive code; hint everything else.
When interop is the right answer
- Use the JDK. Concurrency utilities, NIO, security, time, regex, collections — all production-grade, all available with one form.
- Use a Java library. Apache Kafka, Lucene, Jetty, JDBC drivers — direct interop is cheaper than writing a Clojure wrapper.
- Performance-critical mutation. A
StringBuilder, a primitive array, ajava.util.ArrayDeque— all fine inside a function whose external surface is purely functional.
Check yourself
? quiz
You're calling a Java method on a hot path and notice it runs much slower than expected. The first thing to suspect is:
Exercise
Write a Clojure function read-file-lines that reads a UTF-8 text file and
returns a Clojure vector of its lines, using java.nio.file.Files/readAllLines
under the hood. Make the external API purely functional: the function takes a
path string and returns a vector of strings. Use a type hint where appropriate
and verify with *warn-on-reflection* that no reflective calls remain.