~ / track C / clojure ecosystem

JVM interop from Clojure

Intermediate

Clojure 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

FormMeaning
(Integer/parseInt "42")Static method call
Integer/MAX_VALUEStatic 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:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

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:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

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 a Runnable, Callable, listener, etc.
loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate
  • proxy — like reify but 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 touches Connection, ResultSet, or PreparedStatement.
  • 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, a java.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.

 status: new