Browse Clojure Foundations for Java Developers

Transforming Collections

Move from mutable loops to value-oriented collection pipelines with `map`, `filter`, `reduce`, and threading macros.

One of the first places immutability becomes practical rather than philosophical is collection work.

Java often teaches you to transform collections by:

  • creating an accumulator
  • looping
  • mutating the accumulator step by step

Clojure pushes you toward a different model:

  • keep the original collection unchanged
  • express each transformation explicitly
  • return the final value

That usually produces code that is easier to read, test, and adapt at the REPL.

Start With A Familiar Java Shape

Suppose you need the subtotal of billable order items.

A common Java style might look like:

1BigDecimal subtotal = BigDecimal.ZERO;
2
3for (OrderItem item : items) {
4    if (item.isBillable()) {
5        BigDecimal lineTotal =
6            item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQty()));
7        subtotal = subtotal.add(lineTotal);
8    }
9}

There is nothing wrong with this. But to understand it, you have to mentally simulate:

  • the mutable subtotal
  • the loop
  • the conditional branch
  • the line-total calculation

Clojure usually makes those steps more explicit as a value pipeline.

The Clojure Shape: Filter, Transform, Aggregate

1(defn line-total [{:keys [qty unit-price]}]
2  (* qty unit-price))
3
4(defn subtotal [items]
5  (->> items
6       (filter :billable?)
7       (map line-total)
8       (reduce + 0M)))

This version still does the same work, but the structure is easier to scan:

  • choose the relevant items
  • transform each item
  • combine the results

The original items collection is untouched throughout.

map, filter, And reduce Cover Most Early Work

The three core collection operations show up constantly:

  • map: transform each element
  • filter: keep only matching elements
  • reduce: collapse many values into one

Example:

 1(def orders
 2  [{:order/id 1001 :subtotal 120M :status :paid}
 3   {:order/id 1002 :subtotal 35M  :status :draft}
 4   {:order/id 1003 :subtotal 80M  :status :paid}])
 5
 6(->> orders
 7     (filter #(= :paid (:status %)))
 8     (map :subtotal)
 9     (reduce + 0M))
10;; => 200M

This is a good mental pattern to internalize early. Many imperative loops turn into exactly this three-stage story.

Threading Macros Help You Read Left To Right

The ->> threading macro is useful when the collection flows through the final argument position of each step.

Without it, the same code is still valid but less readable:

1(reduce + 0M
2        (map :subtotal
3             (filter #(= :paid (:status %)) orders)))

Both versions work. The threaded form is easier to read in the same order the data moves.

For Java engineers, ->> often feels like the missing piece that makes nested functional calls readable instead of inside-out.

Prefer Named Functions When The Logic Matters

Anonymous functions are fine for tiny steps, but once the transformation has real meaning, naming it makes the pipeline clearer:

 1(defn paid? [order]
 2  (= :paid (:status order)))
 3
 4(defn order-subtotal [order]
 5  (:subtotal order))
 6
 7(->> orders
 8     (filter paid?)
 9     (map order-subtotal)
10     (reduce + 0M))

This is especially helpful during refactoring because you can test the smaller functions independently.

Use mapv When You Actually Want A Vector

One practical nuance matters here.

map returns a sequence, which is often exactly what you want in a pipeline. But if you specifically want a vector result, use mapv:

1(mapv :order/id orders)
2;; => [1001 1002 1003]

This is a useful habit when:

  • the result will be indexed later
  • you want a realized vector immediately
  • a downstream API expects a vector-like shape

Not every transformation must eagerly realize data, but when you know the concrete result shape you want, say so directly.

Transform Nested Data Without Mutating It

Collection transformation is not limited to flat lists.

You will often transform nested application data:

1(defn add-line-total [item]
2  (assoc item :line-total (line-total item)))
3
4(defn price-order [order]
5  (update order :items #(mapv add-line-total %)))

Here, price-order returns a new order with priced items. It does not mutate the existing order or any shared list of items.

That style scales much better than passing around mutable objects whose nested contents change invisibly.

A Good Refactoring Heuristic

When you see imperative collection code, ask:

  1. What items are being kept?
  2. What is being transformed?
  3. What final result is being produced?

Those three questions often map directly to:

  • filter
  • map
  • reduce

Even if the final solution uses a slightly different combination, this is a strong first-pass way to translate Java loops into idiomatic Clojure.

Knowledge Check

### What is the main collection-transformation shift from Java to Clojure? - [x] Move from mutating accumulators inside loops to expressing value transformations explicitly - [ ] Stop using collections entirely - [ ] Replace all functions with classes - [ ] Avoid aggregation > **Explanation:** Clojure favors explicit transformation pipelines that leave the original collections unchanged. ### What does `map` do in a Clojure pipeline? - [x] It transforms each element of a collection into a corresponding output value - [ ] It removes elements that do not match a predicate - [ ] It collapses a collection into one value - [ ] It mutates the original vector in place > **Explanation:** `map` applies a function across the collection and returns the transformed results. ### When is `mapv` usually a better choice than `map`? - [x] When you specifically want a vector result rather than a sequence - [ ] When you want to mutate the original collection - [ ] When you need a set - [ ] When you are writing Java interop code only > **Explanation:** `mapv` realizes the transformed results into a vector, which is useful when that concrete shape matters. ### What does `->>` mainly improve? - [x] Readability when collection data flows through successive transformation steps - [ ] Runtime type safety - [ ] Object mutation - [ ] Namespace loading > **Explanation:** The threading macro helps you read a transformation pipeline in the same order the data conceptually moves.
Revised on Friday, April 24, 2026