Stream API Fundamentals in Java

Master the Stream API including object and primitive streams, stream creation, intermediate and terminal operations, filtering, transforming, processing, and sorting for the OCP 21 exam.

Table of Contents

1. Stream API Overview

The Stream API (introduced in Java 8) provides a functional approach to processing collections of data. Streams enable declarative, chainable operations on data sequences.

1.1 Key Concepts

  • Stream: A sequence of elements supporting sequential and parallel aggregate operations
  • Source: Collections, arrays, I/O channels, or generator functions
  • Intermediate Operations: Return a stream, can be chained, lazy evaluation
  • Terminal Operations: Produce a result or side effect, trigger stream processing
  • Lazy Evaluation: Operations are not executed until a terminal operation is called

1.2 Stream vs Collection

Aspect Collection Stream
Storage Stores data Does not store data
Operations External iteration Internal iteration
Evaluation Eager Lazy
Reusability Can be reused Single-use (consumed after terminal operation)

2. Creating Streams

2.1 From Collections

Example:
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream1 = list.stream();

Set<Integer> set = Set.of(1, 2, 3);
Stream<Integer> stream2 = set.stream();

Map<String, Integer> map = Map.of("a", 1, "b", 2);
Stream<String> keyStream = map.keySet().stream();
Stream<Integer> valueStream = map.values().stream();
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();

2.2 From Arrays

Example:
String[] array = {"a", "b", "c"};
Stream<String> stream1 = Arrays.stream(array);
Stream<String> stream2 = Arrays.stream(array, 1, 3);  // From index 1 to 3 (exclusive)

int[] intArray = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(intArray);

2.3 Static Factory Methods

Example:
// Stream.of() - from values
Stream<String> stream1 = Stream.of("a", "b", "c");
Stream<Integer> stream2 = Stream.of(1, 2, 3);

// Stream.empty() - empty stream
Stream<String> empty = Stream.empty();

// Stream.generate() - infinite stream
Stream<Double> random = Stream.generate(Math::random);
Stream<Integer> ones = Stream.generate(() -> 1);

// Stream.iterate() - infinite stream with seed and function
Stream<Integer> numbers = Stream.iterate(0, n -> n + 1);
Stream<Integer> evens = Stream.iterate(0, n -> n + 2);

// Stream.iterate() with predicate (Java 9+)
Stream<Integer> limited = Stream.iterate(0, n -> n < 10, n -> n + 1);

3. Intermediate Operations

Intermediate operations return a stream and are lazy. They are not executed until a terminal operation is invoked.

3.1 Filtering

Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// filter() - keep elements matching predicate
List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());  // [2, 4, 6, 8, 10]

// distinct() - remove duplicates
List<Integer> unique = Arrays.asList(1, 2, 2, 3, 3, 3).stream()
    .distinct()
    .collect(Collectors.toList());  // [1, 2, 3]

// limit() - limit number of elements
List<Integer> first5 = numbers.stream()
    .limit(5)
    .collect(Collectors.toList());  // [1, 2, 3, 4, 5]

// skip() - skip first n elements
List<Integer> skip5 = numbers.stream()
    .skip(5)
    .collect(Collectors.toList());  // [6, 7, 8, 9, 10]

3.2 Mapping

Example:
List<String> words = Arrays.asList("hello", "world", "java");

// map() - transform each element
List<String> upper = words.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());  // [HELLO, WORLD, JAVA]

List<Integer> lengths = words.stream()
    .map(String::length)
    .collect(Collectors.toList());  // [5, 5, 4]

// flatMap() - flatten nested structures
List<List<Integer>> nested = Arrays.asList(
    Arrays.asList(1, 2),
    Arrays.asList(3, 4),
    Arrays.asList(5, 6)
);
List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());  // [1, 2, 3, 4, 5, 6]

3.3 Sorting

Example:
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");

// sorted() - natural order (requires Comparable)
List<String> sorted = names.stream()
    .sorted()
    .collect(Collectors.toList());  // [Alice, Bob, Charlie]

// sorted(Comparator) - custom order
List<String> reverse = names.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList());  // [Charlie, Bob, Alice]

List<String> byLength = names.stream()
    .sorted(Comparator.comparing(String::length))
    .collect(Collectors.toList());  // [Bob, Alice, Charlie]

4. Terminal Operations

Terminal operations produce a result or side effect and trigger the execution of the stream pipeline.

4.1 Collecting Results

Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// collect() - collect to collection
List<String> list = names.stream()
    .filter(s -> s.length() > 3)
    .collect(Collectors.toList());

Set<String> set = names.stream()
    .collect(Collectors.toSet());

// collect() - to specific collection
LinkedList<String> linkedList = names.stream()
    .collect(Collectors.toCollection(LinkedList::new));

4.2 Aggregation Operations

Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// count() - count elements
long count = numbers.stream().count();  // 5

