PONY λ M2 Modula-2

Java.CodeCompared.To/Clojure

An interactive executable cheatsheet comparing Java and Clojure

Java 26 Clojure 1.12
Output & Comments
Hello World
class Main { public static void main(String[] args) { System.out.println("Hello, World!"); } }
(println "Hello, World!")
Clojure uses prefix notation: the function name comes first inside parentheses, followed by its arguments. println prints to stdout with a trailing newline — equivalent to System.out.println. There is no class wrapper, no main method, and no semicolons.
print vs println
class Main { public static void main(String[] args) { System.out.println("with newline"); System.out.print("no newline "); System.out.println("continued"); System.out.println(42); System.out.println(true); } }
(println "with newline") (print "no newline ") (println "continued") (println 42) (println true)
Clojure's println appends a newline; print does not. Both accept any value and convert it to a string automatically — there is no need to call String.valueOf() or concatenate with "". pr and prn print in a machine-readable form where strings include their quotes.
String formatting
class Main { public static void main(String[] args) { String message = String.format("Name: %s, score: %d", "Alice", 95); System.out.println(message); System.out.printf("Pi: %.2f%n", Math.PI); } }
(println (format "Name: %s, score: %d" "Alice" 95)) (println (format "Pi: %.2f" Math/PI))
Clojure's format uses Java's printf-style format strings and returns a string — equivalent to String.format. Use (println (format ...)) to print the result. The format specifiers (%s, %d, %.2f) are identical to Java's.
Comments
class Main { public static void main(String[] args) { // Single-line comment /* Multi-line comment */ int total = 42; // inline comment System.out.println(total); } }
;; Standalone comment (double semicolon by convention) ; Single semicolons also work (def total 42) ; inline comment follows code (println total) ; #_ comments out the next form entirely — reader never sees it: (println #_ "this text is ignored" total)
Clojure uses semicolons for comments. By convention, ;; opens standalone comments on their own line, while ; follows code inline. The #_ reader macro comments out the next form entirely — any value, including a nested expression — without the reader processing it at all. There are no block comments equivalent to Java's /* ... */.
Variables & Bindings
Global definitions with def
class Main { static final String GREETING = "Hello"; public static void main(String[] args) { String name = "World"; System.out.println(GREETING + ", " + name + "!"); } }
(def greeting "Hello") (def name "World") (println (str greeting ", " name "!"))
def creates a named var in the current namespace — similar to a static field. Clojure uses kebab-case by convention rather than Java's camelCase or SCREAMING_SNAKE_CASE. There is one naming convention for all definitions — no separate convention for constants vs variables.
Local bindings with let
class Main { public static void main(String[] args) { final double radius = 5.0; final double area = Math.PI * radius * radius; System.out.printf("Area: %.2f%n", area); } }
(let [radius 5.0 area (* Math/PI radius radius)] (println (format "Area: %.2f" area)))
let introduces immutable local bindings scoped to its body. The binding vector evaluates in order, so area can reference radius defined earlier in the same let. Unlike Java's block-scoped variables, let bindings cannot be reassigned — there is no equivalent of area = newValue inside the body.
Immutability by default
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3)); numbers.add(4); // mutates in place System.out.println(numbers); // [1, 2, 3, 4] } }
(let [numbers [1 2 3] more (conj numbers 4)] (println numbers) ; [1 2 3] — unchanged (println more)) ; [1 2 3 4]
conj (short for "conjoin") does not modify numbers — it returns a new vector with the element added. Clojure's persistent data structures use structural sharing, so creating a "modified" copy is efficient even for large collections. This is the central shift from Java: operations on collections always return new values rather than mutating the original.
Everything is an expression
class Main { public static void main(String[] args) { int x = 5; String description = (x > 0) ? "positive" : "non-positive"; System.out.println(description); } }
(let [x 5 description (if (> x 0) "positive" "non-positive")] (println description))
In Clojure, every form is an expression that returns a value — if, let, cond, and defn bodies all return their last evaluated sub-expression. There are no statements that have no value. This is why if directly replaces Java's ternary operator ? : as well as regular if/else blocks.
Naming conventions
class Main { public static void main(String[] args) { int maxRetries = 3; // camelCase final int MAX_TIMEOUT_MS = 5000; // SCREAMING_SNAKE for constants System.out.println(maxRetries); System.out.println(MAX_TIMEOUT_MS); } }
; kebab-case for everything in Clojure — no SCREAMING_SNAKE_CASE (def max-retries 3) (def max-timeout-ms 5000) (println max-retries) (println max-timeout-ms) ; Predicates (returning boolean) end with ?: empty?, even?, string? ; Mutating functions end with !: swap!, reset!, conj!
Clojure uses kebab-case for all names — variables, functions, namespaces, and constants alike. There is no SCREAMING_SNAKE_CASE for constants and no camelCase. Predicates (functions returning a boolean) end with ? (e.g., empty?, even?). Functions with side effects or that mutate state end with ! (e.g., swap!, reset!).
Data Types & Keywords
Numbers and arithmetic
class Main { public static void main(String[] args) { System.out.println(10 / 3); // 3 — integer division System.out.println(10.0 / 3); // 3.3333... System.out.println(10 % 3); // 1 System.out.println((int) Math.pow(2, 10)); // 1024 } }
(println (quot 10 3)) ; 3 — integer division (println (/ 10.0 3)) ; 3.3333... (println (mod 10 3)) ; 1 (println (Math/pow 2 10)) ; 1024.0
Clojure uses named functions for integer division (quot) and modulo (mod). The / operator always returns the most precise result — exact rationals on the JVM, floats in ClojureScript. Math/pow delegates to JavaScript's Math.pow in Scittle, or to java.lang.Math on the JVM.
Keywords — a new concept
class Main { public static void main(String[] args) { // Java: no built-in keyword type — use String constants or enums final String STATUS_ACTIVE = "active"; final String STATUS_PENDING = "pending"; String currentStatus = STATUS_ACTIVE; System.out.println(currentStatus.equals(STATUS_ACTIVE)); // true } }
; Clojure keywords: self-evaluating symbols preceded by : (def current-status :active) (println current-status) ; :active (println (= current-status :active)) ; true (println (keyword? current-status)) ; true (println (name current-status)) ; "active" — the string form
Keywords are one of Clojure's most distinctive types — they have no direct Java equivalent. A keyword like :active is a self-evaluating symbol that is always equal to itself. Unlike strings, keyword equality uses identity comparison, which makes them very fast as map keys. They are the idiomatic choice for map keys, replacing what Java would express with String constants or enum values.
Booleans and nil
class Main { public static void main(String[] args) { System.out.println(true); System.out.println(false); System.out.println((Object) null); System.out.println(null == null); // true System.out.println("hello" instanceof String); // true System.out.println(Integer.valueOf(42) instanceof Number); // true } }
(println true) (println false) (println nil) (println (nil? nil)) ; true (println (boolean? true)) ; true (println (integer? 42)) ; true
Clojure's nil is equivalent to Java's null — it represents the absence of a value. Unlike Java, calling most Clojure functions on nil does not throw a NullPointerException: (count nil) returns 0, (get nil :key) returns nil. The type predicate functions nil?, boolean?, and integer? replace instanceof.
Truthiness rules
class Main { public static void main(String[] args) { // Java requires actual boolean in if conditions // null is not false — it throws NullPointerException if unboxed Integer value = null; System.out.println(value != null ? "has value" : "null"); // null } }
; In Clojure, only nil and false are falsy — everything else is truthy (println (if nil "truthy" "falsy")) ; falsy (println (if false "truthy" "falsy")) ; falsy (println (if 0 "truthy" "falsy")) ; truthy — unlike C or JavaScript (println (if "" "truthy" "falsy")) ; truthy (println (if [] "truthy" "falsy")) ; truthy — even empty collections
Clojure's truthiness rules are simpler than most languages: only nil and false are falsy; everything else is truthy. This differs from C, Python, and JavaScript where 0 and empty collections are falsy. Java is stricter — it requires actual boolean values in if conditions, so the distinction does not arise the same way.
Type checking at runtime
class Main { public static void main(String[] args) { Object value = "Hello"; System.out.println(value instanceof String); // true System.out.println(value instanceof Integer); // false System.out.println(value.getClass().getName()); // java.lang.String } }
(def value "Hello") (println (string? value)) ; true (println (integer? value)) ; false (println (number? 42)) ; true (println (vector? [1 2 3])) ; true (println (map? {:a 1})) ; true (println (fn? println)) ; true
Clojure provides predicate functions for type checking: string?, number?, integer?, vector?, map?, seq?, fn?, keyword?, and many more. These replace instanceof for the common cases. The dynamic type system means there are no type declarations — types are checked at runtime when needed.
Strings
Concatenation with str
class Main { public static void main(String[] args) { String firstName = "Alice"; int score = 95; String message = "Player: " + firstName + ", score: " + score; System.out.println(message); } }
(def first-name "Alice") (def score 95) (def message (str "Player: " first-name ", score: " score)) (println message)
str concatenates any number of arguments, converting non-strings automatically — a single function replaces Java's + operator for strings. Clojure has no string interpolation syntax; use str for simple concatenation or format for formatted output: (format "Player: %s, score: %d" first-name score).
Length, access, and substring
class Main { public static void main(String[] args) { String text = "Hello, World!"; System.out.println(text.length()); // 13 System.out.println(text.charAt(0)); // H System.out.println(text.substring(0, 5)); // Hello System.out.println(text.substring(7)); // World! } }
(def text "Hello, World!") (println (count text)) ; 13 (println (get text 0)) ; H (println (subs text 0 5)) ; Hello (println (subs text 7)) ; World!
count returns the length of strings and all collections. subs extracts a substring by start and optional end index. get retrieves a character by index. Unlike Java's method-on-object style, these are all free functions — composable with map, threading macros, and higher-order functions.
Search and test
class Main { public static void main(String[] args) { String text = "Hello, World!"; System.out.println(text.contains("World")); // true System.out.println(text.startsWith("Hello")); // true System.out.println(text.endsWith("!")); // true System.out.println(text.indexOf("World")); // 7 } }
(def text "Hello, World!") (println (clojure.string/includes? text "World")) ; true (println (clojure.string/starts-with? text "Hello")) ; true (println (clojure.string/ends-with? text "!")) ; true (println (clojure.string/index-of text "World")) ; 7
The clojure.string namespace provides string searching functions as free functions rather than methods. The names follow the predicate convention (includes?, starts-with?) and the functions are composable with threading macros. clojure.string is built into Scittle and does not need an explicit require.
Case conversion and trimming
class Main { public static void main(String[] args) { System.out.println("hello world".toUpperCase()); // HELLO WORLD System.out.println("HELLO".toLowerCase()); // hello System.out.println(" hello ".trim()); // hello System.out.println("hello world".replace("world", "Clojure")); } }
(println (clojure.string/upper-case "hello world")) (println (clojure.string/lower-case "HELLO")) (println (clojure.string/trim " hello ")) (println (clojure.string/replace "hello world" "world" "Clojure"))
The clojure.string functions are all pure — they return new strings without modifying the original. trim, triml (left), and trimr (right) correspond to Java's trim, stripLeading, and stripTrailing. The replace function accepts strings or regex patterns.
Split and join
import java.util.Arrays; class Main { public static void main(String[] args) { String[] words = "one,two,three".split(","); System.out.println(Arrays.toString(words)); // [one, two, three] System.out.println(String.join(", ", words)); // one, two, three } }
(def words (clojure.string/split "one,two,three" #",")) (println words) ; [one two three] (println (clojure.string/join ", " words)) ; one, two, three
clojure.string/split takes a regex pattern written as #"pattern" and returns a vector of strings. clojure.string/join takes a separator and any sequence. These return Clojure's persistent vectors rather than Java arrays — they work naturally with map, filter, and other sequence operations.
Collections
Vectors — the ArrayList equivalent
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5)); System.out.println(numbers.get(0)); // 1 System.out.println(numbers.get(4)); // 5 System.out.println(numbers.size()); // 5 numbers.add(6); System.out.println(numbers); // [1, 2, 3, 4, 5, 6] } }
(def numbers [1 2 3 4 5]) (println (first numbers)) ; 1 (println (last numbers)) ; 5 (println (count numbers)) ; 5 (def more (conj numbers 6)) (println more) ; [1 2 3 4 5 6] (println numbers) ; [1 2 3 4 5] — unchanged
Clojure vectors ([...]) are the most common collection type — ordered, indexed, and persistent. conj returns a new vector with the element appended; the original is unchanged. Vectors are also functions of their index: (numbers 2) retrieves the element at index 2. Use (count v) instead of .size().
Maps — the HashMap equivalent
import java.util.HashMap; import java.util.Map; class Main { public static void main(String[] args) { Map<String, Object> person = new HashMap<>(); person.put("name", "Alice"); person.put("age", 30); System.out.println(person.get("name")); // Alice System.out.println(person.size()); // 2 person.put("email", "alice@example.com"); System.out.println(person.get("email")); // alice@example.com } }
(def person {:name "Alice" :age 30}) (println (:name person)) ; Alice (println (count person)) ; 2 (def updated (assoc person :email "alice@example.com")) (println (:email updated)) ; alice@example.com (println person) ; {:name Alice, :age 30} — unchanged
Clojure maps use {} with key-value pairs separated by spaces or commas. Keywords are the idiomatic key type, and keywords are also functions that look themselves up in a map: (:name person) is equivalent to (get person :name). assoc returns a new map with the key set; dissoc returns a new map with the key removed.
Sets — the HashSet equivalent
import java.util.HashSet; import java.util.Set; class Main { public static void main(String[] args) { Set<Integer> primes = new HashSet<>(Set.of(2, 3, 5, 7, 11)); System.out.println(primes.contains(5)); // true System.out.println(primes.contains(4)); // false System.out.println(primes.size()); // 5 Set<Integer> more = new HashSet<>(primes); more.add(13); System.out.println(more.contains(13)); // true } }
(def primes #{2 3 5 7 11}) (println (contains? primes 5)) ; true (println (contains? primes 4)) ; false (println (count primes)) ; 5 (def more-primes (conj primes 13)) (println (contains? more-primes 13)) ; true (println primes) ; #{2 3 5 7 11} — unchanged
Clojure sets (#{...}) are unordered collections of unique values. contains? tests for membership. Sets are also functions of their elements: (primes 5) returns 5 if present or nil if absent — useful for filtering. conj returns a new set; the original is unmodified. Duplicate literals in a set literal are a compile error.
Nested data structures
import java.util.List; import java.util.Map; class Main { public static void main(String[] args) { Map<String, Object> company = Map.of( "name", "Acme", "employees", List.of( Map.of("name", "Alice", "role", "Engineer"), Map.of("name", "Bob", "role", "Designer") ) ); List<?> employees = (List<?>) company.get("employees"); Map<?, ?> first = (Map<?, ?>) employees.get(0); System.out.println(first.get("name")); // Alice } }
(def company {:name "Acme" :employees [{:name "Alice" :role "Engineer"} {:name "Bob" :role "Designer"}]}) (println (get-in company [:employees 0 :name])) ; Alice (println (:role (first (:employees company)))) ; Engineer
Clojure's nested data structures are built from the same persistent maps, vectors, and sets. get-in navigates a nested structure with a path vector, eliminating the cascading casts required in Java. assoc-in and update-in return new structures with one nested value changed. There is no need for separate DTO classes.
Common collection operations
import java.util.Collections; import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(3, 1, 4, 1, 5, 9, 2, 6); System.out.println(Collections.min(numbers)); // 1 System.out.println(Collections.max(numbers)); // 9 System.out.println(numbers.size()); // 8 System.out.println(numbers.isEmpty()); // false System.out.println(numbers.contains(5)); // true } }
(def numbers [3 1 4 1 5 9 2 6]) (println (apply min numbers)) ; 1 (println (apply max numbers)) ; 9 (println (count numbers)) ; 8 (println (empty? numbers)) ; false (println (boolean (some #{5} numbers))) ; true
apply unpacks a collection as arguments to a function — (apply min numbers) is equivalent to calling (min 3 1 4 ...). empty? tests for an empty collection. some with a set tests membership in any sequence — the set acts as a predicate returning the element if found or nil if not.
Lists — linked-list type
import java.util.LinkedList; class Main { public static void main(String[] args) { LinkedList<Integer> deque = new LinkedList<>(java.util.List.of(2, 3)); deque.addFirst(1); System.out.println(deque.getFirst()); // 1 System.out.println(deque.size()); // 3 } }
(def my-list '(2 3)) ; quoted list — ' prevents evaluation (def extended (cons 1 my-list)) ; prepend 1 (println (first extended)) ; 1 (println (rest extended)) ; (2 3) (println (count extended)) ; 3 ; Prefer vectors [1 2 3] for indexed access; lists for code-as-data
Clojure lists ('(...)) are singly-linked lists — efficient at prepending with cons, but O(n) for indexed access. The quote (') prevents evaluation: without it, (2 3) would try to call 2 as a function. In practice, vectors are preferred for data; lists appear mainly in macros and when representing code as data.
Functions
Defining functions with defn
class Main { static String greet(String name) { return "Hello, " + name + "!"; } public static void main(String[] args) { System.out.println(greet("Alice")); System.out.println(greet("World")); } }
(defn greet [name] (str "Hello, " name "!")) (println (greet "Alice")) (println (greet "World"))
defn defines a named function. The argument vector [name] replaces the Java parameter list. The function body is a sequence of expressions; the last one is implicitly returned — there is no return keyword. The function is defined in the current namespace and called using prefix notation: (greet "Alice").
Multi-arity — replaces method overloading
class Main { static String greet(String name) { return greet(name, "Hello"); } static String greet(String name, String greeting) { return greeting + ", " + name + "!"; } public static void main(String[] args) { System.out.println(greet("Alice")); System.out.println(greet("Alice", "Hi")); } }
(defn greet ([name] (greet name "Hello")) ([name greeting] (str greeting ", " name "!"))) (println (greet "Alice")) (println (greet "Alice" "Hi"))
Clojure handles multiple arities in a single defn using parenthesized groups, one per arity. This replaces Java's method overloading. By convention, lower-arity versions delegate to the highest-arity version with default values. Each arity body is evaluated independently; all share the same function name and namespace binding.
Anonymous functions
import java.util.function.Function; class Main { public static void main(String[] args) { Function<Integer, Integer> doubler = x -> x * 2; System.out.println(doubler.apply(5)); // 10 System.out.println(doubler.apply(21)); // 42 } }
(def doubler (fn [x] (* x 2))) (println (doubler 5)) ; 10 (println (doubler 21)) ; 42 ; Shorthand reader macro — % is the first argument: (def tripler #(* % 3)) (println (tripler 7)) ; 21
fn creates an anonymous function — equivalent to Java 8+ lambda expressions. The shorthand #(* % 2) is a reader macro: % is the first argument, %2 the second. Functions are first-class values in Clojure: stored in variables, passed as arguments, and returned from functions without any functional-interface wrapper.
Higher-order functions
import java.util.function.UnaryOperator; class Main { static <T> T applyTwice(UnaryOperator<T> function, T value) { return function.apply(function.apply(value)); } public static void main(String[] args) { UnaryOperator<Integer> doubler = x -> x * 2; System.out.println(applyTwice(doubler, 3)); // 12 } }
(defn apply-twice [function value] (function (function value))) (def doubler #(* % 2)) (println (apply-twice doubler 3)) ; 12 ; Named functions work as values directly: (println (apply-twice clojure.string/upper-case "hello")) ; HELLOHELLO
In Clojure, any function can be passed as an argument without wrapping it in a functional interface like UnaryOperator<T>. The type system is dynamic, so apply-twice works with any unary function — numeric, string, or otherwise. Named functions like clojure.string/upper-case are values and can be passed directly.
Partial application
import java.util.function.Function; class Main { static Function<Integer, Integer> multiplierOf(int factor) { return x -> x * factor; } public static void main(String[] args) { Function<Integer, Integer> tripler = multiplierOf(3); System.out.println(tripler.apply(5)); // 15 System.out.println(tripler.apply(10)); // 30 } }
(def tripler (partial * 3)) (println (tripler 5)) ; 15 (println (tripler 10)) ; 30 (def add-ten (partial + 10)) (println (add-ten 5)) ; 15
partial returns a new function with one or more arguments pre-filled. (partial * 3) returns a function that multiplies its argument by 3. This replaces the pattern of writing a factory method that returns a lambda. partial works with any function, including built-ins like *, +, str, and conj.
Sequences & Lazy Evaluation
map — transform every element
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3, 4, 5); List<Integer> doubled = numbers.stream() .map(x -> x * 2) .collect(Collectors.toList()); System.out.println(doubled); // [2, 4, 6, 8, 10] } }
(def numbers [1 2 3 4 5]) (def doubled (map #(* % 2) numbers)) (println doubled) ; (2 4 6 8 10) (println (vec doubled)) ; [2 4 6 8 10] — as a vector
Clojure's map is a free function taking a function and a sequence — not a method on a stream. It returns a lazy sequence; nothing is computed until the values are consumed. The function comes before the collection, the opposite of Java's .map(). No .collect() call is needed for most use cases.
filter — keep matching elements
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); List<Integer> evens = numbers.stream() .filter(x -> x % 2 == 0) .collect(Collectors.toList()); System.out.println(evens); // [2, 4, 6] } }
(def numbers [1 2 3 4 5 6]) (println (vec (filter even? numbers))) ; [2 4 6] ; Custom predicate: (println (vec (filter #(> % 3) numbers))) ; [4 5 6]
filter takes a predicate function and a sequence, returning a lazy sequence of elements for which the predicate returns truthy. Named predicates like even?, odd?, nil?, string?, and pos? are available in core and work directly as arguments to filter.
reduce — fold a collection to a value
import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = List.of(1, 2, 3, 4, 5); int total = numbers.stream().reduce(0, (acc, x) -> acc + x); int product = numbers.stream().reduce(1, (acc, x) -> acc * x); System.out.println(total); // 15 System.out.println(product); // 120 } }
(def numbers [1 2 3 4 5]) (println (reduce + 0 numbers)) ; 15 (println (reduce * 1 numbers)) ; 120 ; Operator functions work directly — no lambda wrapper needed: (println (reduce + numbers)) ; 15 — initial value is optional
reduce takes a function, an optional initial value, and a sequence. Arithmetic operators (+, *, max, min) are regular functions and can be passed directly — no lambda wrapper needed. Without an initial value, reduce uses the first element as the starting accumulator.
Lazy sequences
import java.util.stream.IntStream; import java.util.stream.Collectors; class Main { public static void main(String[] args) { // Java streams are lazy — computed element by element var first5 = IntStream.range(0, Integer.MAX_VALUE) .limit(5) .boxed() .collect(Collectors.toList()); System.out.println(first5); // [0, 1, 2, 3, 4] } }
; (range) produces an infinite lazy sequence (println (take 5 (range))) ; (0 1 2 3 4) (println (take 5 (range 10 100))) ; (10 11 12 13 14) ; Laziness makes pipelines over large data sets efficient: (println (take 3 (filter even? (range)))) ; (0 2 4)
Clojure's range returns an infinite lazy sequence — elements are computed only as they are consumed. take forces the first n elements. Chaining filter, map, and take evaluates elements one at a time without building intermediate collections. This is equivalent to Java's stream pipeline but is the default behavior of all sequence operations.
for comprehension
import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<String> pairs = new ArrayList<>(); for (int x = 1; x <= 3; x++) { for (int y = 1; y <= 3; y++) { if (x != y) { pairs.add("(" + x + "," + y + ")"); } } } System.out.println(pairs); } }
(def pairs (for [x (range 1 4) y (range 1 4) :when (not= x y)] (str "(" x "," y ")"))) (println (vec pairs))
Clojure's for is a list comprehension that generates a lazy sequence — not a loop. It binds each variable to each element in turn, producing the Cartesian product by default. The :when modifier filters combinations; :let adds intermediate bindings. This replaces Java's nested for-loops for collection generation.
Control Flow
if expression
class Main { public static void main(String[] args) { int temperature = 25; if (temperature > 30) { System.out.println("hot"); } else { System.out.println("comfortable"); } } }
(let [temperature 25] (if (> temperature 30) (println "hot") (println "comfortable")))
Clojure's if is an expression that returns a value: (if condition then-expr else-expr). The else branch is optional; if omitted and the condition is falsy, if returns nil. For multiple expressions in a branch, use do: (if condition (do expr1 expr2) else-expr).
when — single-branch if
class Main { public static void main(String[] args) { int score = 95; if (score >= 90) { System.out.println("Excellent!"); System.out.println("Grade: A"); } } }
(let [score 95] (when (>= score 90) (println "Excellent!") (println "Grade: A")))
when is equivalent to if with no else branch, but it allows multiple expressions in its body without a do wrapper. All body expressions are evaluated in sequence; the result of the last is returned if the condition is truthy, or nil if falsy. Use when-not for the opposite condition.
cond — multi-branch conditional
class Main { public static void main(String[] args) { int score = 75; String grade; if (score >= 90) grade = "A"; else if (score >= 80) grade = "B"; else if (score >= 70) grade = "C"; else grade = "F"; System.out.println(grade); } }
(let [score 75 grade (cond (>= score 90) "A" (>= score 80) "B" (>= score 70) "C" :else "F")] (println grade))
cond tests conditions in sequence and returns the value paired with the first truthy test. The :else keyword is idiomatic for the default case (any truthy value works, but :else is self-documenting). Because cond returns a value, it can be used directly inside a let binding — no imperative assignment needed.
case — dispatch on a constant value
class Main { public static void main(String[] args) { String day = "Monday"; String dayType = switch (day) { case "Saturday", "Sunday" -> "weekend"; default -> "weekday"; }; System.out.println(dayType); } }
(let [day "Monday" day-type (case day ("Saturday" "Sunday") "weekend" "weekday")] (println day-type))
case dispatches on an exact value using constant-time lookup (not sequential testing like cond). Multiple values matching the same branch are grouped in a list. The final unmatched value is the default — if omitted and no case matches, an exception is thrown. case works with numbers, strings, keywords, and symbols.
dotimes — counted iteration
class Main { public static void main(String[] args) { for (int index = 0; index < 5; index++) { System.out.println("Item " + index); } } }
(dotimes [index 5] (println (str "Item " index)))
dotimes executes its body a fixed number of times, binding the current index (starting at 0) to the given name. It is the direct replacement for Java's counted for loop. dotimes is used for side effects — it returns nil. For building a collection, prefer (mapv ... (range 5)) or a for comprehension.
loop / recur — tail-recursive iteration
class Main { public static void main(String[] args) { int total = 0; for (int index = 1; index <= 10; index++) { total += index; } System.out.println(total); // 55 } }
(loop [index 1 total 0] (if (> index 10) (println total) (recur (inc index) (+ total index))))
loop establishes a recursion point with initial bindings; recur jumps back with new values. This is Clojure's primary iteration mechanism when map, filter, or reduce does not fit. recur uses tail-call optimization — it does not grow the stack, making loop/recur safe for any number of iterations.
Destructuring
Vector destructuring
import java.util.List; class Main { public static void main(String[] args) { List<Integer> coordinates = List.of(10, 20, 30); int x = coordinates.get(0); int y = coordinates.get(1); int z = coordinates.get(2); System.out.println(x + ", " + y + ", " + z); } }
(let [[x y z] [10 20 30]] (println (str x ", " y ", " z))) ; & captures remaining elements: (let [[first-item second-item & remaining] [10 20 30 40 50]] (println first-item) ; 10 (println second-item) ; 20 (println remaining)) ; (30 40 50)
Destructuring binds names to positions in a vector in one step, eliminating the series of .get(0), .get(1) calls needed in Java. The & captures remaining elements as a lazy sequence. Destructuring works anywhere bindings appear: in let, function parameters, for, and loop.
Map destructuring
import java.util.Map; class Main { public static void main(String[] args) { Map<String, Object> person = Map.of("name", "Alice", "age", 30); String name = (String) person.get("name"); int age = (Integer) person.get("age"); System.out.println(name + " is " + age); } }
; :keys shorthand — binding name matches keyword name: (let [{:keys [name age]} {:name "Alice" :age 30}] (println (str name " is " age))) ; Explicit key mapping when names differ: (let [{person-name :name person-age :age} {:name "Alice" :age 30}] (println (str person-name " is " person-age)))
Map destructuring binds names to specific keys. The :keys shorthand is idiomatic when the binding name matches the keyword name. :as whole-map keeps the original map bound alongside the destructured parts. :or provides default values for missing keys: {:keys [name age] :or {age 0}}.
Destructuring in function parameters
import java.util.Map; class Main { static void printPersonInfo(Map<String, Object> person) { String name = (String) person.get("name"); int age = (Integer) person.get("age"); System.out.println(name + " is " + age + " years old"); } public static void main(String[] args) { printPersonInfo(Map.of("name", "Bob", "age", 25)); } }
(defn print-person-info [{:keys [name age]}] (println (str name " is " age " years old"))) (print-person-info {:name "Bob" :age 25})
Destructuring in function parameters eliminates the extraction boilerplate that Java requires inside the method body. The parameter list directly names the keys of interest from the map argument. This pattern is very common for functions that take "options maps" — equivalent to Java's builder pattern or parameter objects, but without the boilerplate class.
Nested destructuring
import java.util.Map; class Main { public static void main(String[] args) { Map<String, Object> data = Map.of( "user", Map.of("name", "Carol", "role", "admin") ); Map<?, ?> user = (Map<?, ?>) data.get("user"); String name = (String) user.get("name"); System.out.println(name); // Carol } }
(let [{user :user} {:user {:name "Carol" :role "admin"}} {:keys [name role]} user] (println name) ; Carol (println role)) ; admin
Nested destructuring unpacks nested structures without cascading casts or repeated .get() calls. Binding user first, then destructuring it in the next binding, is readable and explicit. One or two levels of nesting is idiomatic; deeper nesting is a signal to use get-in instead.
Threading Macros
-> thread-first macro
class Main { public static void main(String[] args) { String result = " hello world " .trim() .toUpperCase() .replace("WORLD", "CLOJURE"); System.out.println(result); } }
(def result (-> " hello world " clojure.string/trim clojure.string/upper-case (clojure.string/replace "WORLD" "CLOJURE"))) (println result)
The -> (thread-first) macro threads a value as the first argument of each successive form, left to right. It transforms deeply nested calls into a readable pipeline that mirrors Java's method-chaining style. When the function takes additional arguments, wrap the step in a list: (clojure.string/replace "WORLD" "CLOJURE").
->> thread-last macro
import java.util.List; import java.util.stream.Collectors; class Main { public static void main(String[] args) { List<Integer> result = List.of(1, 2, 3, 4, 5, 6).stream() .filter(x -> x % 2 == 0) .map(x -> x * x) .collect(Collectors.toList()); System.out.println(result); // [4, 16, 36] } }
(def result (->> [1 2 3 4 5 6] (filter even?) (map #(* % %)))) (println (vec result)) ; [4 16 36]
The ->> (thread-last) macro threads a value as the last argument of each form. This matches Clojure's sequence functions, which always take the collection last. Use ->> for collection pipelines (the equivalent of Java's stream chain) and -> for method-chain-style transformations where the subject is the first argument.
comp — function composition
import java.util.function.Function; class Main { public static void main(String[] args) { Function<String, String> process = ((Function<String, String>) String::trim) .andThen(String::toUpperCase); System.out.println(process.apply(" hello ")); // HELLO } }
(def process (comp clojure.string/upper-case clojure.string/trim)) (println (process " hello ")) ; HELLO (println (process " world ")) ; WORLD
comp creates a composed function that applies functions right-to-left: (comp f g) is equivalent to (fn [x] (f (g x))). This reversal is mathematically conventional but differs from Java's andThen, which applies left-to-right. Read (comp step3 step2 step1) as "apply step1, then step2, then step3."
doseq — iterate for side effects
import java.util.List; class Main { public static void main(String[] args) { List<String> languages = List.of("Java", "Clojure", "Scala"); for (String language : languages) { System.out.println("Hello from " + language); } } }
(def languages ["Java" "Clojure" "Scala"]) (doseq [language languages] (println (str "Hello from " language)))
doseq iterates over a sequence for its side effects, binding each element to the given name. It returns nil. Unlike the for comprehension (which builds a lazy sequence), doseq is the right tool when the goal is side effects — printing, writing to a database, or calling an external API.
Namespaces & Imports
Namespace declaration
// Java: package at top of file, then separate import statements // package com.example.myapp; // import java.util.List; // import java.util.ArrayList; class Main { public static void main(String[] args) { System.out.println("Package: com.example.myapp"); } }
;; Clojure: ns form declares namespace AND all dependencies together ;; (ns com.example.myapp.core ;; (:require [clojure.string :as string] ;; [clojure.set :as set-ops])) ; The current namespace in Scittle: (println *ns*)
Clojure namespaces correspond to Java packages. The ns form at the top of a file declares the namespace and all its dependencies in one place — there are no separate import statements. The :require vector lists dependencies with optional aliases (:as string) or individual names to refer into the namespace (:refer [upper-case]).
Requiring namespaces
import java.util.Collections; import java.util.List; import java.util.ArrayList; class Main { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(List.of(3, 1, 4, 1, 5)); Collections.sort(numbers); System.out.println(numbers); // [1, 1, 3, 4, 5] } }
; clojure.string is built in — use directly with full path: (println (clojure.string/upper-case "hello")) ; Or require with an alias for brevity: (require '[clojure.string :as string]) (println (string/upper-case "hello")) (println (string/lower-case "WORLD"))
require loads a namespace and optionally creates an alias. Using :as string lets you write string/upper-case instead of clojure.string/upper-case. The idiomatic style is to put all :require declarations in the ns form at the top of the file. In-line require calls are used at the REPL.
clojure.set — set operations
import java.util.HashSet; import java.util.Set; class Main { public static void main(String[] args) { Set<Integer> setA = new HashSet<>(Set.of(1, 2, 3, 4)); Set<Integer> setB = new HashSet<>(Set.of(3, 4, 5, 6)); Set<Integer> intersection = new HashSet<>(setA); intersection.retainAll(setB); System.out.println(intersection); // [3, 4] } }
(require '[clojure.set :as set-ops]) (def group-a #{1 2 3 4}) (def group-b #{3 4 5 6}) (println (set-ops/intersection group-a group-b)) ; #{3 4} (println (set-ops/union group-a group-b)) ; #{1 2 3 4 5 6} (println (set-ops/difference group-a group-b)) ; #{1 2}
The clojure.set namespace provides set operations as pure functions returning new persistent sets. intersection, union, and difference replace Java's mutable retainAll, addAll, and removeAll. All results are new sets; the originals are untouched.
:refer — bring names into scope
// Java: static import brings a name into scope without class prefix import static java.util.Collections.sort; import static java.util.Collections.reverse; import java.util.ArrayList; import java.util.List; class Main { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(List.of(3, 1, 4, 1, 5)); sort(numbers); // no Collections. prefix needed System.out.println(numbers); } }
; :refer makes specific names available without namespace prefix: (require '[clojure.string :refer [upper-case lower-case join]]) (println (upper-case "hello")) ; HELLO (println (join ", " ["one" "two" "three"])) ; one, two, three
:refer is the Clojure equivalent of Java's static import. Naming the specific functions to refer (:refer [upper-case lower-case]) is idiomatic. :refer :all (like Java's wildcard import static com.example.*) is discouraged in library code because it makes the origin of names ambiguous when reading the code.
Java Interop
Calling instance methods
class Main { public static void main(String[] args) { String text = "Hello, World!"; System.out.println(text.length()); System.out.println(text.toUpperCase()); System.out.println(text.substring(7, 12)); } }
; JVM Clojure only — Java interop is not available in Scittle (ClojureScript) ; Instance method call: (.methodName object args...) (def text "Hello, World!") (println (.length text)) (println (.toUpperCase text)) (println (.substring text 7 12))
On the JVM, Clojure calls Java instance methods using dot notation: (.methodName object args...). The object comes second, unlike Java where it comes before the dot. This provides full access to any Java library. This example does not run in the browser because Scittle (ClojureScript) does not have access to the JVM class library.
Static methods and fields
class Main { public static void main(String[] args) { System.out.println(Math.PI); System.out.println(Math.sqrt(16.0)); System.out.println(Math.max(10, 20)); System.out.println(System.currentTimeMillis()); } }
; JVM Clojure only — static field: ClassName/fieldName ; static method: (ClassName/methodName args) (println Math/PI) (println (Math/sqrt 16.0)) (println (Math/max 10 20)) (println (System/currentTimeMillis))
Clojure accesses Java static methods and fields using slash notation: Math/PI for a field, (Math/sqrt 16.0) for a method call. Note that Math/PI and (Math/pow x y) work in Scittle as a special case because they delegate to JavaScript's Math object, which Scittle maps to the same syntax. This example is marked norun for clarity about the general rule.
Creating Java objects
import java.util.ArrayList; class Main { public static void main(String[] args) { ArrayList<String> items = new ArrayList<>(); items.add("alpha"); items.add("beta"); System.out.println(items); } }
; JVM Clojure only — constructor call: ClassName. (dot at the end) ; doto calls multiple methods on one object and returns it: (def items (doto (java.util.ArrayList.) (.add "alpha") (.add "beta"))) (println items)
Clojure constructs Java objects using (ClassName.) — a dot at the end of the class name indicates a constructor call. The doto macro calls multiple methods on a single object and returns the object, replacing the Java pattern of naming a variable and calling methods on it line by line. In idiomatic Clojure, Java interop is used only when no pure Clojure alternative exists.
Calling Clojure from Java
// Java can call Clojure functions via the Clojure runtime API: // IFn multiply = Clojure.var("my.namespace", "multiply"); // Object result = multiply.invoke(3, 4); // System.out.println(result); // 12 class Main { public static void main(String[] args) { System.out.println("See notes for Clojure-Java interop details"); } }
; Clojure runs on the JVM and can interoperate with Java in both directions. ; Calling from Clojure → Java: (.method object args) ; Calling from Java → Clojure: via clojure.java.api.Clojure or gen-class ; gen-class compiles Clojure to a Java-compatible .class file: ; (ns my.namespace (:gen-class)) (println "Full Java interop on the JVM — both directions")
Clojure and Java can call each other on the JVM. Clojure code calls Java directly with dot notation. Java code calls Clojure via the clojure.java.api.Clojure runtime API, or by compiling Clojure namespaces to Java classes with :gen-class. This bidirectional interop makes gradual adoption practical — add Clojure to an existing Java project one namespace at a time.
State & Atoms
Atoms — thread-safe mutable references
class Main { static int counter = 0; public static void main(String[] args) { counter++; counter++; counter += 5; System.out.println(counter); // 7 } }
(def counter (atom 0)) (swap! counter inc) ; increment by 1 (swap! counter inc) ; increment by 1 (swap! counter + 5) ; add 5 (println @counter) ; 7 — @ dereferences the atom
Atoms are Clojure's primary tool for managing mutable state. swap! applies a function to the current value and replaces it atomically — without locks. @atom (or deref) reads the current value. Unlike Java's mutable fields, atoms are explicitly identified in the code and their updates are always safe under concurrent access.
Updating atoms with swap!
import java.util.ArrayList; import java.util.List; class Main { static List<String> eventLog = new ArrayList<>(); public static void main(String[] args) { eventLog.add("started"); eventLog.add("processing"); eventLog.add("finished"); System.out.println(eventLog); } }
(def event-log (atom [])) (swap! event-log conj "started") (swap! event-log conj "processing") (swap! event-log conj "finished") (println @event-log) ; [started processing finished]
swap! is the atomic equivalent of reading the current value, applying a function, and writing the result. (swap! event-log conj "started") is equivalent to eventLog.add("started") but is safe under concurrent access — if two threads call swap! simultaneously, both updates are applied without data loss. The function passed to swap! must be pure because it may be retried.
reset! and add-watch
class Main { static volatile int temperature = 20; public static void main(String[] args) { System.out.println("Current: " + temperature); temperature = 25; System.out.println("Updated: " + temperature); } }
(def temperature (atom 20)) (println "Current:" @temperature) ; 20 (reset! temperature 25) ; set to an absolute value (println "Updated:" @temperature) ; 25 ; add-watch fires a callback whenever the atom changes: (add-watch temperature :logger (fn [_key _atom-ref old-value new-value] (println (str "Changed: " old-value " → " new-value)))) (reset! temperature 30) ; triggers the watcher
reset! sets an atom to an absolute value (unlike swap! which applies a function). add-watch registers a callback that fires whenever the atom's value changes, providing a reactive pattern without the complexity of Java's PropertyChangeListener. The watcher receives the key, the atom reference, the old value, and the new value.
Functional approach — pass state, return new state
import java.util.HashMap; import java.util.Map; class Main { public static void main(String[] args) { Map<String, Integer> scores = new HashMap<>(); scores.put("Alice", 0); scores.put("Bob", 0); scores.put("Alice", scores.get("Alice") + 10); scores.put("Bob", scores.get("Bob") + 5); System.out.println(scores); } }
(defn add-score [scoreboard player points] (update scoreboard player (fnil + 0) points)) (let [scoreboard {} after-alice (add-score scoreboard "Alice" 10) final-scores (add-score after-alice "Bob" 5)] (println final-scores)) ; {Alice 10, Bob 5}
The idiomatic Clojure approach often avoids atoms entirely — pass state as a function argument and return the new state. update applies a function to a value in a map and returns the new map. fnil wraps a function so that a nil argument (missing key) is replaced with a default value. This functional style makes state transitions explicit and easy to test in isolation.
Error Handling
try / catch / finally
class Main { public static void main(String[] args) { try { int result = 10 / 0; System.out.println(result); } catch (ArithmeticException error) { System.out.println("Caught: " + error.getMessage()); } finally { System.out.println("Always runs"); } } }
(try (let [result (/ 10 0)] (println result)) (catch :default error (println (str "Caught: " (ex-message error)))) (finally (println "Always runs")))
Clojure's try/catch/finally mirrors Java's structure. In ClojureScript (Scittle), use :default to catch any exception — there are no named exception classes as in the JVM. On the JVM, write (catch ArithmeticException error ...). The finally block runs regardless of whether an exception was thrown.
Throwing exceptions
class Main { static int divide(int numerator, int denominator) { if (denominator == 0) { throw new IllegalArgumentException("Denominator cannot be zero"); } return numerator / denominator; } public static void main(String[] args) { try { System.out.println(divide(10, 2)); System.out.println(divide(10, 0)); } catch (IllegalArgumentException error) { System.out.println(error.getMessage()); } } }
(defn divide [numerator denominator] (when (zero? denominator) (throw (ex-info "Denominator cannot be zero" {:numerator numerator :denominator denominator}))) (/ numerator denominator)) (println (divide 10 2)) (try (divide 10 0) (catch :default error (println (ex-message error))))
ex-info creates an exception with a message and a data map — richer than Java's plain message string. The data map carries structured context (the actual values that caused the error). On the JVM, ex-data retrieves this map from a caught exception. For catching specific Java exception classes on the JVM: (catch IllegalArgumentException error ...).
ex-info — structured error data
class Main { static class ValidationError extends RuntimeException { private final String field; ValidationError(String field, String message) { super(message); this.field = field; } String getField() { return field; } } public static void main(String[] args) { try { throw new ValidationError("email", "Invalid format"); } catch (ValidationError error) { System.out.println(error.getMessage() + " (field: " + error.getField() + ")"); } } }
(try (throw (ex-info "Invalid format" {:field "email" :value "not-an-email"})) (catch :default error (println (ex-message error)) ; On JVM Clojure, retrieve the data map with ex-data: ; (println (:field (ex-data error))) (println "Field info available via ex-data on JVM")))
ex-info replaces Java's custom exception subclasses. Instead of defining a new class with additional fields, attach structured data as a map. On the JVM, (ex-data error) retrieves this map from a caught ExceptionInfo. This approach is more flexible than a class hierarchy — the data map can contain arbitrary context without writing boilerplate classes.
nil-safe operations
class Main { public static void main(String[] args) { String text = null; // text.length() would throw NullPointerException int length = (text != null) ? text.length() : 0; System.out.println(length); // 0 // Java Optional: java.util.Optional<String> opt = java.util.Optional.ofNullable(text); System.out.println(opt.map(String::length).orElse(0)); // 0 } }
; Most Clojure functions are nil-safe by convention: (println (count nil)) ; 0 — not NullPointerException (println (get nil :key)) ; nil (println (first nil)) ; nil ; some-> stops at the first nil step (safe navigation): (def text nil) (println (some-> text clojure.string/upper-case)) ; nil — no error
Most Clojure core functions are nil-safe by convention — count, get, and first on nil return sensible defaults rather than throwing NullPointerException. The some-> macro threads a value like -> but stops and returns nil if any step returns nil — equivalent to Java's optional chaining or Optional.map().