Definition
- Index access:
list.get(i)vslist.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
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");
}
}
}
}
2024-01-03: balance dropped
Case 2: Checked exceptions per element
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());
}
}
}
}
Transferred Rs.50000
Failed: Amount exceeds limit: 200000
Transferred Rs.30000
Case 3: Complex break with mutable accumulator
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);
}
}
Executed: 2, spent: Rs.130000
When should you use streams instead?
Streams are better for declarative pipelines, grouping, aggregation, and parallelisation.
// 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
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.
---