Real-World Java Stream API: From Theory to Practical Code

This article walks through Java Stream fundamentals, showing how to replace verbose loops with concise Stream pipelines, compares map and flatMap, explains intermediate and terminal operations such as filter, sorted, distinct, limit, peek, foreach, and collect, demonstrates parallel streams, highlights common pitfalls, and summarizes the advantages and disadvantages of using Stream APIs in everyday development.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Real-World Java Stream API: From Theory to Practical Code

Basic Stream Usage

Traditional loop implementation for extracting the top three longest words (length > 5) from a sentence:

public List<String> sortGetTop3LongWords(@NotNull String sentence) {
    // split sentence
    String[] words = sentence.split(" ");
    List<String> wordList = new ArrayList<>();
    // filter by length
    for (String word : words) {
        if (word.length() > 5) {
            wordList.add(word);
        }
    }
    // sort descending by length
    wordList.sort((o1, o2) -> o2.length() - o1.length());
    // keep only top 3
    if (wordList.size() > 3) {
        wordList = wordList.subList(0, 3);
    }
    return wordList;
}

Equivalent Stream version, reducing code size by almost half:

public List<String> sortGetTop3LongWordsByStream(@NotNull String sentence) {
    return Arrays.stream(sentence.split(" "))
        .filter(word -> word.length() > 5)
        .sorted((o1, o2) -> o2.length() - o1.length())
        .limit(3)
        .collect(Collectors.toList());
}

Stream Pipeline Types

Creation : Build a Stream from arrays, collections, maps, etc.

Intermediate Operations : Transform or filter data; can be chained.

Terminal Operations : Produce a result or side‑effect and close the pipeline.

Common Intermediate Operations

filter() : Keep elements that satisfy a predicate.

map() : One‑to‑one element conversion.

flatMap() : One‑to‑many conversion, flattening nested streams.

limit() : Truncate the stream to a given number of elements.

skip() : Discard the first N elements.

distinct() : Remove duplicate elements.

sorted() : Order elements according to a comparator.

peek() : Perform an action on each element without consuming the stream (intermediate).

Common Terminal Operations

collect() : Gather the stream into a collection, map, string, or statistical summary.

count() : Return the number of elements.

max()/min() : Return the maximum or minimum element.

findFirst()/findAny() : Return the first (or any) matching element.

anyMatch()/allMatch()/noneMatch() : Return a boolean based on predicate evaluation.

forEach() : Consume each element with a side‑effect (terminal).

map vs flatMap

Both convert existing elements, but map() is one‑to‑one while flatMap() can produce zero, one, or many elements per input.

Example of map() converting a list of string IDs to objects:

List<String> ids = Arrays.asList("205", "105", "308", "469", "627", "193", "111");
List<NormalOfferModel> results = ids.stream()
    .map(id -> {
        NormalOfferModel model = new NormalOfferModel();
        model.setCate1LevelId(id);
        return model;
    })
    .collect(Collectors.toList());
System.out.println(results);

Result shows a one‑to‑one mapping, preserving the element count.

Example of flatMap() extracting all words from a list of sentences:

List<String> sentences = Arrays.asList("hello world", "Hello Price Info The First Version");
List<String> words = sentences.stream()
    .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
    .collect(Collectors.toList());
System.out.println(words);

Result: [hello, world, Hello, Price, Info, The, First, Version].

peek vs forEach

peek()

is an intermediate operation; it runs only when a subsequent terminal operation is present. forEach() is terminal and executes immediately.

public void testPeekAndForeach() {
    List<String> sentences = Arrays.asList("hello world", "Jia Gou Wu Dao");
    System.out.println("----before peek----");
    sentences.stream().peek(s -> System.out.println(s));
    System.out.println("----after peek----");
    System.out.println("----before foreach----");
    sentences.stream().forEach(s -> System.out.println(s));
    System.out.println("----after foreach----");
    System.out.println("----before peek and count----");
    sentences.stream().peek(s -> System.out.println(s)).count();
    System.out.println("----after peek and count----");
}

Output shows that peek() alone does not execute, but executes when followed by count(), whereas forEach() always runs.

Combined Intermediate Operations

Chaining several intermediate operations before collecting:

public void testGetTargetUsers() {
    List<String> ids = Arrays.asList("205","10","308","49","627","193","111","193");
    List<OfferModel> results = ids.stream()
        .filter(s -> s.length() > 2)
        .distinct()
        .map(Integer::valueOf)
        .sorted(Comparator.comparingInt(o -> o))
        .limit(3)
        .map(id -> new OfferModel(id))
        .collect(Collectors.toList());
    System.out.println(results);
}

Filter out short strings.

Remove duplicates.

Convert to integers.

Sort ascending.

Take the first three.

Map to OfferModel objects.

Collect into a list.

Result: [OfferModel{id=111}, OfferModel{id=193}, OfferModel{id=205}].

Simple Terminal Operations

public void testSimpleStopOptions() {
    List<String> ids = Arrays.asList("205","10","308","49","627","193","111","193");
    System.out.println(ids.stream().filter(s -> s.length() > 2).count());
    System.out.println(ids.stream().filter(s -> s.length() > 2).anyMatch("205"::equals));
    ids.stream().filter(s -> s.length() > 2)
        .findFirst()
        .ifPresent(s -> System.out.println("findFirst:" + s));
}

Output: 6, true, findFirst:205.

Stream Reuse Pitfall

public void testHandleStreamAfterClosed() {
    List<String> ids = Arrays.asList("205","10","308","49","627","193","111","193");
    Stream<String> stream = ids.stream().filter(s -> s.length() > 2);
    System.out.println(stream.count());
    try {
        System.out.println(stream.anyMatch("205"::equals));
    } catch (Exception e) {
        e.printStackTrace();
        System.out.println(e.toString());
    }
}

Second operation throws

IllegalStateException: stream has already been operated upon or closed

.

Collect Variants

// collect to List
List<NormalOfferModel> list = normalOfferModelList.stream()
    .filter(o -> o.getCate1LevelId().equals("11"))
    .collect(Collectors.toList());
// collect to Set
Set<NormalOfferModel> set = normalOfferModelList.stream()
    .filter(o -> o.getCate1LevelId().equals("22"))
    .collect(Collectors.toSet());
// collect to Map (key = id)
Map<String, NormalOfferModel> map = normalOfferModelList.stream()
    .filter(o -> o.getCate1LevelId().equals("33"))
    .collect(Collectors.toMap(NormalOfferModel::getCate1LevelId, Function.identity(), (k1, k2) -> k2));

String Joining with Collectors

// loop version
StringBuilder sb = new StringBuilder();
for (String id : ids) { sb.append(id).append(','); }
sb.deleteCharAt(sb.length() - 1);
// Stream version
String joined = ids.stream().collect(Collectors.joining(","));

Both produce the same comma‑separated string; the Stream version is more concise.

Parallel Stream

Parallel streams split the pipeline into multiple sub‑tasks that run on the ForkJoinPool.commonPool(), leveraging multiple CPU cores. The pool uses work‑stealing to balance tasks.

Key constraints when using parallelStream():

The forEach() operation must be thread‑safe.

Avoid the default global pool for heavy or blocking tasks; create a custom ForkJoinPool if needed.

Do not perform time‑consuming or I/O‑heavy work inside a parallel stream.

Common Pitfalls

Parallel streams share the global ForkJoinPool, which can cause contention with other parts of the application.

ThreadLocal values are not propagated to worker threads, leading to missing context.

When converting to a map, duplicate keys cause IllegalStateException unless a merge function is supplied.

Advantages

Code is more concise and declarative, making intent clearer.

Intermediate operations are decoupled from upstream and downstream logic.

Parallel streams can improve performance on multi‑core hardware.

Lazy evaluation means intermediate steps execute only when a terminal operation is invoked, avoiding unnecessary work.

Disadvantages

Debugging can be harder because the pipeline is not executed step‑by‑step.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaFunctional ProgrammingmapStream APIflatMapParallel StreamCollect
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.