Introduction to Streams in Java 8: Creating Streams from Values, Arrays and Collections
-
Last Updated: August 11, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
This introduction to streams in Java 8 starts where every pipeline starts: creating the stream itself. We walk through Stream.of, Stream.iterate, Stream.generate, Arrays.stream and collection streams with plain examples.
Every stream starts somewhere. Before you filter, map or collect anything, you need a stream in your hands. That first step goes by a simple name: stream creation.
Most tutorials rush past it. They show you list.stream() once and then spend twenty pages on collectors. Fair enough, because collectors are exciting. But half the confusion I see from beginners lives right at the source.
Why does Stream.of behave strangely with an int[]? Why does an infinite stream freeze the program? And why does the same stream blow up the second time you touch it? All three answers sit in the creation step.
So this article stays there on purpose. We will look at every practical way to build a stream, from a plain list to an endless sequence of random numbers.
The code samples use Java, and most of them run on Java 8. A few newer helpers arrived in Java 9, 11 and 16. I have marked each of those clearly, so nothing surprises you at compile time.
Here is the ground we cover:
Stream.of, and arrays with Arrays.streamStream.iterate and Stream.generateStream.ofNullableIntStream.range and friendsYou do not need lambda expertise for this. A quick look at lambda expressions helps, but the samples stay small enough to follow either way.
Before creating something, it helps to know what you are creating. A stream confuses people because it looks like a collection in code. It behaves nothing like one.
Picture a factory conveyor belt. Items roll along it, workers do something to each item, and a box at the end catches the results. The belt itself stores nothing.
A stream works the same way. Your ArrayList holds the data. The stream simply carries elements past a few operations, one at a time.
This explains a rule that trips up newcomers. You cannot ask a stream for its size, and you cannot grab element number three. The belt has already moved on.
It also explains why a stream works only once. Once the items reach the end of the belt, nothing remains to carry.
Every stream pipeline has exactly three parts. Learn these three words and the rest of the API falls into place.
filter, map or sorted. Each returns a new streamcollect, forEach or countThis whole article lives in part one. The other two parts have their own guides on the site, and I link to them as we go.
List<String> names = Arrays.asList("Riya", "Arjun", "Meera");
long count = names.stream() // source
.filter(n -> n.length() > 4) // intermediate
.count(); // terminal
System.out.println(count); // Output: 1Creating a stream from a list does not copy the list. It does not modify the list either. The stream keeps a reference and reads through it lazily.
So when you sort a stream, your original list stays in its original order. Beginners expect the opposite and then hunt for a bug that never existed.
List<Integer> numbers = Arrays.asList(3, 1, 2); numbers.stream().sorted().forEach(System.out::print); // Output: 123 System.out.println(); System.out.println(numbers); // Output: [3, 1, 2]
Notice the second line. The list still reads 3, 1, 2. The sorting happened on the belt, not in the box.
A few terms show up in every stream discussion. Here they are in plain English.
limit or findFirst that stops earlyIntStream, LongStream or DoubleStream, which avoid wrapper objectsThis route covers maybe eighty percent of real code. You already have a list or a set, and you want a stream over it.
Java 8 added a default method called stream() to the Collection interface. Every class in the collections family inherited it that day, for free.
List<String> cities = new ArrayList<>();
cities.add("Pune");
cities.add("Delhi");
cities.add("Kochi");
Stream<String> stream = cities.stream();
stream.forEach(System.out::println);
// Output:
// Pune
// Delhi
// KochiThe element type carries over. A List<String> gives you a Stream<String>, so the compiler still checks everything downstream.
Most of the time you will not store the stream in a variable at all. You chain straight off the collection and finish in one expression.
Immutable lists behave identically. A list from Arrays.asList, or from List.of on Java 9 and later, streams just as happily as an ArrayList. Read-only sources actually suit streams better, since nobody can change the data halfway through.
The same method works on a HashSet, a TreeSet, an ArrayDeque or a PriorityQueue. If it implements Collection, it can hand you a stream.
Set<String> tags = new TreeSet<>(Arrays.asList("java", "api", "stream"));
tags.stream().forEach(System.out::println);
// Output:
// api
// java
// streamOrder follows the source. A TreeSet streams in sorted order, an ArrayList in insertion order, and a HashSet in whatever order it feels like. The stream never adds ordering of its own.
That last point matters more than it looks. If you need a predictable order from a HashSet, add a sorted() step yourself.
Here comes the first small surprise. Map does not extend Collection, so a map has no stream() method.
You pick a view first. Three views exist, and each gives you a different element type.
Map<String, Integer> scores = new HashMap<>();
scores.put("Riya", 91);
scores.put("Arjun", 78);
scores.keySet().stream().forEach(System.out::println); // keys
scores.values().stream().forEach(System.out::println); // values
scores.entrySet().stream()
.forEach(e -> System.out.println(e.getKey() + " = " + e.getValue()));
// Output:
// Riya = 91
// Arjun = 78The entrySet() view wins most of the time. It keeps the key and the value together, which is usually what a report needs.
Curious about how a map stores those entries underneath? The HashMap guide digs into the buckets.
Collections also offer parallelStream(). Same elements, but the work spreads across several threads.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int total = numbers.parallelStream()
.mapToInt(Integer::intValue)
.sum();
System.out.println(total); // Output: 15Do not reach for it by reflex. On small lists the thread coordination costs more than the work itself. The parallel stream article covers when it actually pays off.
Sometimes you have no collection at all. You just have three or four values sitting in front of you.
Stream.of takes a varargs list and hands back a stream over those values. No list, no array, no ceremony.
Stream<String> colours = Stream.of("red", "green", "blue");
colours.forEach(System.out::println);
// Output:
// red
// green
// blue
Stream.of(10, 20, 30).forEach(System.out::println);
// Output:
// 10
// 20
// 30Notice the second example. Those integers become Integer objects, not int values, because a Stream only carries objects. For heavy number crunching, section 9 shows a cheaper route.
I use this constantly in unit tests. Three sample values inline read far better than a helper list built two lines earlier.
A single value works too. Stream.of("solo") gives you a stream of exactly one element.
Zero arguments also compile. Stream.of() returns an empty stream, though Stream.empty() says the same thing far more clearly.
System.out.println(Stream.of("solo").count()); // Output: 1
System.out.println(Stream.<String>of().count()); // Output: 0
System.out.println(Stream.empty().count()); // Output: 0Now the trap. Hand Stream.of an object array and it spreads the array into elements, exactly as you would expect.
String[] words = {"alpha", "beta", "gamma"};
System.out.println(Stream.of(words).count()); // Output: 3Hand it an int[] and everything changes. A primitive array cannot spread into varargs of type T, so the compiler treats the whole array as a single element.
int[] nums = {1, 2, 3};
Stream<int[]> odd = Stream.of(nums);
System.out.println(odd.count()); // Output: 1
// What you actually wanted:
System.out.println(Arrays.stream(nums).count()); // Output: 3Read that type again: Stream<int[]>. One element, and that element happens to be your whole array. Nothing fails at compile time, which makes it a nasty little bug.
Rule of thumb: for a primitive array, always call Arrays.stream.
Arrays predate the Stream API by two decades, so the bridge lives in the Arrays helper class rather than on the array itself.
The method takes an array and returns a stream over it. Object arrays give you a Stream, and that behaves exactly like a list stream.
String[] fruits = {"apple", "mango", "pear"};
Arrays.stream(fruits)
.map(String::toUpperCase)
.forEach(System.out::println);
// Output:
// APPLE
// MANGO
// PEARArrays carry no stream() method of their own. An array in Java has only a length field and the methods it inherits from Object, so the JDK could never bolt one on. That is why the helper lives in Arrays.
A second overload accepts two index arguments. The first index counts, the second does not. Same half-open rule you see everywhere else in Java.
int[] marks = {40, 55, 63, 78, 91};
// index 1, 2 and 3 only
Arrays.stream(marks, 1, 4).forEach(System.out::println);
// Output:
// 55
// 63
// 78This beats copying a sub-array first. No extra allocation, and the intent reads clearly at the call site.
Pass a bad index and you get an ArrayIndexOutOfBoundsException straight away, not a silent empty stream.
Here the return type shifts. An int[] produces an IntStream, a long[] produces a LongStream, and a double[] produces a DoubleStream.
That shift buys you real methods. sum, average, max and summaryStatistics all live on the primitive streams and nowhere else.
int[] marks = {40, 55, 63, 78, 91};
System.out.println(Arrays.stream(marks).sum()); // Output: 327
System.out.println(Arrays.stream(marks).max().getAsInt()); // Output: 91
System.out.println(Arrays.stream(marks).average().getAsDouble()); // Output: 65.4Each call needs a fresh Arrays.stream(marks). Three statements, three streams. Section 12.1 explains why reuse fails.
A two-dimensional array is really an array of arrays. So Arrays.stream on it yields a stream of rows, not a stream of values.
int[][] grid = {{1, 2}, {3, 4}, {5, 6}};
System.out.println(Arrays.stream(grid).count()); // Output: 3
int total = Arrays.stream(grid)
.flatMapToInt(Arrays::stream)
.sum();
System.out.println(total); // Output: 21Flattening deserves its own discussion, and it has one. The flatMap guide walks through nested structures properly.
Every source so far needed existing data. The next two build values out of thin air.
Stream.iterate takes a seed and a function. It emits the seed, applies the function to get the next value, then applies it again, forever.
Stream.iterate(1, n -> n * 2)
.limit(6)
.forEach(System.out::println);
// Output:
// 1
// 2
// 4
// 8
// 16
// 32Each value depends on the one before it. That dependency makes iterate perfect for sequences: powers of two, dates, running totals, tree depths.
Stream.iterate(LocalDate.of(2026, 1, 1), d -> d.plusDays(7))
.limit(3)
.forEach(System.out::println);
// Output:
// 2026-01-01
// 2026-01-08
// 2026-01-15Notice the limit(6) in both samples. Drop it and your program hangs, quietly burning CPU until you kill it.
Laziness saves you, but only partly. Nothing runs while you build the pipeline. The moment a terminal operation asks for elements, an unbounded source keeps handing them over.
Any short-circuiting operation works as the brake. limit, findFirst, anyMatch and takeWhile all stop the flow early.
int firstBig = Stream.iterate(1, n -> n * 3)
.filter(n -> n > 100)
.findFirst()
.get();
System.out.println(firstBig); // Output: 243That pipeline never ends on paper. In practice it stops after seven values, because findFirst asks for exactly one.
Java 9 added a friendlier overload. It takes a seed, a condition and a step function, which reads almost exactly like a classic for loop.
// Java 9 and later
Stream.iterate(1, n -> n <= 20, n -> n * 2)
.forEach(System.out::println);
// Output:
// 1
// 2
// 4
// 8
// 16Compare the shape with for (int n = 1; n <= 20; n *= 2). Seed, test, step. Same three slots, same order.
This version ends on its own, so no limit call sits at the end. On Java 8 you cannot use it, and limit remains your only option.
Fibonacci needs two previous values, and iterate carries only one. The usual trick keeps a small array as the state.
Stream.iterate(new int[]{0, 1}, f -> new int[]{f[1], f[0] + f[1]})
.limit(8)
.map(f -> f[0])
.forEach(n -> System.out.print(n + " "));
// Output: 0 1 1 2 3 5 8 13Each element carries the pair, and the final map throws away the half you do not need. Slightly clever, but a genuinely common pattern in interviews.
Its sibling Stream.generate takes a Supplier instead. No seed, no previous value, just a function that produces something whenever asked.
The simplest supplier returns a constant. Handy for padding, placeholders and test fixtures.
Stream.generate(() -> "pending")
.limit(3)
.forEach(System.out::println);
// Output:
// pending
// pending
// pendingLike iterate, this source never stops by itself. The limit call remains compulsory.
A supplier shines when every element should differ. Random values and freshly built objects both fit that shape.
Random random = new Random();
Stream.generate(() -> random.nextInt(100))
.limit(5)
.forEach(n -> System.out.print(n + " "));
// Output: five random numbers between 0 and 99
Stream.generate(UUID::randomUUID)
.limit(2)
.forEach(System.out::println);
// Output: two random UUIDsOne warning about parallel streams here. A supplier has no defined order, so generate plus parallel plus limit can pick elements in any sequence at all.
Keep generate sequential unless you truly do not care about order. Nobody enjoys debugging a test that fails once a week.
Both build infinite streams, so which do you pick? Ask yourself a single question: does the next value depend on the previous one?
| Point | Stream.iterate | Stream.generate |
|---|---|---|
| Input | Seed plus a UnaryOperator |
A Supplier only |
| Next value | Depends on the previous value | Independent every time |
| Ordering | Strictly ordered | Unordered |
| Typical use | Counters, dates, sequences | Random data, constants, new objects |
| Can stop itself | Yes, with the Java 9 overload | No, limit is mandatory |
| Parallel friendly | Poorly, values come in order | Yes, but order goes out the window |
Sequences point to iterate. Independent values point to generate. That single question settles nearly every case.
Empty cases sound boring until a NullPointerException wakes you at midnight. Two small factory methods prevent most of that pain.
Stream.empty() hands back a stream with zero elements. Every operation on it still works and simply produces nothing.
Stream<String> nothing = Stream.empty();
System.out.println(nothing.count()); // Output: 0
List<String> result = Stream.<String>empty()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(result); // Output: []That is the beauty of it. No null checks, no special branch, no extra if block. The pipeline runs and yields an empty list.
Java 9 added Stream.ofNullable. Give it a value and you get a stream of one element. Give it null and you get an empty stream.
// Java 9 and later
String maybe = null;
System.out.println(Stream.ofNullable(maybe).count()); // Output: 0
System.out.println(Stream.ofNullable("hello").count()); // Output: 1It really pays off inside a flatMap. Rows that lack a value quietly drop out, instead of poisoning the pipeline with nulls.
Map<String, String> config = new HashMap<>();
config.put("host", "localhost");
List<String> found = Stream.of("host", "port")
.flatMap(key -> Stream.ofNullable(config.get(key)))
.collect(Collectors.toList());
System.out.println(found); // Output: [localhost]If your method returns a Stream, return an empty one for the empty case. A null return forces every caller to guard against it.
// Please do not do this
Stream<String> badLookup(List<String> items) {
if (items == null) return null;
return items.stream();
}
// Much better
Stream<String> goodLookup(List<String> items) {
return items == null ? Stream.empty() : items.stream();
}The second version keeps caller code flat. Chain straight off it and stop worrying.
IntStream, LongStream and DoubleStream carry raw numbers. No Integer wrappers, no boxing, and a handful of maths methods for free.
Two factory methods build number ranges. range leaves out the upper bound, while rangeClosed keeps it.
IntStream.range(1, 5).forEach(n -> System.out.print(n + " ")); // Output: 1 2 3 4 System.out.println(); IntStream.rangeClosed(1, 5).forEach(n -> System.out.print(n + " ")); // Output: 1 2 3 4 5
Off-by-one errors love this pair. My habit: range for array indexes, rangeClosed for anything a human will read.
String[] names = {"Riya", "Arjun", "Meera"};
IntStream.range(0, names.length)
.forEach(i -> System.out.println(i + ": " + names[i]));
// Output:
// 0: Riya
// 1: Arjun
// 2: MeeraThat pattern solves a classic annoyance. Streams hide the index, so when you truly need it, stream the indexes instead of the values.
The old Random class picked up stream methods in Java 8. ints, longs and doubles all return primitive streams.
Random random = new Random();
// 5 numbers, from 1 up to but not including 50
random.ints(5, 1, 50)
.forEach(n -> System.out.print(n + " "));
// Output: for example 12 47 3 28 9Three arguments: how many, the lowest value, and the ceiling. That ceiling stays exclusive, matching IntStream.range.
Skip the count argument and the stream runs forever, so limit comes back into play.
Primitive streams cannot feed Collectors.toList() directly. Call boxed() first and each int turns into an Integer.
List<Integer> firstFive = IntStream.rangeClosed(1, 5)
.boxed()
.collect(Collectors.toList());
System.out.println(firstFive); // Output: [1, 2, 3, 4, 5]Travel the other way with mapToInt. Both directions come up constantly once you start mixing collections and maths.
List<Integer> prices = Arrays.asList(120, 340, 80);
int total = prices.stream()
.mapToInt(Integer::intValue)
.sum();
System.out.println(total); // Output: 540Beyond the obvious factories, plenty of JDK classes quietly hand out streams. These five come up in real work again and again.
Every CharSequence carries a chars() method since Java 8. It returns an IntStream of character codes, not a stream of characters.
long vowels = "programming".chars()
.filter(c -> "aeiou".indexOf(c) >= 0)
.count();
System.out.println(vowels); // Output: 3
"abc".chars()
.mapToObj(c -> (char) c)
.forEach(System.out::println);
// Output:
// a
// b
// cThose codes surprise everyone once. Print a character stream without mapToObj and you see numbers such as 97 and 98.
Java 11 added String.lines(), which splits on line breaks and gives a proper Stream<String>.
// Java 11 and later String text = "one\ntwo\nthree"; System.out.println(text.lines().count()); // Output: 3
Pattern.splitAsStream splits text without building the intermediate array that String.split creates.
String csv = "pune,delhi,kochi";
Pattern.compile(",")
.splitAsStream(csv)
.map(String::trim)
.forEach(System.out::println);
// Output:
// pune
// delhi
// kochiOn a huge string this saves memory, because elements arrive lazily. On a short one, plain split reads better. Pick by size.
Files.lines streams a text file line by line. Memory stays flat even on a large file, since it never loads the whole thing.
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
lines.filter(line -> !line.isEmpty())
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}Look at the try-with-resources block. A file stream holds an open handle, so it needs closing. This stands out as the one stream type you must never leave dangling.
Most streams need nothing of the sort. A list stream holds no resource, so wrapping it in try-with-resources only adds noise.
A BufferedReader offers the same deal through its lines() method. Same rule applies: close the reader, and the stream over it stops with the reader.
Stream.concat glues two streams together, in order, and returns a third one.
Stream<String> morning = Stream.of("tea", "toast");
Stream<String> evening = Stream.of("coffee");
Stream.concat(morning, evening)
.forEach(System.out::println);
// Output:
// tea
// toast
// coffeeBoth inputs count as consumed afterwards. Touch morning again and Java throws an IllegalStateException.
For three or more streams, nest the calls or switch to Stream.of(a, b, c).flatMap(s -> s). Deep nesting of concat hurts performance and readability alike.
Sometimes you add elements one by one, based on conditions. Stream.builder() handles that without a temporary list.
Stream.Builder<String> builder = Stream.builder();
builder.add("always");
boolean includeExtra = true;
if (includeExtra) {
builder.add("sometimes");
}
builder.build().forEach(System.out::println);
// Output:
// always
// sometimesOne rule governs the builder: after build(), the door shuts. Another add call throws an IllegalStateException.
Honestly, I reach for a list plus stream() more often. The builder wins when the stream feeds straight into a return statement.
Ten options sound like a lot. In daily work the choice usually takes two seconds.
| What you have | Use this | Result type |
|---|---|---|
| A List, Set or Queue | collection.stream() |
Stream<T> |
| A Map | map.entrySet().stream() |
Stream<Entry<K,V>> |
| An object array | Arrays.stream(arr) |
Stream<T> |
| An int, long or double array | Arrays.stream(arr) |
IntStream and friends |
| A few loose values | Stream.of(a, b, c) |
Stream<T> |
| A counted number range | IntStream.range(a, b) |
IntStream |
| A sequence built step by step | Stream.iterate(seed, f) |
Infinite Stream<T> |
| Independent generated values | Stream.generate(supplier) |
Infinite Stream<T> |
| Nothing at all | Stream.empty() |
Empty Stream<T> |
| A value that might be null | Stream.ofNullable(v) |
Zero or one element |
| A text file | Files.lines(path) |
Stream<String>, needs closing |
Five habits keep stream code clean and predictable.
Arrays.stream beats Stream.of thereStream.empty() rather than null, alwaysThese six catch nearly everyone once. Recognise them now and you skip a frustrating afternoon later.
A stream runs exactly once. Store it in a variable, use it twice, and the second call throws.
Stream<String> s = Stream.of("a", "b", "c");
System.out.println(s.count()); // Output: 3
System.out.println(s.count());
// Throws IllegalStateException:
// stream has already been operated upon or closedThe fix takes one line. Create a fresh stream each time, or better, store the list and call stream() again.
An infinite source without a brake will hang your program. No exception, no stack trace, just a frozen console.
// Never finishes Stream.iterate(1, n -> n + 1).forEach(System.out::println); // Finishes instantly Stream.iterate(1, n -> n + 1).limit(5).forEach(System.out::println);
Watch out for a subtle version of this. Putting filter before limit is safe, but a sorted() call on an infinite stream hangs, because sorting must see every element first.
Intermediate operations only describe work. Until a terminal operation arrives, nothing executes at all.
Stream.of("a", "b")
.map(v -> {
System.out.println("mapping " + v);
return v.toUpperCase();
});
// Output: nothing at allThe lambda never runs. Add .count() or .forEach(...) at the end and both lines appear immediately.
Section 4.3 covered this trap, and it earns a second mention. Stream.of(intArray) compiles happily and produces a single-element stream.
If a count comes back as 1 when you expected 50, check the array type. That symptom points straight at this bug.
Files.lines opens a file handle. Skip the try-with-resources block and the handle leaks, one per call.
On a laptop you might never notice. On a server processing thousands of files, the process eventually dies with “too many open files”.
A stream reads its source while the terminal operation runs. Modify that collection during the run and you get a ConcurrentModificationException.
List<String> items = new ArrayList<>(Arrays.asList("a", "b"));
items.stream().forEach(v -> items.add(v + "!"));
// Throws ConcurrentModificationExceptionCollect into a new list instead. Building a fresh result beats mutating the thing you are reading.
Time to put several sources into one small program. We will build a tiny sales report using four different creation methods.
Two data shapes turn up here. Daily sales arrive as a primitive array, and a couple of manual adjustments arrive as loose values.
int[] dailySales = {1200, 950, 1730, 400, 2210, 1600, 890};
List<String> regions = Arrays.asList("North", "South", "East");Each block below uses a different source. Read the comment above each one before the code.
public class SalesReport {
public static void main(String[] args) {
int[] dailySales = {1200, 950, 1730, 400, 2210, 1600, 890};
List<String> regions = Arrays.asList("North", "South", "East");
// 1. Primitive array -> IntStream
int total = Arrays.stream(dailySales).sum();
double average = Arrays.stream(dailySales).average().getAsDouble();
System.out.println("Total: " + total);
System.out.printf("Average: %.2f%n", average);
// 2. Number range -> day numbers with their sales
IntStream.range(0, dailySales.length)
.filter(i -> dailySales[i] > 1500)
.forEach(i -> System.out.println("Day " + (i + 1) + " was strong: " + dailySales[i]));
// 3. Collection -> a stream of regions
String regionList = regions.stream()
.collect(Collectors.joining(", "));
System.out.println("Regions: " + regionList);
// 4. Loose values -> targets for the next week
Stream.of(1500, 1800, 2000)
.map(t -> "Target: " + t)
.forEach(System.out::println);
}
}Run it and the console shows this:
Total: 8980 Average: 1282.86 Day 3 was strong: 1730 Day 5 was strong: 2210 Day 6 was strong: 1600 Regions: North, South, East Target: 1500 Target: 1800 Target: 2000
Four sources, four shapes of data, one readable program. Notice that no loop counter or index variable appears anywhere except inside the range.
Also notice how each pipeline creates its own stream. Nothing sits in a field waiting to go stale.
From here, the natural next steps are trimming elements with filter, reshaping them with map, and gathering results with the Collectors class.
A: The common routes are collection.stream() for any Collection, Arrays.stream() for arrays, Stream.of() for loose values, Stream.iterate() and Stream.generate() for infinite sequences, IntStream.range() for number ranges, and Stream.empty() for the empty case. Files.lines(), String.chars() and Pattern.splitAsStream() cover text sources.
A: On an object array both behave the same way and spread the elements. On a primitive array they differ sharply. Arrays.stream(intArray) returns an IntStream over every number, while Stream.of(intArray) returns a Stream of one element, and that element holds the whole array.
A: Stream.iterate takes a seed and a function that turns each value into the next, so elements form an ordered sequence. Stream.generate takes a Supplier that produces every element independently, with no ordering guarantee. Use iterate for counters and dates, and generate for random or freshly built values.
A: The two-argument form of iterate never ends. A terminal operation such as forEach keeps pulling elements forever. Add limit(n) or a short-circuiting operation such as findFirst or anyMatch. On Java 9 and later, the three-argument iterate carries its own stop condition.
A: Not directly, because Map does not extend Collection. Stream one of its three views instead: keySet() for keys, values() for values, or entrySet() for both together. The entrySet() route works best when a key and its value belong together in the result.
A: Each stream supports a single terminal operation. Calling a second one gives the message “stream has already been operated upon or closed”. Create a new stream from the source each time, or keep the source in a list and call stream() again when you need it.
A: IntStream.range(1, 5) produces 1, 2, 3 and 4, leaving out the upper bound. IntStream.rangeClosed(1, 5) produces 1, 2, 3, 4 and 5. Use range for array indexes, because the length lines up naturally, and rangeClosed when a person will read the numbers.
A: No. The stream reads elements from the source as the pipeline runs, and the source stays untouched. Sorting or filtering a stream never changes the underlying list. If you want a modified copy, collect the pipeline result into a new collection.
A: Choose IntStream, LongStream or DoubleStream whenever you work with raw numbers. They skip wrapper objects, which saves memory and time, and they offer sum, average, max and summaryStatistics directly. Call boxed() when you need to collect the values into a List of Integer.
A: Almost never. Streams over collections, arrays and plain values hold no resource, so the garbage collector handles them. File-backed streams differ. Wrap Files.lines() in try-with-resources, otherwise the file handle stays open and a long-running server eventually runs out of handles.
Let us wrap up what we covered. A stream carries elements past a set of operations. It stores nothing, changes nothing, and runs exactly once.
Collections give you a stream through stream(). Arrays go through Arrays.stream, and loose values through Stream.of. Those three handle most days at work.
For values you build rather than store, Stream.iterate follows a sequence and Stream.generate calls a supplier. Both run forever, so pair them with limit every single time.
Number work belongs to IntStream and its siblings, especially through range and rangeClosed. Empty cases belong to Stream.empty(), never to null.
Keep the six pitfalls in mind and the rest of the Stream API opens up smoothly. Reuse throws, infinite sources hang, and lazy pipelines wait for a terminal operation.
Now open your editor and try each factory method once. Break one on purpose, read the error, and the whole model clicks into place.