Review how Java for, enhanced for, while, and do-while loops express counting, traversal, accumulation, and conditional repetition, then map each intent to idiomatic Clojure forms.
Java gives you loop statements. Clojure gives you expressions that return values. That difference matters more than the syntax.
When you move Java loop code into Clojure, start by asking what the loop is doing:
| Java loop intent | Common Java construct | Clojure first choice | Use loop/recur when |
|---|---|---|---|
| Transform every item | Enhanced for, stream map |
map, mapv, for comprehension |
You need custom stepping state that is awkward as a sequence pipeline |
| Keep selected items | Enhanced for, stream filter |
filter, remove, keep |
The filter depends on several evolving local values |
| Accumulate one result | for plus mutable accumulator |
reduce, transduce, into |
You need early termination or several accumulators |
| Count with an index | Classic for loop |
map-indexed, keep-indexed, range |
You must update index and accumulator together |
| Repeat until a condition changes | while, do-while |
iterate, take-while, some, loop/recur |
The next state depends on the previous state |
| Traverse a tree or nested value | Recursion or explicit stack | Direct recursion, tree-seq, zipper APIs |
The traversal is tail-recursive and local |
The rest of this page translates the Java constructs you already know into the Clojure choice that best preserves intent.
for Loops: Counted WorkA Java for loop combines initialization, condition checking, mutation, and body execution:
1int total = 0;
2for (int i = 1; i <= 5; i++) {
3 total += i;
4}
In Clojure, the first translation is usually not direct recursion. If the loop is reducing a range to one value, use reduce:
1(def total
2 (reduce + (range 1 6)))
If the index is meaningful, make it data:
1(def labels
2 (map-indexed (fn [idx value]
3 {:index idx
4 :value value})
5 ["alpha" "beta" "gamma"]))
That mental move is important for Java engineers: the counter is often not control flow anymore; it is an input to a pure transformation.
for: Collection TraversalAn enhanced Java for loop hides the index but still often mutates an external result:
1List<String> activeNames = new ArrayList<>();
2
3for (User user : users) {
4 if (user.isActive()) {
5 activeNames.add(user.getName());
6 }
7}
The Clojure version should make the transformation visible:
1(def active-names
2 (->> users
3 (filter :active?)
4 (map :name)
5 vec))
The result is explicit. There is no external collection to mutate, and each step can be tested independently.
while: State Until a Condition FailsJava while loops are common when the number of iterations is not known:
1int attempts = 0;
2while (!ready() && attempts < 3) {
3 refresh();
4 attempts++;
5}
In Clojure, use a sequence operation if you are consuming values. Use loop/recur when you are advancing local state:
1(defn wait-until-ready [ready? refresh!]
2 (loop [attempts 0]
3 (cond
4 (ready?) :ready
5 (= attempts 3) :timed-out
6 :else (do
7 (refresh!)
8 (recur (inc attempts))))))
recur is not automatic tail-call optimization. It is an explicit jump back to the nearest loop or function entry, and Clojure only allows it in tail position. That restriction is useful because it makes stack-safe loops visible in the code.
do-while: Execute Once, Then DecideJava’s do-while guarantees that the body runs at least once:
1String token;
2do {
3 token = readToken();
4} while (token.isBlank());
Clojure normally models that as a small recursive loop with the first read inside the loop body:
1(require '[clojure.string :as str])
2
3(defn read-nonblank-token [read-token]
4 (loop []
5 (let [token (read-token)]
6 (if (str/blank? token)
7 (recur)
8 token))))
This is one of the cases where loop/recur is clearer than forcing the code into map or reduce.
Java gives you break and continue. Clojure usually encodes those decisions as data-flow choices:
| Java control habit | Clojure alternative | Example use |
|---|---|---|
continue after a failed predicate |
filter, keep, when inside for |
Skip invalid records |
break after finding a match |
some, first plus filter, reduced in reduce |
Stop at the first qualifying value |
| Loop body builds a collection | mapv, into, for, transducers |
Produce a new vector or map |
| Loop body updates multiple counters | reduce with a map accumulator |
Count valid, invalid, and skipped rows |
Early termination with reduce uses reduced:
1(defn first-large-amount [amounts]
2 (reduce (fn [_ amount]
3 (when (> amount 1000)
4 (reduced amount)))
5 nil
6 amounts))
Use this sparingly. If some expresses the search directly, prefer it.
flowchart TD
A["Java loop"] --> B{"One result?"}
B -->|Yes| C["reduce or transduce"]
B -->|No| D{"New collection?"}
D -->|Yes| E["map, filter, keep, for, into"]
D -->|No| F{"Local evolving state?"}
F -->|Yes| G["loop/recur"]
F -->|No| H{"Nested recursive shape?"}
H -->|Yes| I["direct recursion or tree-seq"]
H -->|No| J["restate the problem as data transformation"]
->> pipeline.reduce.while retry loop with loop/recur.loop/recur, and direct recursion for nested shapes.recur when you need an explicit local loop and can keep the recursive jump in tail position.