---
title: 'Stream API'
source: 'https://academia.sh/en/courses/java-standard-library/stream-api'
course: 'The Standard Library and Streams'
language: en
updated: '2026-08-17T18:09:42+00:00'
license: 'CC BY-SA 4.0'
---

# Stream API

Java's stream chain guarantees three of ten operations independent of the source, defers five to the source's order, and leaves two to the caller. An intermediate operation never runs without a terminal operation, but even once a terminal operation gets called, which item gets visited is not guaranteed. Consuming the same stream twice and growing the source while iterating both fall with an exception.

The previous lesson measured what a single lambda expression captures
from its surroundings, and showed that four syntax forms bind to the
same target. The standard library's real form of use is not a single
lambda like this, it is several lambdas chained one after another — a
**stream** chain. Even though every link works correctly on its own, a
separate question is born once it gets strung into a chain: which part
of the chain depends on the source's order, which one gives the same
order under every condition, which one never runs without a terminal
operation, and which rule is left entirely to the caller?

This lesson classifies ten separate claims by running them: for each
one, is the observed behavior independent of the source (an
**interface guarantee**), bound to the source's own order, or a rule
the **caller** follows? The measurement's shape is the same as before
— code gets written, run, the result gets compared — but what gets
compared here is not two implementations, it is the same operation's
behavior on two separate sources. To avoid confusion with `java.io`
streams, every bare "stream" word used from here on gets the qualifier
from this paragraph: the **stream chain**, or stream for short.

## The Stream Chain's Order Comes from the Source, Not the Interface

- **FJ17** — The same five words get held in two sources: an ordered
  List and an unordered HashSet. The same stream chain runs on both,
  and the results get compared with equals; the raw iteration order
  never gets printed anywhere.
- **FJ18** — `sorted()` runs with natural order; uppercasing happens
  with `Locale.ROOT`.
- **FJ19** — `distinct()` and `findFirst()` get tried on the ordered
  source; they do not get repeated with the unordered source, because
  what gets measured is the existence of first-seen order, and this
  concept is undefined on an unordered source.

```java
// StreamOrder.java - does the stream chain's order come from the source or the interface
import java.util.*;
import java.util.stream.*;

public class StreamOrder {
    public static void main(String[] args) {
        List<String> orderedSource = List.of("pear", "cherry", "apple", "grape", "fig");
        Set<String> unorderedSource = new HashSet<>(orderedSource);

        List<String> sorted1 = orderedSource.stream().sorted().toList();
        List<String> sorted2 = unorderedSource.stream().sorted().toList();
        System.out.println("sorted(), alphabetical order on ordered source : "
                + sorted1.equals(List.of("apple", "cherry", "fig", "grape", "pear")));
        System.out.println("sorted(), same result on unordered source      : " + sorted2.equals(sorted1));

        List<String> expectedUpper = List.of("PEAR", "CHERRY", "APPLE", "GRAPE", "FIG");
        List<String> mappedOrdered = orderedSource.stream().map(x -> x.toUpperCase(Locale.ROOT)).toList();
        List<String> mappedUnordered = unorderedSource.stream().map(x -> x.toUpperCase(Locale.ROOT)).toList();
        System.out.println("map(), keeps input order on ordered source     : " + mappedOrdered.equals(expectedUpper));
        System.out.println("map(), keeps input order on unordered source   : " + mappedUnordered.equals(expectedUpper));

        List<String> expectedLong = List.of("pear", "cherry", "apple", "grape");
        List<String> filtered = orderedSource.stream().filter(x -> x.length() > 3).toList();
        System.out.println("filter(), keeps order on ordered source        : " + filtered.equals(expectedLong));

        List<String> repeated = List.of("pear", "cherry", "pear", "apple", "cherry");
        List<String> unique = repeated.stream().distinct().toList();
        System.out.println("distinct(), kept in first-seen order           : "
                + unique.equals(List.of("pear", "cherry", "apple")));

        Optional<String> first1 = orderedSource.stream().findFirst();
        Optional<String> first2 = orderedSource.stream().findFirst();
        System.out.println("findFirst(), do two calls give the same item   : " + first1.equals(first2));
    }
}
```

