Java Streams - Revisions

4 minute read

What is a Stream?

  • A stream in Java is a sequence of data that takes input from Collections or IO Channels.
  • Streams don’t change the original data structure.
  • A Stream Pipeline is the operation (STREAM OPERATIONS) that run on a stream to produce a result.
  • Each intermediate operation is lazily executed and returns a stream as a result.
  • Terminal operations mark the end of the stream and return the result.
  • Finite Streams have a limit.
  • Infinite Streams are like sunrise/sunset cycles.

Important Notes

  • Re-stream a List each time: Don’t re-use a stream because “creating” a Stream just points at the existing data structure behind the scenes; it does not copy the data.
  • Do not support indexed access: findFirst() can give the top element, but not the second, third, or last element.
  • Simple syntax to build a List or array from a Stream.

Three Common Ways to Create a Stream

  1. From ArrayList
    List<Student> students = new ArrayList<>();
    Stream<Student> studentStream = students.stream();
    
  2. From Array of Objects (not array of primitives)
    Student[] students = {....};
    Stream<Student> studentStream = Stream.of(students).map().filter().other(); // No Terminal Operator
    
  3. From Individual Elements
    Student s1 = ;
    Student s2 = ;
       
    Stream<Student> studentStream = Stream.of(s1, s2, ...).map().filter().other(); // No Terminal Operator
    

Outputting Streams

Getting a List out of a Stream

List<SomeClass> list = someStream.map().collect(Collectors.toList());

Getting an Array out of a Stream

// Fill elements into an Array from a Stream
Student[] studentArray = someStream.map(someLambda).toArray(Student[]::new);
String[] strArray = stringStream.filter(...).map(...).toArray(String[]::new);

What Cannot be Done with Streams forEach

  1. Multiple Loops are not possible as forEach is a Terminal operation consuming a Stream.
  2. Local variable modification:
    list.stream().forEach(e -> total += e);
    
    • Do this with map and reduce.
    • Or use the built-in sum method of DoubleStream or IntStream.
  3. Cannot use break or return within forEach loop.

Stream Operations

Source

  • SOURCE: Where the stream comes from.

Sure! Here’s a completed and organized explanation of Intermediate Operations in Java Streams, including the ones you listed:


Intermediate Operations

INTERMEDIATE OPERATIONS: These transform a stream into another stream. Streams use lazy evaluation, meaning intermediate operations are not executed until a terminal operation is invoked.

Common Intermediate Operations:

  • filter(Predicate<T> predicate)
    Selects elements that match the given predicate.
    Example: stream.filter(x -> x > 10)

  • map(Function<T, R> mapper)
    Transforms each element using the provided function.
    Example: stream.map(String::toUpperCase)

  • flatMap(Function<T, Stream<R>> mapper)
    Flattens nested structures by mapping each element to a stream and then flattening the result.
    Example: stream.flatMap(list -> list.stream())

  • distinct()
    Removes duplicate elements from the stream (based on equals() method).
    Example: stream.distinct()

  • limit(long maxSize)
    Truncates the stream to contain no more than maxSize elements.
    Example: stream.limit(5)

  • peek(Consumer<T> action)
    Performs the given action on each element as they are consumed from the stream. Useful for debugging.
    Example: stream.peek(System.out::println)

  • sorted() / sorted(Comparator<T> comparator)
    Returns a stream with elements sorted in natural order or using a custom comparator.
    Example: stream.sorted() or stream.sorted(Comparator.reverseOrder())


Terminal Operations

These operations produce a result or a side-effect and consume the stream, making it unusable afterward. They trigger the processing of all intermediate operations.

Common Terminal Operations:

  • collect(Collector<T, A, R> collector)
    Performs a mutable reduction operation on the elements of the stream and returns a collection or another result container.
    Example: stream.collect(Collectors.toList())

  • forEach(Consumer<T> action)
    Performs the given action for each element of the stream.
    Example: stream.forEach(System.out::println)

  • reduce(BinaryOperator<T> accumulator)
    Reduces the stream to a single value using an associative accumulation function.
    Example: stream.reduce((a, b) -> a + b)

  • count()
    Returns the number of elements in the stream.
    Example: stream.count()

  • min(Comparator<T> comparator)
    Returns the minimum element of the stream according to the provided comparator, wrapped in an Optional.
    Example: stream.min(Comparator.naturalOrder())

  • max(Comparator<T> comparator)
    Returns the maximum element of the stream according to the provided comparator, wrapped in an Optional.
    Example: stream.max(Comparator.naturalOrder())

  • toArray()
    Returns an array containing the elements of the stream.
    Example: stream.toArray()

  • anyMatch(Predicate<T> predicate)
    Returns true if any elements of the stream match the provided predicate.
    Example: stream.anyMatch(x -> x > 10)

  • allMatch(Predicate<T> predicate)
    Returns true if all elements of the stream match the provided predicate.
    Example: stream.allMatch(x -> x > 0)

  • noneMatch(Predicate<T> predicate)
    Returns true if no elements of the stream match the provided predicate.
    Example: stream.noneMatch(x -> x < 0)

  • findFirst()
    Returns the first element of the stream, if present, wrapped in an Optional.
    Example: stream.findFirst()

  • findAny()
    Returns any element of the stream, useful in parallel streams, wrapped in an Optional.
    Example: stream.findAny()


Map vs FlatMap

Employee with Address Object

@Data
class Address {
    private String city;
}

Using map

Scenario: Transforming a list of Employee objects to a list of their names.

class Employee {
    private Address address;//One Address Object
}
public void testMain(List<Employee> employees) {

    //Extract the address of every employhee
    List<Address> addreses = employees.stream()
        .map(Employee::address)
        .collect(Collectors.toList());
}

Using flatMap

merge multiple streams into one

Stream<String> stream1 = Stream.of("a", "b");
Stream<String> stream2 = Stream.of("c", "d");

Stream<Stream<String>> streams = Stream.of(stream1, stream2);

List<String> combined = streams
    .flatMap(s -> s)
    .collect(Collectors.toList());

Scenario: Given a list of Employee objects, each with a list of Address objects within it, return a single list of all addresses.

class Employee {
    private List<Address> addresses;//Each employee has multiple addresses, including empty
}

public void testFlatMap(List<Employee> employees) {
    List<Address> allAddresses = employees.stream()
        .flatMap(employee -> employee.getAddresses().stream())//Flatmap needs a Stream that it can stitch together.
        .collect(Collectors.toList());
}

Streams with Map

The entry set returns a collection (Set) which can then be used to create the stream

Map<String, Integer> filteredMap = map.entrySet()
    .stream()
    .filter(entry -> entry.getValue() > 1)
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

Tags:

Categories:

Updated: