100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Clojure and Java Interop

Understand how Clojure calls Java classes and methods directly via dot notation, implements interfaces with proxy/reify/gen-class, and handles exceptions and type hints for performance.

Practical ClojureIntermediate10 min readJul 10, 2026
Analogies

Why Interop Matters

Clojure compiles to JVM bytecode and runs directly on the JVM, so it can call any Java class, constructor, or method without a wrapper library or translation layer. This is a deliberate design choice: rather than reinventing collections, I/O, networking, and date/time handling, Clojure leans on the vast existing Java standard library and ecosystem, reached through a small, consistent dot-notation syntax.

🏏

Cricket analogy: A cricketer who's fluent in both T20 and Test formats can walk onto either pitch without needing a translator; Clojure code runs directly on the JVM and calls Java methods natively, with no wrapper layer needed, the way a bilingual commentator switches between Hindi and English mid-sentence.

Calling Java: Dot Notation and Constructors

Clojure's interop syntax covers three cases with one consistent dot convention. (.method obj args) calls an instance method on an existing object. (Class. args) constructs a new instance — the trailing dot is the constructor call. (Class/staticMethod args) calls a static method or reads a static field, using a slash instead of a leading dot since there's no object instance involved.

🏏

Cricket analogy: Calling a hitSix method on a batsman object mirrors shouting a specific instruction at a specific player, while constructing a fresh StrikeRate record from raw numbers assembles a brand-new player profile, and reading a bowling economy stat off a Bowler class reaches a figure owned by the department, not one player.

clojure
(ns myapp.files
  (:import (java.io File BufferedReader FileReader)
           (java.time LocalDate)))

(defn file-info [path]
  (let [f (File. path)]                 ; constructor call
    {:name   (.getName f)               ; instance method
     :exists (.exists f)
     :size   (.length f)
     :today  (LocalDate/now)}))         ; static method call

(defn read-lines [^String path]         ; type hint avoids reflection
  (with-open [rdr (BufferedReader. (FileReader. path))]
    (try
      (doall (line-seq rdr))
      (catch java.io.IOException e
        (println "Failed to read" path ":" (.getMessage e))
        []))))

Interfaces, Protocols, and proxy/reify/gen-class

reify creates a lightweight, anonymous object that implements one or more interfaces or Clojure protocols inline, ideal for a one-off Comparator or event listener. proxy does the same but can additionally extend a concrete Java class, not just interfaces. gen-class compiles a real, named .class file at build time — the option to reach for when external Java code needs to construct or reference your class by name, such as an application's Main-Class entry point.

🏏

Cricket analogy: reify is like a player stepping in for a single match to fill a specific fielding role without formally joining the franchise, whereas gen-class is like officially drafting a player onto the team roster with a real, named file that other systems outside Clojure can see and call.

reify implements one or more interfaces or protocols inline and returns an anonymous object — ideal for a one-off Comparator or a callback. proxy does the same but can also extend a concrete Java class. gen-class produces a real, named .class file at compile time, which is what you need when external Java code must instantiate your class by name, e.g. as a JAR entry point.

Exception Handling and Type Hints

try/catch/finally works uniformly with Java's exception classes, since Clojure exceptions are just JVM exceptions — there's no separate Clojure exception hierarchy to bridge. Separately, type hints such as ^String or ^File tell the compiler the exact class of a value so interop calls resolve at compile time; without a hint, the JVM falls back to slower runtime reflection, which (set! *warn-on-reflection* true) will flag during development.

🏏

Cricket analogy: A batsman who doesn't watch the bowler's grip closely has to guess the delivery type on the fly, costing precious reaction time — the JVM equivalent is reflection, where an unhinted method call must look up the method at runtime instead of resolving it at compile time, which *warn-on-reflection* flags.

Enable (set! *warn-on-reflection* true) at the top of performance-sensitive namespaces during development. Without type hints, an interop call like (.length s) can't be resolved at compile time, so the JVM falls back to slow reflection on every call — often a 10-100x slowdown in hot loops.

  • Clojure runs directly on the JVM and calls Java methods and constructors without any wrapper library, using dot notation.
  • (.method obj args) calls an instance method; (Class. args) constructs a new instance; (Class/staticMethod args) calls a static method.
  • reify implements interfaces/protocols inline for a one-off object; proxy can additionally extend a concrete class; gen-class compiles a real, externally-referenceable .class file.
  • try/catch/finally in Clojure works with any Java exception class, e.g. (catch java.io.IOException e ...).
  • Type hints (^String, ^File) tell the compiler the exact class so it can resolve methods at compile time instead of via slow runtime reflection.
  • (set! *warn-on-reflection* true) surfaces every unhinted call that falls back to reflection, which is critical to catch in hot code paths.
  • with-open ensures java.io.Closeable resources like readers and streams are closed automatically, similar to Java's try-with-resources.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ClojureStudyNotes#ClojureAndJavaInterop#Clojure#Java#Interop#Matters#StudyNotes#SkillVeris#ExamPrep