Back to Stream API Fundamentals
Q5Question 5 of 8

When to use a loop over Stream API?

Key Concepts
  • Use a for-loop when you need index-based access to compare adjacent elements (list.get(i) vs list.get(i-1)).
  • Use a for-loop for in-place mutation during iteration, especially with JPA dirty checking.
  • Use a for-loop when per-element try/catch is needed for checked exceptions that lambdas cannot handle cleanly.
  • Use a for-loop for complex break/continue with mutable accumulators like running budget totals.
  • Use a for-loop for nested loops with labeled break (break outer) which has no stream equivalent.
  • Streams win for declarative filter-map-collect-reduce, grouping, aggregation, and easy parallelisation.
  • Knowing when NOT to use streams demonstrates deeper understanding than always defaulting to streams.

Definition

Use a traditional loop for index access, in-place mutation, checked exceptions, complex break logic, and nested loops; use streams for declarative pipelines, grouping, and parallelisation.
  • Index access: list.get(i) vs list.get(i-1) for comparing adjacent elements has no natural stream equivalent.
  • In-place mutation: modifying the original collection during iteration is stream-hostile (side-effect-free principle).
  • Checked exceptions: lambdas do not support checked exceptions without ugly wrappers.
  • Complex break: running totals with conditional break need mutable accumulators that violate stream principles.
  • Nested loops with labeled break: break outer; labels have no stream equivalent.
  • Performance-critical primitives: for tight loops on millions of primitive values, the for-loop avoids autoboxing overhead.
  • Streams win: for filter-map-collect-reduce, grouping, aggregation, and any read-only analytical query.

Streams are the automated conveyor belt at National Bank of India: perfect for standard processing like filter, sort, and count. But some tasks need a human clerk: comparing today's balance with yesterday's (index access), crossing out a wrong amount and writing the correct one (in-place mutation), trying to contact the customer and marking as unreachable if they do not answer (try/catch per element). For these tasks, the conveyor belt is awkward: use the clerk (for-loop).

Understanding the Concepts

When should you use a loop over the Stream API?

Streams and loops are complementary tools, not replacements. Use each where it naturally fits.

Case 1: Index-based access

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;

public class IndexAccessDemo {
    record DailyBalance(String date, BigDecimal balance) {}

    public static void main(String[] args) {
        List<DailyBalance> balances = List.of(
            new DailyBalance("2024-01-01", new BigDecimal("100000")),
            new DailyBalance("2024-01-02", new BigDecimal("105000")),
            new DailyBalance("2024-01-03", new BigDecimal("98000"))
        );

        // LOOP: compare adjacent elements naturally
        for (int i = 1; i < balances.size(); i++) {
            BigDecimal today = balances.get(i).balance();
            BigDecimal yesterday = balances.get(i - 1).balance();
            if (today.compareTo(yesterday) < 0) {
                System.out.println(balances.get(i).date() + ": balance dropped");
            }
        }
    }
}
TEXT
2024-01-03: balance dropped

Case 2: Checked exceptions per element

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;

public class CheckedExceptionDemo {
    record Transfer(String from, String to, BigDecimal amount) {}
    static class TransferException extends Exception {
        TransferException(String msg) { super(msg); }
    }

    static void execute(Transfer t) throws TransferException {
        if (t.amount().compareTo(new BigDecimal("100000")) > 0) {
            throw new TransferException("Amount exceeds limit: " + t.amount());
        }
        System.out.println("Transferred Rs." + t.amount());
    }

    public static void main(String[] args) {
        List<Transfer> transfers = List.of(
            new Transfer("SAV2001", "SAV2002", new BigDecimal("50000")),
            new Transfer("SAV2001", "CUR3001", new BigDecimal("200000")),
            new Transfer("CUR3001", "SAV2003", new BigDecimal("30000"))
        );

        // LOOP: clean try/catch per element
        for (Transfer t : transfers) {
            try {
                execute(t);
            } catch (TransferException e) {
                System.out.println("Failed: " + e.getMessage());
            }
        }
    }
}
TEXT
Transferred Rs.50000
Failed: Amount exceeds limit: 200000
Transferred Rs.30000

Case 3: Complex break with mutable accumulator

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;

public class BudgetBreakDemo {
    record Order(String id, BigDecimal amount) {}

    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("ORD-001", new BigDecimal("50000")),
            new Order("ORD-002", new BigDecimal("80000")),
            new Order("ORD-003", new BigDecimal("120000")),
            new Order("ORD-004", new BigDecimal("30000"))
        );

        BigDecimal budget = new BigDecimal("200000");
        BigDecimal spent = BigDecimal.ZERO;
        List<Order> executed = new ArrayList<>();

        for (Order order : orders) {
            if (spent.add(order.amount()).compareTo(budget) > 0) {
                break; // stop: budget would be exceeded
            }
            spent = spent.add(order.amount());
            executed.add(order);
        }

        System.out.println("Executed: " + executed.size() + ", spent: Rs." + spent);
    }
}
TEXT
Executed: 2, spent: Rs.130000

When should you use streams instead?

Streams are better for declarative pipelines, grouping, aggregation, and parallelisation.

JAVA
// Grouping: impossible to beat with a for-loop
Map<String, BigDecimal> totalByBranch = accounts.stream()
    .collect(Collectors.groupingBy(Account::branch,
        Collectors.reducing(BigDecimal.ZERO, Account::balance, BigDecimal::add)));

What are the decision rules?

| Need | Use |
|------|-----|
| Index access (adjacent comparison) | for-loop |
| In-place mutation (JPA dirty checking) | for-loop |
| Checked exception per element | for-loop |
| Break with mutable accumulator | for-loop |
| Nested loops with labeled break | for-loop |
| filter-map-collect-reduce | Stream |
| groupingBy, partitioningBy | Stream |
| parallelStream | Stream |
| Read-only analytics | Stream |

What should a senior engineer know?

Forcing streams where loops are natural leads to worse code: convoluted workarounds for checked exceptions, side-effect-heavy forEach, and AtomicReference hacks. Conversely, forcing loops where streams shine leads to verbose boilerplate. In interviews, showing that you know when NOT to use streams demonstrates deeper understanding.

Visual Overview

DIAGRAM
  Decision: Loop vs Stream
  ────────────────────────
  Need index access?          → Loop
  Need in-place mutation?     → Loop
  Need checked exceptions?    → Loop
  Need break with accumulator?→ Loop
  ────────────────────────────────────
  Need filter-map-reduce?     → Stream
  Need groupingBy?            → Stream
  Need parallelism?           → Stream
  Need read-only analytics?   → Stream

Key Takeaway

Streams and loops are complementary tools. Use a for-loop for index-based access, in-place mutation, checked exceptions, complex break logic, and nested loops. Use streams for declarative filter-map-collect-reduce pipelines, grouping, aggregation, and parallelisation.

The key insight is that forcing streams where loops are natural leads to worse code, and forcing loops where streams shine leads to verbose boilerplate. In interviews, showing that you know when NOT to use streams demonstrates deeper understanding than always defaulting to streams.

Remember one sentence: use loops for index access, mutation, and checked exceptions; use streams for declarative pipelines, grouping, and parallelism.

---

Share
Use ← → arrow keys to navigate