```
sorted(), alphabetical order on ordered source : true
sorted(), same result on unordered source      : true
map(), keeps input order on ordered source     : true
map(), keeps input order on unordered source   : false
filter(), keeps order on ordered source        : true
distinct(), kept in first-seen order           : true
findFirst(), do two calls give the same item   : true
```

The first two lines measure a guarantee independent of the source:
`sorted()` produces alphabetical order on the ordered source, and
gives the **same** result on the unordered `HashSet` source too.
Whatever container the input gets held in, `sorted()` fulfills its own
promise on its own — this is an **interface guarantee**, and it needs
nothing from the source's own order.

The third and fourth lines show exactly the opposite. `map()` carries
the input's order on the ordered `List` source (`true`), but does not
carry that order on the unordered `HashSet` source, even though it
works on the same words (`false`). `map()` itself never changed; what
changed was the **source**. The order promise belongs not to `map()`,
but to the **container underneath it** — this is the lesson's boundary
measurement, and it confirms the topic's claim: the sentence "a stream
keeps order" is the source's promise, not the stream's.

The remaining three lines fall into the same **source-dependent** set.
`filter()` keeps the order of the remaining items on the ordered
source; `distinct()` keeps the first-seen one among duplicates and
does not disturb the order; `findFirst()` gives the same item on two
separate calls on the ordered source. What the three share: what
decides the result is the source's **encounter order**, and this order
exists only when the source is ordered. Repeated with an unordered
source, the concept of "first seen" or "first item" itself becomes
undefined.

This lesson uses "source-dependent" as a separate bucket, because the
trio's middle term from the collections topic — **implementation
behavior** — does not correspond to a different choice here. There,
what got chosen was a separate class, like `HashMap` versus `TreeMap`;
here, `map()` itself never changes, the only thing that changes is the
source's **structure** (ordered or not). The outcome comes out the
same direction: the observation is in the hands of a third party
invisible in the call's writing, and that party in this topic is not
the interface, it is the **source**.

The same row also has a looser end. `findFirst()` says "give the same
item on an ordered source"; the neighboring method `findAny()` gives
no such promise, it only guarantees returning **an** item that
matches.

- **FJ20** — `findFirst()` and `findAny()` get called separately on
  the same ordered source, from the same stream chain, and the
  returned values get compared with equals.

```java
// FindAny.java - do findFirst and findAny give the same promise
import java.util.*;

public class FindAny {
    public static void main(String[] args) {
        List<String> source = List.of("pear", "cherry", "apple", "grape", "fig");
        Optional<String> first = source.stream().findFirst();
        Optional<String> any = source.stream().findAny();
        System.out.println("findFirst() result: " + first.get());
        System.out.println("findAny() result  : " + any.get());
        System.out.println("are the two equal : " + first.equals(any));
    }
}
```

```
findFirst() result: pear
findAny() result  : pear
are the two equal : true
```

In this ordered, single-threaded stream chain, the two give the same
item — the API's looseness never shows up here, because putting that
looseness to actual use only comes up once the stream gets
**parallelized**, and this course does not measure stream
parallelization. The only thing measured is this: two separate methods
giving the same result with two separate promises shows that a
promise's tightness can be independent of the outcome — `findAny()`
promises less, but we do not pay this lesser promise's cost in this
run. `findAny()` does not enter the table, because the one promise it
gives — "a matching item" — is already unconditional; the only thing
bound to the source is `findFirst()`'s "**which** item" question.

## An Intermediate Operation Never Runs Without a Terminal

Intermediate operations in a stream chain, like `map`, `filter`,
`sorted`, do not run the moment they get built; they only get added to
the chain. This is the here-asked form of **lazy evaluation**, a
concept built in a previous topic — the concept does not get
reopened, the question asked is this: is this laziness the stream
chain's **promise**, or is it only an implementation detail?

