Table of Contents

Introduction to Streams in Java 8: Creating Streams from Values, Arrays and Collections

  • Last Updated: August 11, 2026
  • By: javahandson
  • Series
img

Introduction to Streams in Java 8: Creating Streams from Values, Arrays and Collections

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.

1. Introduction

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.

1.1 What This Article Covers

Here is the ground we cover:

  • What a stream actually holds, and what it does not
  • Building a stream from any collection, including maps
  • Loose values with Stream.of, and arrays with Arrays.stream
  • Endless streams through Stream.iterate and Stream.generate
  • Empty streams, null safety and Stream.ofNullable
  • Number ranges with IntStream.range and friends
  • Hidden stream sources: strings, files, patterns and builders
  • Six mistakes that bite almost everyone once
  • A full walkthrough plus ten interview questions

You do not need lambda expertise for this. A quick look at lambda expressions helps, but the samples stay small enough to follow either way.

2. What a Stream Really Is

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.

2.1 A Conveyor Belt, Not a Container

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.

2.2 The Three Parts of Every Pipeline

Every stream pipeline has exactly three parts. Learn these three words and the rest of the API falls into place.

  • Source – where elements come from. A list, an array, a file, a supplier
  • Intermediate operations – steps such as filter, map or sorted. Each returns a new stream
  • Terminal operation – the final step, such as collect, forEach or count

This 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: 1

2.3 Streams Never Touch Your Data

Creating 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.

2.4 Words You Will Keep Seeing

A few terms show up in every stream discussion. Here they are in plain English.

  • Lazy – nothing runs until a terminal operation asks for results
  • Infinite stream – a source that keeps producing values forever
  • Short-circuit – an operation such as limit or findFirst that stops early
  • Primitive streamIntStream, LongStream or DoubleStream, which avoid wrapper objects
  • Consumed – a stream that already ran and now refuses further work

3. Streams From Collections

This route covers maybe eighty percent of real code. You already have a list or a set, and you want a stream over it.

3.1 The stream() Method

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
// Kochi

The 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.

3.2 Sets, Queues and Everything Else

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
// stream

Order 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.

3.3 Getting a Stream From a Map

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 = 78

The 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.

3.4 A One-Line Jump to Parallel

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: 15

Do 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.

4. Stream.of for Loose Values

Sometimes you have no collection at all. You just have three or four values sitting in front of you.

4.1 Passing Values Straight In

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
// 30

Notice 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.

4.2 One Element, or None at All

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: 0

4.3 The Array Surprise

Now 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: 3

Hand 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: 3

Read 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.

5. Streams From Arrays

Arrays predate the Stream API by two decades, so the bridge lives in the Arrays helper class rather than on the array itself.

5.1 Arrays.stream in One Line

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
// PEAR

Arrays 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.

5.2 Slicing With Start and End

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
// 78

This 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.

5.3 Primitive Arrays Give Primitive Streams

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.4

Each call needs a fresh Arrays.stream(marks). Three statements, three streams. Section 12.1 explains why reuse fails.

5.4 Arrays Inside Arrays

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: 21

Flattening deserves its own discussion, and it has one. The flatMap guide walks through nested structures properly.

6. Stream.iterate for Step-by-Step Values

Every source so far needed existing data. The next two build values out of thin air.

6.1 The Two-Argument Form

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
// 32

Each 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-15

6.2 Always Put a Limit On It

Notice 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: 243

That pipeline never ends on paper. In practice it stops after seven values, because findFirst asks for exactly one.

6.3 The Three-Argument Form From Java 9

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
// 16

Compare 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.

6.4 A Fibonacci Sequence

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 13

Each 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.

7. Stream.generate for Supplier-Driven Values

Its sibling Stream.generate takes a Supplier instead. No seed, no previous value, just a function that produces something whenever asked.

7.1 The Same Value Again and Again

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
// pending

Like iterate, this source never stops by itself. The limit call remains compulsory.

7.2 Random Numbers and Fresh Objects

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 UUIDs

One 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.

7.3 iterate Versus generate

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.

8. Empty Streams and Null Safety

Empty cases sound boring until a NullPointerException wakes you at midnight. Two small factory methods prevent most of that pain.

8.1 Stream.empty

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.

8.2 Stream.ofNullable in Java 9

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: 1

It 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]

8.3 Never Return null From a Method

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.

9. Primitive Streams and Number Ranges

IntStream, LongStream and DoubleStream carry raw numbers. No Integer wrappers, no boxing, and a handful of maths methods for free.

9.1 IntStream.range and rangeClosed

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: Meera

That pattern solves a classic annoyance. Streams hide the index, so when you truly need it, stream the indexes instead of the values.

9.2 Random Numbers as a Stream

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 9

Three 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.

9.3 Boxing Back to Objects

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: 540

10. Streams Hiding in the JDK

Beyond the obvious factories, plenty of JDK classes quietly hand out streams. These five come up in real work again and again.

10.1 Characters and Lines From a String

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
// c

Those 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

10.2 Splitting Text With a Pattern

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
// kochi

On a huge string this saves memory, because elements arrive lazily. On a short one, plain split reads better. Pick by size.

10.3 Reading a File as a Stream

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.

10.4 Joining Two Streams With concat

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
// coffee

Both 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.

10.5 Stream.builder for Conditional Elements

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
// sometimes

One 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.

11. Which Creation Method Should You Pick?

Ten options sound like a lot. In daily work the choice usually takes two seconds.

11.1 A Quick Decision Table

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

11.2 Rules of Thumb

Five habits keep stream code clean and predictable.

  • Prefer the source closest to your data. Wrapping an array in a list first only adds a copy
  • Primitive data belongs in a primitive stream, so Arrays.stream beats Stream.of there
  • Every infinite source needs a brake in the same expression, not three lines later
  • Return Stream.empty() rather than null, always
  • Create the stream where you use it. A stored stream field invites reuse bugs

12. Common Mistakes and Pitfalls

These six catch nearly everyone once. Recognise them now and you skip a frustrating afternoon later.

12.1 Reusing a Stream

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 closed

The fix takes one line. Create a fresh stream each time, or better, store the list and call stream() again.

12.2 Forgetting the Limit

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.

12.3 Expecting Work Without a Terminal Operation

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 all

The lambda never runs. Add .count() or .forEach(...) at the end and both lines appear immediately.

12.4 Calling Stream.of on a Primitive Array

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.

12.5 Leaving a File Stream Open

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”.

12.6 Changing the Source Mid-Pipeline

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 ConcurrentModificationException

Collect into a new list instead. Building a fresh result beats mutating the thing you are reading.

13. A Practical Walkthrough

Time to put several sources into one small program. We will build a tiny sales report using four different creation methods.

13.1 The Data

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");

13.2 Building the Report

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);
    }
}

13.3 Reading the Output

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.

14. Interview Questions

Q: What are the main ways to create a stream in Java?

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.

Q: What is the difference between Stream.of and Arrays.stream?

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.

Q: What is the difference between Stream.iterate and Stream.generate?

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.

Q: Why does my program hang when I use Stream.iterate?

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.

Q: Can I create a stream from a Map?

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.

Q: Why do I get IllegalStateException on a stream?

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.

Q: What is the difference between range and rangeClosed in IntStream?

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.

Q: Does creating a stream copy my collection?

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.

Q: When should I use a primitive stream instead of Stream?

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.

Q: Do I need to close a stream after using it?

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.

15. Conclusion

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.

Further Reading

Leave a Comment