Java Streams, Lambdas and Primitive Streams (1Z0-831)
A Java stream is a lazy, single-use pipeline that pulls elements from a source through intermediate operations such as filter, map and sorted to one terminal operation, and Oracle 1Z0-831 objective 6.1 asks you to create, filter, transform, process and sort data with object and primitive streams using lambda expressions that implement functional interfaces. This lesson shows how a pipeline actually executes, how to create finite and infinite streams, how slicing, matching and primitive-stream operations behave (including on empty streams), how to build comparators, and how lambdas, method references and the java.util.function interfaces fit the stream methods. Reduction, collectors, flatMap, concatenation and gatherers belong to objective 6.2.
On this page10 sections
- How a stream pipeline runs: lazy, element by element, and once
- Creating streams: finite sources, ranges and infinite streams
- Filtering, slicing and transforming: filter, map, distinct, skip, limit, takeWhile, dropWhile
- Matching and finding: short-circuit terminals and empty streams
- Primitive streams: IntStream, LongStream, DoubleStream and converting between them
- Sorting: natural order, Comparator factories and what reversed() covers
- The functional interfaces in java.util.function: shapes and method names
- Lambda syntax and the effectively final rule
- Method references: static, bound, unbound and constructor
- Composing functions and predicates: and, or, negate, andThen, compose
- Trace a stream pipeline element by element, including laziness, short-circuiting and single-use errors
- Create streams from collections, values, arrays, ranges, iterate and generate, and keep infinite streams bounded
- Predict the output of filter, map, distinct, skip, limit, takeWhile and dropWhile in any order
- Use anyMatch, allMatch, noneMatch, findFirst and findAny and state their results on empty streams
- Convert between object and primitive streams and use sum, count, max, min and average with their return types
- Sort with natural order and with comparators built from comparing, thenComparing, reversed and reverseOrder
- Match lambdas and method references to the core and primitive functional interfaces, and compose them
How a stream pipeline runs: lazy, element by element, and once
A stream pipeline is a source, zero or more intermediate operations and exactly one terminal operation, and nothing happens until the terminal operation is called. Intermediate operations (filter, map, peek, sorted, limit…) only describe work and return a new stream; the terminal operation (forEach, count, findFirst, anyMatch, sum, toList…) pulls elements through. A pipeline with no terminal operation prints nothing, even if its lambdas contain System.out calls.
Elements travel vertically: each element passes through every stage before the next element starts. Short-circuiting terminals and limit stop pulling as soon as they have their answer, so later elements are never examined at all.
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream.of("ant", "bee", "wasp", "moth")
.filter(s -> {
System.out.print("F:" + s + " ");
return s.length() == 4;
})
.map(s -> {
System.out.print("M:" + s + " ");
return s.toUpperCase();
})
.limit(1)
.forEach(System.out::println);
}
}This prints F:ant F:bee F:wasp M:wasp WASP. The filter ran three times, the map once, and moth was never touched — not "filter everything, then map everything".
A stream is single-use. Once an operation has been called on a stream object, calling a second operation on that same object throws IllegalStateException ("stream has already been operated upon or closed").
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> s = Stream.of("x", "y");
System.out.println(s.count());
s.forEach(System.out::print);
}
}This prints 2, then the forEach call throws. The check is eager: it fires at the call itself, so even attaching a second filter to a stream that already has one throws on that line, before any terminal operation runs. Reuse is never a compile error. The fix is always a fresh stream from the source (list.stream() again) or a Supplier<Stream<T>>.
Creating streams: finite sources, ranges and infinite streams
Streams are created from a collection (list.stream()), from values (Stream.of(...), IntStream.of(...)), from an array (Arrays.stream(array) — an int[] gives an IntStream), empty (Stream.empty()), from a numeric range, or from a generator function.
| Factory | Produces | Ends? |
|---|---|---|
IntStream.range(a, b) | a up to b−1 (end excluded, like a for loop) | Yes |
IntStream.rangeClosed(a, b) | a up to b (both ends included) | Yes |
Stream.iterate(seed, next) | seed, next(seed), next(next(seed))… | No — infinite |
Stream.iterate(seed, hasNext, next) | Like for (x = seed; hasNext(x); x = next(x)): the predicate is checked before every element, including the seed | When the predicate is false |
Stream.generate(supplier) | supplier.get() called again for every element | No — infinite |
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
IntStream.range(0, 3).forEach(System.out::print);
System.out.print(" | ");
IntStream.rangeClosed(0, 3).forEach(System.out::print);
System.out.print(" | ");
Stream.iterate(1, n -> n * 2).limit(4)
.forEach(n -> System.out.print(n + " "));
System.out.print("| ");
Stream.iterate(1, n -> n < 20, n -> n * 2)
.forEach(n -> System.out.print(n + " "));
System.out.print("| ");
Stream.generate(() -> "z").limit(3)
.forEach(System.out::print);
System.out.println();
}
}This prints 012 | 0123 | 1 2 4 8 | 1 2 4 8 16 | zzz. The three-argument iterate stopped because 32 fails n < 20.
Infinite streams need a stop the pipeline actually reaches. limit(n) only counts the elements that arrive at it. If a filter placed before limit never passes anything, limit never fills and the program runs forever without printing — no exception, and no memory build-up either, because elements are processed one at a time and discarded. Likewise sorted() or count() on an infinite stream never return. Put limit (or takeWhile) where it bounds the source.
Filtering, slicing and transforming: filter, map, distinct, skip, limit, takeWhile, dropWhile
filter(Predicate) keeps the elements for which the predicate is true; map(Function) replaces each element with the function's result, and that result's type becomes the stream's element type from then on. Every later stage sees the mapped values, so a filter after a map tests the new value, not the original, and + on an element that has become a String concatenates instead of adding.
The slicing operations are applied strictly in the order written; reordering them changes the result.
| Operation | What it does | Stops testing early? |
|---|---|---|
distinct() | Drops duplicates (by equals), keeping the first occurrence in encounter order | No |
skip(n) | Discards the first n elements that reach it | No |
limit(n) | Passes at most n elements, then stops pulling | Yes (short-circuiting) |
takeWhile(p) | Passes elements while p is true; at the first false it ends the stream | Yes |
dropWhile(p) | Discards elements while p is true; from the first false it passes everything, without testing again | Stops testing |
filter(p) | Tests every element independently | No |
import java.util.List;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream.of(4, 4, 1, 7, 1, 9, 2)
.distinct() // 4 1 7 9 2
.skip(1) // 1 7 9 2
.limit(3) // 1 7 9
.forEach(System.out::print);
System.out.print(" | ");
List<Integer> v = List.of(1, 3, 4, 5, 7);
v.stream().takeWhile(n -> n % 2 == 1)
.forEach(System.out::print);
System.out.print(" ");
v.stream().dropWhile(n -> n % 2 == 1)
.forEach(System.out::print);
System.out.print(" ");
v.stream().filter(n -> n % 2 == 1)
.forEach(System.out::print);
System.out.println();
}
}This prints 179 | 13 457 1357. takeWhile ends at 4 and never sees the later odd numbers; dropWhile keeps 5 and 7 even though they are odd, because it stopped testing at 4; filter looks at every element. On an ordered stream these two depend on encounter order, which is why they are usually applied to lists, ranges or sorted data. Note also that sorted() sorts only what reaches it: placed after limit, it sorts the few survivors, not the whole source.
Matching and finding: short-circuit terminals and empty streams
anyMatch, allMatch and noneMatch return a boolean and stop as soon as the answer is known; findFirst and findAny return an Optional<T> and stop at the first element that reaches them.
| Terminal | Stops at | Result on an empty stream |
|---|---|---|
anyMatch(p) | first element that matches (true) | false |
allMatch(p) | first element that fails (false) | true (nothing fails) |
noneMatch(p) | first element that matches (false) | true |
findFirst() | first element in encounter order | Optional.empty |
findAny() | any element (on a sequential stream, in practice the first) | Optional.empty |
import java.util.List;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
List<String> none = List.of();
System.out.println(none.stream().allMatch(s -> false)
+ " " + none.stream().anyMatch(s -> true)
+ " " + none.stream().noneMatch(s -> true));
System.out.println(Stream.of("a", "bb").findFirst()
+ " " + Stream.of("a").filter(s -> s.isEmpty())
.findFirst());
}
}This prints true false true and then Optional[a] Optional.empty. Printing an Optional shows its wrapper (Optional[a]), not the bare value; use get(), orElse(other) or ifPresent(consumer) to reach the value. When a peek precedes a match operation, it prints only the elements up to the one that decided the answer.
Primitive streams: IntStream, LongStream, DoubleStream and converting between them
IntStream, LongStream and DoubleStream hold unboxed numbers and add numeric terminals — sum(), average(), max(), min(), summaryStatistics() — that a Stream<Integer> does not have. Stream<Integer> has no sum() at all: calling it is a compile error ("cannot find symbol").
| From | To | Method |
|---|---|---|
Stream<T> | IntStream / LongStream / DoubleStream | mapToInt / mapToLong / mapToDouble |
IntStream | Stream<U> | mapToObj(IntFunction) |
IntStream | Stream<Integer> | boxed() |
IntStream | LongStream / DoubleStream | asLongStream() / asDoubleStream() (widening), or mapToLong / mapToDouble |
IntStream | IntStream | map(IntUnaryOperator) — the lambda must return an int |
IntStream.map cannot change the type: map(i -> i * 1.5) fails to compile with a lossy-conversion error, so use mapToDouble or mapToObj. Arithmetic inside a stage follows the element type at that stage: i / 2 on an IntStream is integer division even if asDoubleStream() comes later, while the same division after asDoubleStream() keeps the fraction. Likewise mapToLong(...).sum() returns a long, which cannot be assigned to an int without a cast.
| Terminal on IntStream | Return type | Empty stream |
|---|---|---|
sum() | int | 0 |
count() | long | 0 |
max() / min() | OptionalInt | OptionalInt.empty |
average() | OptionalDouble (on every primitive stream) | OptionalDouble.empty |
import java.util.List;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("sun", "moon", "sky");
int total = words.stream()
.mapToInt(String::length)
.sum();
System.out.println(total);
System.out.println(IntStream.of(2, 7, 3).max());
System.out.println(IntStream.of(2, 7, 3).average());
System.out.println(IntStream.empty().sum() + " "
+ IntStream.empty().count() + " "
+ IntStream.empty().max() + " "
+ IntStream.empty().average().orElse(0));
IntStream.rangeClosed(1, 3)
.mapToDouble(i -> i * 0.5)
.forEach(d -> System.out.print(d + " "));
System.out.println();
System.out.println(IntStream.of(1, 2, 3)
.boxed().toList());
}
}This prints 10, OptionalInt[7], OptionalDouble[4.0], 0 0 OptionalInt.empty 0.0, 0.5 1.0 1.5 and [1, 2, 3]. Because max() returns OptionalInt, int m = IntStream.of(1, 2).max(); does not compile; read it with getAsInt() or orElse(int) (getAsDouble() for OptionalDouble). An orElse(-1) on an OptionalDouble returns the double -1.0.
Sorting: natural order, Comparator factories and what reversed() covers
sorted() sorts by natural order, which requires the elements to implement Comparable; sorted(Comparator) sorts by the comparator you pass. sorted() on elements that are not Comparable (a plain record or class) compiles and then throws ClassCastException when the terminal operation runs, because Stream<T> places no bound on T.
Natural String order compares UTF-16 code units, so every uppercase initial sorts before every lowercase one. Comparator offers factories and default methods to build other orders:
| Building block | Meaning |
|---|---|
Comparator.comparing(keyFn) | Compare by a Comparable key |
Comparator.comparing(keyFn, keyCmp) | Compare by a key using a given order for that key only, e.g. Comparator.reverseOrder() |
comparingInt / comparingLong / comparingDouble | Compare by a primitive key without boxing |
.thenComparing(keyFn) | Tie-breaker used only when the earlier keys are equal |
.reversed() | Reverses the whole comparator built so far — every key before it |
Comparator.reverseOrder() / naturalOrder() | Static, no-argument; reverse or natural order of Comparable elements |
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
public class Main {
record Book(String title, int pages) {}
public static void main(String[] args) {
Stream.of("plum", "Date", "cherry", "Apple")
.sorted()
.forEach(s -> System.out.print(s + " "));
System.out.println();
var books = List.of(new Book("Go", 300),
new Book("C", 200), new Book("Ada", 300));
books.stream()
.sorted(Comparator.comparingInt(Book::pages)
.reversed()
.thenComparing(Book::title))
.forEach(b -> System.out.print(b.title() + " "));
System.out.println();
books.stream()
.sorted(Comparator.comparingInt(Book::pages)
.thenComparing(Book::title,
Comparator.reverseOrder()))
.forEach(b -> System.out.print(b.title() + " "));
System.out.println();
}
}This prints Apple Date cherry plum, then Ada Go C (pages descending, ties by title ascending, because reversed() came before thenComparing), then C Go Ada (pages ascending, and only the title tie-breaker reversed). Had reversed() come last, it would have reversed the title tie-breaker too. Two compile rules matter. First, reverseOrder() is a static factory with no parameters: it takes no key extractor and is not a method you call on a comparator you have built — to reverse one key, pass it as the second argument of comparing/thenComparing; to reverse everything so far, call the instance method reversed(). Second, an implicitly typed lambda gets its parameter type from the target type, and a call that has another method chained onto it has no target type, so the parameter is inferred as Object and any member access on it fails. A method reference to a record accessor, a lambda with a declared parameter type, or an explicit type witness (Comparator.<T, K>comparing(...)) gives the compiler the type back.
Stream sorting of an ordered stream is stable: elements that compare equal keep their encounter order, even under a reversed comparator. Reversing a length comparator puts longer strings first but leaves strings of equal length in their original order.
The functional interfaces in java.util.function: shapes and method names
Every lambda and method reference implements a functional interface, and the interface fixes how many arguments it takes, what it returns and the method name you call. The core types are generic; the primitive specializations avoid boxing and are what IntStream and friends require.
| Interface | Method | Shape |
|---|---|---|
Supplier<T> | get() | () → T |
Consumer<T> / BiConsumer<T,U> | accept | T → void / (T, U) → void |
Predicate<T> / BiPredicate<T,U> | test | T → boolean / (T, U) → boolean |
Function<T,R> / BiFunction<T,U,R> | apply | T → R / (T, U) → R |
UnaryOperator<T> | apply | T → T (extends Function<T,T>) |
BinaryOperator<T> | apply | (T, T) → T (extends BiFunction<T,T,T>) |
IntPredicate | test(int) | int → boolean |
IntUnaryOperator / IntBinaryOperator | applyAsInt | int → int / (int, int) → int |
IntFunction<R> | apply(int) | int → R (takes a primitive) |
ToIntFunction<T> | applyAsInt(T) | T → int (returns a primitive) |
IntToLongFunction / IntToDoubleFunction | applyAsLong / applyAsDouble | int → long / int → double |
IntSupplier / IntConsumer | getAsInt / accept(int) | () → int / int → void |
The naming rule: IntFunction takes an int; ToIntFunction returns one; and any interface whose method returns an int, long or double names it applyAsInt/applyAsLong/applyAsDouble (getAsInt/getAsLong/getAsDouble for suppliers). Boolean results are the exception: every predicate, primitive or not (IntPredicate, LongPredicate, DoublePredicate, BiPredicate), uses test, and BooleanSupplier uses getAsBoolean. The same pattern exists for Long and Double. The stream methods map onto them: IntStream.filter takes an IntPredicate (the generic Predicate is a different type and is not accepted, and the lambda parameter is a primitive int, so calling a method on it, such as x.toString(), does not compile), IntStream.map an IntUnaryOperator, mapToObj an IntFunction, mapToLong an IntToLongFunction, and Stream.mapToInt a ToIntFunction.
import java.util.function.*;
public class Main {
public static void main(String[] args) {
Supplier<String> sup = () -> "hi";
Consumer<String> con = s -> System.out.print(s + " ");
Function<String, Integer> fun = String::length;
Predicate<String> pre = String::isEmpty;
UnaryOperator<String> up = String::toUpperCase;
BinaryOperator<Integer> add = Integer::sum;
ToIntFunction<String> len = String::length;
IntFunction<String> stars = "*"::repeat;
con.accept(sup.get());
System.out.println(fun.apply("abc") + " " + pre.test("")
+ " " + up.apply("ok") + " " + add.apply(2, 3)
+ " " + len.applyAsInt("four") + " "
+ stars.apply(3));
}
}This prints hi 3 true OK 5 4 ***. A Supplier runs its body on every get() call — it does not cache — so () -> new StringBuilder() hands back a new object each time, and a Consumer that mutates one of those objects changes only that one.
Lambda syntax and the effectively final rule
A lambda is a parameter list, an arrow and a body: an expression body returns its value, a block body in braces needs explicit statements and a return when a value is expected. The parameter rules:
- One implicitly typed parameter can drop the parentheses (
s -> s.length()); zero, two or more parameters, or any declared type, need them. - All parameters must use the same style: all implicit
(s, n), all explicitly typed(String s, Integer n)or allvar(var s, var n). A list that mixesvarwith a name, or a declared type with a name, does not compile. - A block body holds statements, not bare values: an arithmetic expression on its own followed by a semicolon is not a statement, so a block that should produce a value needs
returnin front of it —{ return s.length() + n; }is the block form ofs.length() + n. - A lambda parameter or local declared in the body cannot reuse the name of a local variable already in scope ("variable s is already defined"); lambdas do not open a new scope for name shadowing the way a nested class does.
- For a functional interface returning
void(Runnable,Consumer), an expression body must be a statement expression — a method call, assignment, increment ornew— whose value is then discarded.() -> sb.append("!")is fine for aRunnableeven thoughappendreturns a value;() -> 1 + 2and() -> total == 0are not, because arithmetic and comparisons are not statements.
A lambda can read a local variable of the enclosing method only if that variable is final or effectively final — never reassigned after its one assignment. Any reassignment anywhere in the method, including one written after the lambda, makes every capture of it a compile error ("local variables referenced from a lambda expression must be final or effectively final"). Two things are allowed: a variable declared without a value and definitely assigned once on every path before use (for example in both branches of an if/else), and mutating the object a captured reference points to (adding to a captured list, incrementing an element of a captured array). Fields are not subject to the rule.
import java.util.ArrayList;
import java.util.List;
import java.util.function.*;
public class Main {
public static void main(String[] args) {
BiFunction<String, Integer, String> a =
(s, n) -> s.repeat(n);
BiFunction<String, Integer, String> b =
(String s, Integer n) -> s + n;
BiFunction<String, Integer, Integer> c =
(var s, var n) -> {
return s.length() + n;
};
String tag;
if (args.length > 0) {
tag = args[0];
} else {
tag = "w";
}
List<String> words = new ArrayList<>();
List.of("x", "w", "w").forEach(t -> {
if (t.equals(tag)) words.add(t.toUpperCase());
});
System.out.println(a.apply("ab", 2) + " "
+ b.apply("v", 9) + " " + c.apply("abc", 4)
+ " " + words);
}
}Run with no arguments, this prints abab v9 7 [W, W]: tag is effectively final because each path assigns it exactly once, and words is never reassigned — only its contents change.
Method references: static, bound, unbound and constructor
A method reference is shorthand for a lambda that only calls one method, and its kind decides how the functional method's parameters are passed to that method.
| Kind | Example | Equivalent lambda |
|---|---|---|
| Static | Integer::parseInt | s -> Integer.parseInt(s) |
| Bound (specific object) | greeting::contains | s -> greeting.contains(s) |
| Unbound (any object of a type) | String::strip, String::endsWith | s -> s.strip(), (s, t) -> s.endsWith(t) |
| Constructor | HashMap::new, TreeSet::new | () -> new HashMap<>(), c -> new TreeSet<>(c) |
| Array constructor | int[]::new | n -> new int[n] (an IntFunction<int[]>) |
For a bound reference the captured object is the receiver and every argument goes to the method; for an unbound Type::instanceMethod the first argument becomes the receiver and the rest go to the method. So String::isBlank fits Predicate<String> (one argument, the receiver, and a boolean result) and String::substring fits BiFunction<String,Integer,String> (receiver plus start index). Count the arguments and check the return type: an unbound reference to a no-argument instance method can never be a Supplier, because a Supplier supplies no receiver, and a method returning int cannot be a Predicate. A method reference never carries an argument list — writing parentheses after the method name is a syntax error.
A constructor reference picks the constructor whose parameter list matches the functional method's, and fails to compile when the class has none: HashMap::new as a Supplier uses the no-argument constructor, the same reference as a Function<Integer, Map<K,V>> uses HashMap(int initialCapacity), and a functional shape with no matching constructor (for example a BiFunction taking two strings) is a compile error. An array constructor reference always needs the length as its argument, so an array reference like String[]::new is an IntFunction and has no zero-argument form.
import java.util.*;
import java.util.function.*;
public class Main {
public static void main(String[] args) {
Function<String, Integer> parse = Integer::parseInt;
String greeting = "hello";
Predicate<String> inHello = greeting::contains;
Function<String, String> strip = String::strip;
BiFunction<String, String, Boolean> ends = String::endsWith;
BiFunction<String, Integer, String> cut = String::substring;
Supplier<Map<String, Integer>> mk = HashMap::new;
Function<List<String>, TreeSet<String>> sorted =
TreeSet::new;
IntFunction<String[]> arr = String[]::new;
System.out.println(parse.apply("41") + 1);
System.out.println(inHello.test("ell") + " "
+ strip.apply(" x ") + " " + ends.apply("java", "va")
+ " " + cut.apply("lambda", 3));
System.out.println(mk.get() + " "
+ sorted.apply(List.of("b", "a")) + " "
+ arr.apply(3).length);
}
}This prints 42, true x true bda and {} [a, b] 3.
Composing functions and predicates: and, or, negate, andThen, compose
The functional interfaces carry default methods that combine instances into new ones, so a pipeline can reuse small predicates and functions instead of writing one large lambda.
| Call | Result |
|---|---|
p.and(q) / p.or(q) | A predicate that is p && q / p || q (short-circuiting) |
p.negate() | The opposite of p only — it binds to the predicate it is called on |
Predicate.not(p) | Static form of negate, handy with method references: filter(Predicate.not(String::isBlank)) |
f.andThen(g) | Runs f first, then g on its result: g(f(x)) |
f.compose(g) | Runs g first, then f: f(g(x)) |
Calls chain left to right, so p.negate().or(q) means (not p) or q, not "not (p or q)". Consumer also has andThen, and the primitive interfaces have their own versions (IntPredicate.negate, IntUnaryOperator.andThen).
import java.util.function.*;
public class Main {
public static void main(String[] args) {
Function<Integer, Integer> inc = x -> x + 1;
Function<Integer, Integer> dbl = x -> x * 2;
System.out.println(inc.andThen(dbl).apply(5) + " "
+ inc.compose(dbl).apply(5));
Predicate<String> shortWord = s -> s.length() < 4;
Predicate<String> hasA = s -> s.contains("a");
System.out.println(shortWord.and(hasA).test("cat") + " "
+ shortWord.negate().or(hasA).test("dog") + " "
+ shortWord.negate().test("horse"));
UnaryOperator<String> trim = String::trim;
Function<String, Integer> len = trim.andThen(String::length);
System.out.println(len.apply(" ab "));
}
}This prints 12 11 ((5 + 1) × 2, then 5 × 2 + 1), true false true and 2. Composition changes the type: andThen and compose on a UnaryOperator<String> are inherited from Function and return a Function, never a UnaryOperator, so assigning the result to a UnaryOperator variable does not compile. The type arguments must also line up: trimming a String and then taking its length is a Function<String,Integer>, and the declared variable must say exactly that.
Tip. Objective 6.1 is examined through short, complete programs that ask for one of three things: the exact output, whether the code compiles, or whether it throws at runtime. Pipeline questions place print statements inside intermediate operations to test laziness, element-by-element processing and where short-circuiting stops, or check what happens when a stream is reused or an infinite source is bounded. Data-processing questions chain slicing, mapping, primitive-stream and sorting operations and ask for the result or its type. Lambda and functional-interface questions present several similar-looking declarations or insertion points and ask which ones compile or give a stated result, so precise knowledge of parameter syntax, interface shapes, method-reference kinds and composition order matters more than memorised examples.
- Nothing runs without a terminal operation; elements then pass through all stages one at a time, and short-circuiting operations stop early.
- A stream is single-use: a second operation on the same stream object throws IllegalStateException at that call, never a compile error.
- range excludes its end, rangeClosed includes it; three-argument iterate checks its predicate before every element; iterate and generate without a bound never end.
- Slicing operations apply in the order written; takeWhile ends at the first failure, dropWhile stops testing at the first failure.
- On an empty stream allMatch and noneMatch are true and anyMatch is false; findFirst returns Optional.empty.
- Only primitive streams have sum and average; max, min and average return OptionalInt/OptionalDouble, while sum and count return a primitive 0 when empty.
- sorted() on non-Comparable elements compiles and throws ClassCastException; reversed() reverses every key before it, and a chained comparing(lambda) loses its target type.
- IntFunction takes an int, ToIntFunction returns one; methods returning int, long or double are named applyAsInt, applyAsLong, applyAsDouble, while every predicate uses test and BooleanSupplier uses getAsBoolean.
- Captured locals must be effectively final, but the objects they reference may be mutated; lambda parameters cannot reuse an in-scope local's name.
- compose runs its argument first, andThen runs it last, and composing a UnaryOperator yields a Function.
Frequently asked questions
Why does a Java stream with peek or map print nothing?
Intermediate operations such as peek, map and filter are lazy: they only describe work and return a new stream. The lambdas run only when a terminal operation such as forEach, count, findFirst or toList pulls elements through the pipeline, so a pipeline without a terminal operation never executes them.
Can a Java stream be reused?
No. A stream can be operated on only once. Calling a second operation on a stream object that has already been used, whether another terminal operation or another intermediate operation, throws IllegalStateException at that call. It compiles fine; to process the data again, create a new stream from the source.
What is the difference between IntStream.range and rangeClosed?
IntStream.range(a, b) produces a up to b minus 1, excluding the end like a standard for loop, while IntStream.rangeClosed(a, b) includes b. So range(1, 4) gives 1, 2, 3 and rangeClosed(1, 4) gives 1, 2, 3, 4.
Why can't I call sum() on a Stream<Integer>?
sum(), average() and the other numeric terminals are defined only on the primitive streams IntStream, LongStream and DoubleStream. A Stream<Integer> is a stream of objects with no sum method, so the call does not compile. Convert first with mapToInt(Integer::intValue) or build an IntStream directly with mapToInt.
What do allMatch, anyMatch and noneMatch return on an empty stream?
On an empty stream allMatch returns true because no element fails the predicate, noneMatch returns true because no element matches it, and anyMatch returns false because no element matches it. None of them throws an exception.
What is the difference between Function.compose and Function.andThen?
f.andThen(g) applies f first and then g to its result, giving g(f(x)). f.compose(g) applies g first and then f, giving f(g(x)). Both return a Function, even when called on a UnaryOperator.
What does effectively final mean for Java lambdas?
A local variable is effectively final if it is assigned once and never reassigned anywhere in the method. A lambda can capture only final or effectively final locals; reassigning the variable, even after the lambda, is a compile error. Changing the state of the object the variable refers to, such as adding to a list, is allowed.
When is a method reference bound or unbound?
A bound reference such as prefix::startsWith names a specific object, which becomes the receiver while all arguments go to the method. An unbound reference such as String::startsWith names a type, so the first argument of the functional method becomes the receiver and the remaining arguments are passed to the method.
Source
This lesson covers the "Processing Data Using Streams and Lambda Expressions" domain of the official 1Z0-831 exam guide. Vendors revise their guides — check the source for the current version.
- Java SE 25 Developer Professional (1Z0-831) exam page and topics — Oracle University
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.