- **FJ21** — The same intermediate operation gets built three times:
  with no terminal at all, with the `count()` terminal, and with the
  `toList()` terminal. Each time, a counter counts how many times the
  intermediate operation ran.

```java
// Laziness.java - does an intermediate operation run without a terminal operation
import java.util.*;
import java.util.stream.*;

public class Laziness {
    public static void main(String[] args) {
        List<String> source = List.of("pear", "cherry", "apple", "grape", "fig");

        int[] counter1 = {0};
        Stream<String> pipeline = source.stream().peek(x -> counter1[0]++);
        System.out.println("steps run with no terminal called          : " + counter1[0]);

        int[] counter2 = {0};
        long length = source.stream().peek(x -> counter2[0]++).count();
        System.out.println("steps run with the count() terminal        : " + counter2[0] + " (length=" + length + ")");

        int[] counter3 = {0};
        source.stream().peek(x -> counter3[0]++).toList();
        System.out.println("steps run with the toList() terminal       : " + counter3[0]);
    }
}
```

```
steps run with no terminal called          : 0
steps run with the count() terminal        : 0 (length=5)
steps run with the toList() terminal       : 5
```

The first line gives the expected result: when no terminal ever gets
called, the intermediate operation never runs, the counter stays at
zero. This is an unconditional guarantee — the stream chain's own
promise — and this is the extent of lazy evaluation's counterpart here
from the fourth topic.

The second line narrows this guarantee in one direction. Even though
`count()` gets called, the counter is still **zero**: the length got
computed correctly (**5**), but the intermediate operation never ran.
The reason is in the source itself: the list `List.of(...)` produces
already knows its size, and once an intermediate operation that does
not change item count, like `map`/`filter`, gets added to the chain,
the stream chain can compute the result straight from the source's
size without visiting items one by one. Since `peek` is part of this
chain too, this computation that takes a shortcut through the source's
size never calls it at all. The third line confirms this: `toList()`
genuinely needs every item to produce its result, so there is no
shortcut, and the intermediate operation runs **five** times.

The conclusion here is narrower than it looks at first glance: what
gets guaranteed is "never running without a terminal," not "running
for every item once a terminal gets called." The second is an
**implementation decision** — if the stream chain can produce the
result another way, it can skip the intermediate step. `peek`, for
this reason, is only for observation and debugging; if it gets built
as part of business logic, whether it runs stays dependent on the
terminal.

## A Stream Is Single-Use, and Cannot Grow Its Source While Iterating

The last two rows fall into the **caller's rule**: these two are
constraints the caller has to follow, independent of the source's type
or the stream chain's shape.

- **FJ22** — A stream object gets consumed twice; the second
  consumption gets tried in a separate `try` block. In a separate
  stream chain, an item gets added to the source list during
  iteration.

```java
// Rule.java - the caller's two rules: single use and no growing while iterating
import java.util.*;
import java.util.stream.*;

public class Rule {
    public static void main(String[] args) {
        List<String> source = List.of("pear", "cherry", "apple", "grape", "fig");

        Stream<String> singleUse = source.stream();
        singleUse.count();
        try {
            singleUse.count();
            System.out.println("consuming the same stream a second time: fell through");
        } catch (IllegalStateException e) {
            System.out.println("consuming the same stream a second time -> exception: " + e.getClass().getSimpleName());
        }

        List<String> growing = new ArrayList<>(source);
        try {
            growing.stream().forEach(x -> {
                if (x.equals("apple")) growing.add("mango");
            });
            System.out.println("growing the source while iterating: fell through, size=" + growing.size());
        } catch (ConcurrentModificationException e) {
            System.out.println("growing the source while iterating -> exception: " + e.getClass().getSimpleName());
        }
    }
}
```

```
consuming the same stream a second time -> exception: IllegalStateException
growing the source while iterating -> exception: ConcurrentModificationException
```

