Stream recipes: indices, last element and strings
Practical stream recipes for indexing elements, finding the last element, joining and splitting strings, character counting, word transforms, and interview-safe trade-offs.
On this page
- Picture it first: three adapters on the same stream
- Indices under the hood: range over positions, then look up values
- Getting the last element: traversal versus direct access
- String joins and splits: keep text at the edges
- Character recipes: counts, first non-repeat and small transforms
- How to say it in the interview
🧰 What are stream recipes?
A stream recipe is a small, repeatable pattern for a job that shows up in interviews and code review: add indices, get the last element, join strings, split lines or count characters. Think of it like a kitchen card, not a full cookbook. You still choose the right tool, but the shape is familiar enough that you should not invent it from scratch every time.
For this lesson, we'll focus on three recipe families: index-aware traversal, last-element access and string operations. We'll keep primitive stream fundamentals, especially chars() versus codePoints(), in Primitive streams: IntStream, LongStream and DoubleStream, and we'll link to collection iteration where a plain list loop is clearer. Interviewers ask these because the code is short, but the trade-offs expose whether you understand stream order, single-use pipelines and when not to force a stream.
Below the line (out of scope)
- Primitive numeric stream basics: Primitive streams: IntStream, LongStream and DoubleStream
- Slicing operations such as
skipandlimit: Slicing streams: skip, limit, takeWhile and dropWhile - General iteration choices: Iterator, ListIterator and every way to iterate
- Grouping collectors in depth: Grouping and partitioning
The 30-second answer
For indices over a List, use IntStream.range(0, list.size()).mapToObj(i -> ...); it keeps the index source explicit and works because lists have O(1) indexed access for ArrayList. Avoid AtomicInteger counters in streams because they smuggle mutable state into the pipeline and become wrong-looking or wrong in parallel. To get the last element of a finite stream, reduce((a, b) -> b) works but traverses everything; for a sized list, prefer list.getLast() in Java 21 or list.get(list.size() - 1) when you already have a list. For strings, use Collectors.joining for joins, Pattern.splitAsStream or String.lines() for splits, and String.chars() for simple UTF-16 code-unit recipes. Reach for codePoints() when full Unicode characters matter.
What you’ll learn
- Use
IntStream.rangefor index-aware list traversal without mutable counters - Build a small zip-with-index record when indexed values deserve a name
- Choose between
reduce((a, b) -> b),skip(count - 1)andList.getLast()for last-element access - Join values with
Collectors.joining(delimiter, prefix, suffix)and split text withPattern.splitAsStreamorString.lines() - Write character recipes with
String.chars(),LinkedHashMapandgroupingBywhile preserving order - Explain why streams are not always the best tool for strings, especially when Unicode or mutation matters
01Picture it first: three adapters on the same stream
Picture a stream as a narrow pipe. An index recipe adds a ruler beside the pipe. A last-element recipe drains the pipe and keeps replacing the remembered value. A string recipe converts between text and stream elements at the edges.
The important habit is to name the adapter you are using. Indexing, last-element access and text processing are different problems, even though all three can fit into a stream pipeline.
Three stream recipes add an index ruler, remember the last element, or convert strings at the edge of a pipeline
02Indices under the hood: range over positions, then look up values
Java streams do not carry an index because Stream<T> is built around elements, not positions. If the source is a List, the clean indexed recipe is to stream the positions with IntStream.range(0, list.size()) and look up each element by index. It reads like a loop, but the index is now part of the pipeline.
Try it yourself first
Before reading the output, write the three labels. Then decide whether this would still be a good recipe for a LinkedList with 100,000 elements.
import java.util.List;
import java.util.stream.IntStream;
final class IndexedLabelsDemo {
public static void main(String[] args) {
List<String> names = List.of("Asha", "Ben", "Mira");
List<String> labels = IntStream.range(0, names.size())
.mapToObj(i -> (i + 1) + ". " + names.get(i))
.toList();
System.out.println(labels);
}
}
The output is:
[1. Asha, 2. Ben, 3. Mira]
Notice three things.
range(0, names.size())is half-open, so the last index issize() - 1.- This is a good fit for
ArrayListand immutable lists, whereget(i)is O(1). - It is a poor fit for
LinkedList, where repeatedget(i)can make the traversal O(n²).
If the pair has meaning, I prefer to name it. A tiny record makes downstream code less cryptic than passing around Map.Entry<Integer, T>.
import java.util.List;
import java.util.stream.IntStream;
record Indexed<T>(int index, T value) { }
final class ZipWithIndexDemo {
static <T> List<Indexed<T>> zipWithIndex(List<T> values) {
return IntStream.range(0, values.size())
.mapToObj(i -> new Indexed<>(i, values.get(i)))
.toList();
}
public static void main(String[] args) {
System.out.println(zipWithIndex(List.of("red", "green", "blue")));
}
}
The output is:
[Indexed[index=0, value=red], Indexed[index=1, value=green], Indexed[index=2, value=blue]]
The tempting shortcut is an AtomicInteger counter:
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
final class AtomicIndexSmellDemo {
public static void main(String[] args) {
AtomicInteger index = new AtomicInteger();
List<String> labels = List.of("Asha", "Ben", "Mira").stream()
.map(name -> index.getAndIncrement() + ":" + name)
.toList();
System.out.println(labels);
}
}
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Getting the last element: traversal versus direct access
- String joins and splits: keep text at the edges
- Character recipes: counts, first non-repeat and small transforms
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal