1 of 23

ADVANCED STREAM FEATURES AND REGULAR EXPRESSIONS

UNIT 5

2 of 23

Importance and Use Cases of Advanced Stream Features

  • Java’s Stream API revolutionized data processing by enabling declarative, functional-style operations on collections.
  • While basic features like map(), filter(), and forEach() are widely used, advanced stream features provide powerful, expressive, and performance-efficient ways to handle complex data operations.
  • Why Are Advanced Stream Features Important?
    • Expressiveness:

Write compact, readable code to handle sophisticated transformations.

    • Parallelism and Performance:

Use parallel streams to take advantage of multi-core processors.

    • Custom Processing:

Group, partition, and reduce data beyond simple collection operations.

    • Pipeline Reusability:

Compose dynamic, modular data processing pipelines.

    • Error-Free Declarative Logic:

Avoid side effects, mutable state, and complex loops.

3 of 23

Key Concepts & Use Cases

  • collecting and grouping:
    • collecting: Gathering stream elements into a different form like a List, Set, Map, or a custom structure.
    • Grouping: Organizing stream elements into groups based on a classifier function, typically resulting in a Map<K, List<V>>.
    • Collectors and the collect() Method
      • The collect() method in Java streams is a terminal operation used to transform the elements of the stream into a different form. It uses the Collectors utility class.
      • List<String> names = Arrays.asList("John", "Jane", "Jack", "Jill");
      • List<String> result = names.stream().collect(Collectors.toList());

4 of 23

Collector

Description

toList()

Collect elements into a List

toSet()

Collect into a Set

toMap()

Collect into a Map with key-value pairs

joining()

Concatenate elements into a single String

counting()

Count the number of elements

summarizingInt()

Collect statistics (count, sum, min, avg, max)

groupingBy()

Group elements by a classifier

partitioningBy()

Partition elements by a predicate

5 of 23

  • Grouping Using Collectors.groupingBy()
    • Map<String, List<String>> grouped = Stream.of("apple", "banana", "apricot", "blueberry", "avocado") .
    • collect(Collectors.groupingBy(s -> s.substring(0, 1)));
    • System.out.println(grouped);
    • Output : { a=[apple, apricot, avocado], b=[banana, blueberry]}
  • Partitioning:
    • partitioning is a special case of grouping by a boolean condition (true/false):
    • Map<Boolean, List<String>> partitioned = Stream.of("apple", "banana", "apricot", "blueberry", "avocado")
    • collect(Collectors.partitioningBy(s -> s.startsWith("a")));
    • Output: { true=[apple, apricot, avocado], false=[banana, blueberry]}

6 of 23

Custom Streams

  • In Java, Custom Streams refer to user-defined or manually created streams, rather than using built-in ones like Stream.of(), Arrays.stream(), or Collection.stream().
  • Custom streams are useful when:
    • You want to generate elements on the fly
    • You have a custom data source
    • You need more control over how and when stream elements are produced
  • Ways to Create Custom Streams:
    • Using Stream.generate() – For infinite streams
    • Using Stream.iterate() – For iterative values
    • Using Stream.builder() – Manually add elements
    • Using a Custom Data Source with StreamSupport

7 of 23

Stream.generate()

  • Stream.generate(Supplier<T> supplier) is a static method from the Stream interface used to create an infinite stream of values provided by a Supplier.
  • This stream doesn't have a natural end — it keeps producing values until you explicitly limit it.
  • static <T> Stream<T> generate(Supplier<T> s)
  • Supplier<T>: A functional interface that supplies a value of type T each time it’s called.
  • Returns: An infinite sequential unordered stream where each element is generated by the Supplier. (StreamGenerateDemo.java)

8 of 23

Stream.iterate()

  • Stream.iterate() is used to generate a sequential stream of values, where each value is computed from the previous one, typically forming a mathematical or logical progression.
  • static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
  • seed: initial element
  • f: function to apply repeatedly
  • Creates an infinite stream
  • static <T> Stream<T> iterate(T seed, Predicate<T> hasNext, UnaryOperator<T> next)
  • Stops when hasNext returns false
  • Allows for finite or bounded iteration (StreamIterateDemo.java)

9 of 23

Stream.builder()

  • The Stream.builder() is a mutable stream builder in Java that allows you to manually add elements to a stream.
  • Unlike the other stream creation methods (Stream.of() or Stream.generate()), which generate streams directly, Stream.builder() provides more flexibility by allowing manual construction of the stream.
  • How Does Stream.builder() Work?
    • Stream.builder() creates an empty stream builder that allows elements to be added one by one, and then you can build the stream from those elements.
    • This is useful when you have data to be added dynamically or in situations where the stream elements are computed during the stream construction.

10 of 23

  • Key Methods:
    • builder(): Creates a new stream builder.
    • add(T t): Adds elements to the builder.
    • build(): Creates the stream from the added elements.

  • Stream<T> stream = Stream.<T>builder() .add(element1) .add(element2) .add(element3) .build();
  • Stream.<T>builder() creates an empty builder.
  • .add(element) adds elements to the stream.
  • .build() converts the builder into a final stream. (StreamBuilderExample.java)

11 of 23

  • Filtering and Processing Elements Before Adding:
    • You can add some logic to filter or process the elements before adding them to the stream. (StreamBuilderExampled.java)
  • Advantages of Stream.builder()
    • Flexibility: You can manually control the data being added to the stream, making it ideal for dynamic data sources.
    • Performance: Efficient for constructing streams with a fixed or dynamic size.
    • Clearer Construction: Allows for a more explicit creation of streams, particularly useful in complex scenarios where elements may require computation or conditional checks before adding them to the stream.

12 of 23

FlatMapping

  • flatMap is an intermediate operation that flattens a stream of collections into a single stream.
  • It’s useful when you have a Stream of Collections or Stream of Arrays, and you want to combine them into a single stream.
  • flatMap vs map
    • map: Transforms each element in the stream into another element. For example, it turns a Stream<String> into a Stream<Integer>.
    • flatMap: Transforms each element into a Stream<T> and then flattens all the streams into one.
    • It is often used when the transformation results in a collection or stream of data.
  • Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper); (StreamFlatMapExample.java)
  • Function<? super T, ? extends Stream<? extends R>> mapper
  • This is a function that takes each element of type T from the stream and transforms it into a stream of type R.
  • The flatMap operation then flattens the stream of streams into a single stream of R elements.
  • T is the type of elements in the original stream.
  • R is the type of elements in the resulting flattened stream.
  • The mapper function returns a Stream<? extends R>, which means each element from the input stream (T) is mapped to another stream, and then these individual streams are merged into one.

13 of 23

Chaining Stream Operations

  • Stream operations are often chained together to perform complex transformations on data. Since streams are lazily evaluated, chaining operations can result in efficient and powerful data transformations.
  • Chaining Stream Operations Example:
    • You can chain multiple operations like filter, map, sorted, and collect together to produce the desired result. (StreamChainingExample.java)

14 of 23

Stream Peeking (peek)

  • peek is an intermediate operation that allows you to inspect the elements in the stream without modifying them.
  • It’s mostly used for debugging or logging purposes during stream processing.
  • Stream<T> peek(Consumer<? super T> action);
  • action: A consumer function that is applied to each element as it is processed by the stream.
  • peek does not alter the stream, it simply lets you inspect the elements as they are processed. (StreamPeekExample.java)

15 of 23

Introduction to Regular Expressions

  • A Regular Expression (Regex) is a sequence of characters that forms a search pattern. In Java, regex is used for pattern matching with strings.
  • Java provides the java.util.regex package, which includes:
    • Pattern – compiles regex into a pattern
    • Matcher – applies the pattern to a string
    • PatternSyntaxException – thrown for invalid regex (RegexExample.java)

16 of 23

Character Classes

  • Character classes match a group of characters. They're enclosed in square brackets []. (CharacterClassExample.java)

Pattern

Matches

[abc]

a, b, or c

[^abc]

any character except a, b, or c

[a-z]

any lowercase letter

[A-Z]

any uppercase letter

[0-9]

any digit

[a-zA-Z]

any letter

[a-zA-Z0-9]

any alphanumeric character

17 of 23

Quantifiers

  • Quantifiers define how many times a character or group can appear.
  • (QuantifierExample.java)

Symbol

Meaning

Example

Matches

?

0 or 1 time

a?

"", "a"

*

0 or more times

a*

"", "a", "aa", "aaa"

+

1 or more times

a+

"a", "aa", "aaa"

{n}

exactly n times

a{3}

"aaa"

{n,}

n or more times

a{2,}

"aa", "aaa", ...

{n,m}

between n and m times

a{2,4}

"aa", "aaa", "aaaa"

18 of 23

String Manipulation

  • String Manipulation refers to the process of accessing, modifying, analyzing, and transforming string data using a variety of built-in methods.
  • In Java, strings are immutable objects, meaning once a String object is created, it cannot be changed — but you can create new strings based on operations on existing ones.
  • Why is String Manipulation Important?
    • Processing user input
    • Formatting output
    • Parsing and analyzing text (e.g., from files or web)
    • Implementing business logic like validation and transformation
    • Handling data in web development, databases, APIs

19 of 23

Operation

Method

Length

length()

Character at index

charAt(int)

Substring

substring(int, int)

Replace

replace(old, new)

Split

split(regex)

Case conversion

toUpperCase(), toLowerCase()

Trimming

trim()

Comparison

equals(), compareTo()

20 of 23

String Replacement

  • String replacement is the process of substituting one portion of a string with another.
  • Java provides multiple ways to replace characters or substrings in a String, which is essential for cleaning, transforming, or formatting text.
  • Why String Replacement?
    • String replacement is commonly used to:
      • Clean input (e.g., removing unwanted characters)
      • Standardize content (e.g., replacing tabs with spaces)
      • Format data (e.g., anonymizing sensitive info)
      • Implement business rules (e.g., replace currency symbols)

21 of 23

  • replace(char oldChar, char newChar)
    • Replaces all occurrences of one character with another.

String s = "banana";

String replaced = s.replace('a', 'o');

System.out.println(replaced); // "bonono"

  • replace(CharSequence target, CharSequence replacement)
    • Replaces all occurrences of a literal string.

String s = "The cat sat on the mat.";

String replaced = s.replace("cat", "dog");

System.out.println(replaced); // "The dog sat on the mat."

22 of 23

  • replaceAll(String regex, String replacement)
    • Replaces all substrings matching the regular expression with a new string.

String s = "Price: $100, $200, and $300";

String replaced = s.replaceAll("\\$\\d+", "[PRICE]");

System.out.println(replaced); // "Price: [PRICE], [PRICE], and [PRICE]"

  • replaceFirst(String regex, String replacement) (StringReplacementDemo.java)
    • Replaces only the first substring that matches the regex.

String s = "abc123xyz123";

String replaced = s.replaceFirst("\\d+", "###");

System.out.println(replaced); // "abc###xyz123"

23 of 23

Replacing with Backreferences

  • Backreferences in Java string replacement allow you to reuse matched parts of a regex when forming a replacement string.
  • This is extremely useful when performing pattern-aware substitutions—such as swapping words, reordering groups, or formatting data.
  • What Are Backreferences?
    • In regular expressions, parentheses () create capturing groups.
    • When used with String.replaceAll() or String.replaceFirst(), you can reference these groups in the replacement string using:
    • \1 for the first group
    • \2 for the second group