// min() - find minimum
Optional<Integer> min = numbers.stream().min(Integer::compareTo);

// max() - find maximum
Optional<Integer> max = numbers.stream().max(Integer::compareTo);

// findFirst() - first element
Optional<Integer> first = numbers.stream().findFirst();

// findAny() - any element (useful for parallel streams)
Optional<Integer> any = numbers.stream().findAny();

// anyMatch() - any element matches predicate
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);  // true

// allMatch() - all elements match predicate
boolean allPositive = numbers.stream().allMatch(n -> n > 0);  // true

// noneMatch() - no elements match predicate
boolean noNegative = numbers.stream().noneMatch(n -> n < 0);  // true

4.3 Reduction Operations

Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// reduce() - with identity and accumulator
int sum1 = numbers.stream()
    .reduce(0, (a, b) -> a + b);  // 15

// reduce() - with accumulator only (returns Optional)
Optional<Integer> sum2 = numbers.stream()
    .reduce((a, b) -> a + b);  // Optional[15]

// reduce() - with identity, accumulator, and combiner (for parallel)
int sum3 = numbers.parallelStream()
    .reduce(0, (a, b) -> a + b, (a, b) -> a + b);  // 15

// Specialized reduction operations
int sum4 = numbers.stream().mapToInt(Integer::intValue).sum();  // 15
OptionalInt max = numbers.stream().mapToInt(Integer::intValue).max();

4.4 Iteration

Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// forEach() - iterate and perform side effect
names.stream()
    .forEach(System.out::println);

// forEachOrdered() - maintain order (important for parallel streams)
names.parallelStream()
    .forEachOrdered(System.out::println);

5. Primitive Streams

Java provides specialized stream types for primitives to avoid boxing overhead: IntStream, LongStream, and DoubleStream.

5.1 Creating Primitive Streams

Example:
// IntStream
IntStream intStream1 = IntStream.of(1, 2, 3, 4, 5);
IntStream intStream2 = IntStream.range(1, 6);      // 1 to 5 (exclusive)
IntStream intStream3 = IntStream.rangeClosed(1, 5); // 1 to 5 (inclusive)
IntStream intStream4 = Arrays.stream(new int[]{1, 2, 3});

// LongStream
LongStream longStream = LongStream.of(1L, 2L, 3L);
LongStream range = LongStream.range(1, 10);

// DoubleStream
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);

// Converting from object stream
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
IntStream intStream5 = numbers.stream().mapToInt(Integer::intValue);

5.2 Primitive Stream Operations

Example:
IntStream numbers = IntStream.of(1, 2, 3, 4, 5);

// Specialized terminal operations
int sum = numbers.sum();                    // 15
OptionalDouble avg = numbers.average();     // OptionalDouble[3.0]
OptionalInt min = numbers.min();            // OptionalInt[1]
OptionalInt max = numbers.max();            // OptionalInt[5]

// Converting back to object stream
Stream<Integer> boxed = numbers.boxed();

// Converting to other primitive streams
LongStream longStream = numbers.asLongStream();
DoubleStream doubleStream = numbers.asDoubleStream();

6. Stream Characteristics

6.1 Stream Properties

  • Lazy: Intermediate operations are not executed until terminal operation
  • Single-use: Streams can only be consumed once
  • Non-interfering: Source should not be modified during stream operations
  • Stateless: Operations should be stateless (no dependency on mutable state)

6.2 Common Pitfalls

Common Mistakes:
Stream<String> stream = Stream.of("a", "b", "c");

// WRONG: Reusing stream
stream.forEach(System.out::println);
stream.count();  // IllegalStateException - stream already consumed

// CORRECT: Create new stream for each operation
Stream.of("a", "b", "c").forEach(System.out::println);
Stream.of("a", "b", "c").count();

// WRONG: Modifying source during stream operation
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
list.stream()
    .filter(s -> {
        list.remove(s);  // ConcurrentModificationException
        return true;
    })
    .collect(Collectors.toList());

7. Exam Key Points

Critical Concepts for OCP 21 Exam:

  • Stream Creation: collection.stream(), Arrays.stream(), Stream.of()
  • Intermediate Operations: filter, map, flatMap, distinct, sorted, limit, skip
  • Terminal Operations: collect, forEach, reduce, count, min, max, findFirst, findAny
  • Lazy Evaluation: Intermediate operations don't execute until terminal operation
  • Single-use: Streams can only be consumed once
  • Primitive Streams: IntStream, LongStream, DoubleStream for performance
  • map() vs flatMap(): map transforms, flatMap flattens nested structures
  • Optional Return: min(), max(), findFirst() return Optional
  • Predicate Operations: anyMatch(), allMatch(), noneMatch() return boolean
  • Reduction: reduce() with identity, accumulator, and combiner
  • Collectors: toList(), toSet(), toCollection() for collecting results
  • Method References: Can be used in stream operations

Post a Comment

0 Comments