Both rules give an **exception** when they fall, they do not stay
silent. Once `count()` gets called once on `singleUse`, the stream
counts as consumed; the second call fails with `IllegalStateException`
because it tries to reuse the stream itself — a rule that comes from
the stream object's own state, independent of the source's type. This
behavior shows that a stream object internally carries a flag marked
"consumed": once that flag flips, calling a new terminal on it —
whichever one — gets met with the same exception.

The second rule rests on a different mechanism: while the `growing`
list gets iterated with `forEach`, the same list gets grown, and this
breaks the list's own modification counter, raising
`ConcurrentModificationException` — even though the stream's terminal
is `forEach`, the exception that falls is actually the **source's
own** protection; the stream chain here is only an intermediary using
the source's iterator. The difference between these two matters: the
first exception is the stream API's **own** rule and holds for every
stream object; the second is bound to the stream's **source** — a
stream chain built from an unmodifiable source can never grow while
iterating in the first place, so this rule never enters play there at
all.

The ten rows measured across this lesson can get gathered in a single
table:

| # | Measured | Result | Source |
|---|---|---|---|
| 1 | `sorted()`, ordered source | alphabetical order | interface guarantee |
| 2 | `sorted()`, unordered source | same result | interface guarantee |
| 3 | `map()`, ordered source | order kept | source-dependent |
| 4 | `map()`, unordered source | order not kept | source-dependent |
| 5 | `filter()`, ordered source | order kept | source-dependent |
| 6 | `distinct()`, ordered source | first-seen kept | source-dependent |
| 7 | `findFirst()`, ordered source | consistent result | source-dependent |
| 8 | intermediate operation, no terminal | 0 runs | interface guarantee |
| 9 | second consumption of the same stream | exception | caller's rule |
| 10 | growing the source while iterating | exception | caller's rule |

The distribution is this: **three** are a guarantee independent of the
source, **five** depend on the source's or the terminal's choice,
**two** are rules the caller has to follow and that fall with an
exception when broken. `count()` skipping `peek` did not enter the
table as a separate row, because what it measures is not a source
dependency, it is the terminal's own choice — it should get read as a
lower bound of row eight. The majority of the ten operations — **five**
— carry the promise not of the interface, but of the **source**; this
runs the same direction as the order the previous topic built: most of
the standard library's visible behavior is the promise not of the
interface it speaks through, but of the choice underneath it.

## Summary

- A stream chain's promise about order usually comes from the source:
  `map`, `filter`, `distinct`, `findFirst` keep encounter order only
  if the source is ordered.
- `sorted()` is a guarantee independent of the source: it produces the
  same result with an ordered or unordered source. Boundary
  measurement: the sentence "a stream keeps order" is mostly the
  source's promise.
- An intermediate operation never runs without a terminal operation;
  this is an unconditional guarantee. But visiting every item once a
  terminal gets called is not guaranteed — `count()` can skip the
  intermediate operation entirely, `toList()` cannot.
- Consuming the same stream a second time falls with
  `IllegalStateException`; this is a rule belonging to the stream
  object's own state, independent of the source's type.
- Growing the source collection while iterating the stream falls with
  `ConcurrentModificationException`; the exception that falls is not
  the stream's protection, it is the underlying collection's.
- Three of the ten measurements are a guarantee independent of the
  source, five are source-dependent, two are the caller's rule; the
  majority stayed source-dependent.

## Next Step

This lesson measured what a stream chain says, and what it does not:
order is mostly the source's promise, laziness is an unconditional
guarantee but visiting every item is not, and two rules — single use,
no growing while iterating — fall directly to the caller. What
accumulates at the end of the chain is usually not a single value, it
is a **container** — a list, a map, a number — and this lesson always
used ready-made collecting methods (`toList()`, `count()`). The next
lesson looks at the piece that builds that container: what promise a
**collector** gives, whether the result container's type is the
interface's promise, and, once a repeated key gets encountered, which
collector throws an exception and which one merges.
