Skip to content
AgileLogics
7 min read

Java Interop

Prerequisites: Namespaces, Error handling

Clojure was designed to use the host platform directly. Any Java class — from the JDK or a library — is available with a small, consistent syntax. If you know some Java, you can use all of it from Clojure.

The syntax

(println (.toUpperCase "clojure"))                ; instance method
(println (.substring "clojure" 0 3))              ; with arguments
(println (Math/abs -42) (Math/max 3 7))           ; static methods
(println Integer/MAX_VALUE Math/PI)               ; static fields
(println (str (java.util.ArrayList. [1 2 3])))    ; constructor (note the dot)
(println (str (new java.util.HashSet [1 1 2])))   ; constructor with new
(println (.. "  padded  " trim toUpperCase))      ; chained calls
CLOJURE
clo
42 7
2147483647 3.141592653589793
[1, 2, 3]
[1, 2]
PADDED

println shows most Java objects as #object[ClassName 0x1a2b3c value] (the hex part is an identity hash that changes on every run), so the examples call str, which uses the object's own toString.

Java Clojure
s.toUpperCase() (.toUpperCase s)
Math.abs(x) (Math/abs x)
new ArrayList<>() (ArrayList.) or (new ArrayList)
Integer.MAX_VALUE Integer/MAX_VALUE
a.b().c() (.. a b c) or (-> a .b .c)

Imports and doto

(import '(java.time LocalDate DayOfWeek)
        '(java.time.format DateTimeFormatter)
        '(java.util ArrayList))

(def launch (LocalDate/of 2026 3 15))
(println (str (.plusWeeks launch 6)))
(println (str (.getDayOfWeek launch)) (= DayOfWeek/SUNDAY (.getDayOfWeek launch)))
(println (.format launch (DateTimeFormatter/ofPattern "dd.MM.yyyy")))

(def names (doto (ArrayList.)
             (.add "Ada")
             (.add "Linus")))
(println (str names) (.size names))
2026-04-26
SUNDAY true
15.03.2026
[Ada, Linus] 2

doto calls several methods on the same object and returns the object — handy for Java APIs that mutate.

Methods as functions (Clojure 1.12)

(println (map String/.toUpperCase ["a" "b"]))     ; instance method value
(println (map Long/parseLong ["1" "20"]))         ; static method value
(println (map String/new ["x" "y"]))              ; constructor value
(println (map #(.length ^String %) ["abc" "de"]))
(A B)
(1 20)
(x y)
(3 2)

Since 1.12 you can pass Java methods directly where a function is expected: Class/.method for instance methods, Class/method for static ones, and Class/new for constructors. Before 1.12 you wrapped them in #(...).

Collections cross the boundary

(def java-list (java.util.ArrayList. [3 1 2]))
(println (map inc java-list))                   ; Java collections are seqable
(println (into [] java-list))
(println (instance? java.util.List [1 2]))      ; Clojure vectors ARE Java lists
(println (instance? java.util.Map {:a 1}))
(println (.get {:a 1} :a))
(java.util.Collections/sort java-list)
(println (str java-list))
(4 2 3)
[3 1 2]
true
true
1
[1, 2, 3]

Clojure collections implement Java's read-only collection interfaces, so you can pass them to Java methods that take a List or Map (as long as the method doesn't try to modify them).

Implementing interfaces

(def by-length
  (reify java.util.Comparator
    (compare [_ a b] (compare (count a) (count b)))))

(println (sort by-length ["ccc" "a" "bb"]))

(def task (reify Runnable (run [_] (println "running in" (.getName (Thread/currentThread))))))
(doto (Thread. task "worker-1") (.start) (.join))

(println (.call (fn [] :clojure-fns-are-callables)))
(a bb ccc)
running in worker-1
:clojure-fns-are-callables
  • reify creates an object implementing Java interfaces (or Clojure protocols).
  • Clojure functions already implement Runnable, Callable, and Comparator, so you can often pass a plain fn.

Type hints and reflection

When Clojure can't tell an object's class at compile time, it finds the method by reflection at runtime, which is slow. A type hint like ^String fixes that; (set! *warn-on-reflection* true) makes the compiler warn you. The Performance lesson in Level 3 shows this in detail.