Java 8 Features — Streams and Optional

Stream API, Stream Operations, Optional, StringJoiner & Java 8 Map Enhancements

Prepared by Srikanth Mamillapalli

1. Java 8 Streams Overview

Optional

Optionality is expressed via a special wrapper class.

Stream API

A particular iterator-style API used to efficiently process collections of items.

Streams

To process objects of a collection, the Stream concept was introduced in Java 8.

Stream: Once a stream is obtained from a collection, stream operations can be used to process the objects in that collection.
Stream s = c.stream();

The stream() method is a default method added to the Collection interface in Java 8.

Stream is an interface present in java.util.stream.

2. Difference Between Collection and Stream

CollectionStream
Represents a group of individual objects as a single entity. Used to process a group of objects from a collection.
Primarily represents/stores the group of objects. Provides operations for processing the objects.
Can be converted to a Stream using stream(). Obtained from a collection through collection.stream().
Collection
stream()
Stream
Operations

3. Java 8 Stream Features

Java 8 FeatureStream / Related Capability
Lambda ExpressionsIntermediate operations
Functional InterfacesFilter — Predicate
Method ReferencesMap — Function
Default Methods in InterfacesflatMap — Function<T, Stream<R>>
Static Methods in InterfacesDistinct uses Object.equals() and hashCode()
Stream APIsorted() — Comparator<T>
Date and Time API (java.time)limit(n)
Optional Classskip(n)
Nashorn JavaScript Enginepeek — Consumer<T>
Parallel StreamsTerminal Operations
CompletableFuturecollect(Collectors)
Collectors UtilityforEach(Consumer), count(), anyMatch(Predicate<T>)
More terminal operationsallMatch(), noneMatch(), findFirst(), findAny(), reduce()

4. Stream Processing Phases

The source describes processing Stream objects in two phases:

Configuration
Processing

Configuration

Configuration can be performed using either:

filter() map() flatMap()

Processing

After configuration, the objects can be processed using several methods such as:

collect() count() sorted() min() max() forEach() toArray() Stream.of()

5. Filtering with filter()

The filter() method is used to filter elements from a collection based on a boolean condition.

public Stream filter(Predicate<T> t)

The Predicate can be a boolean-valued function or Lambda Expression.

Stream s = c.stream();

Stream s1 =
    s.filter(i -> i % 2 == 0);
Use filter() when you want to retain elements that satisfy a boolean condition.

6. Mapping with map()

If we want to create a separate/new object for every object present in the collection based on our requirement, we can use the map() method.

public Stream map(Function f);

The function can be represented by a Lambda Expression.

Stream s = c.stream();

Stream s1 =
    s.map(i -> i + 10);
Original Element
map(Function)
Transformed Element

7. flatMap()

The source describes flatMap() as a combination of transformation and flattening.

flatMap
=
map
+
flattening

The notes distinguish the two operations by describing map as processing each value, whereas flatMap produces individual elements from the mapped result.

8. Stream Processing Operations

OperationPurpose
collect()Collects stream elements into a specified result/collection.
count()Counts the number of elements.
sorted()Sorts stream elements.
min()Finds minimum according to a comparator.
max()Finds maximum according to a comparator.
forEach()Processes each element.
toArray()Converts stream elements into an array.
Stream.of()Creates a Stream from specified values.
reduce()Combines elements using an accumulator such as BinaryOperator.
anyMatch()Checks whether any element matches a predicate.
allMatch()Checks whether all elements match a predicate.
noneMatch()Checks whether no elements match a predicate.
findFirst()Retrieves the first element if present.
findAny()Retrieves an element, mainly useful with parallel streams.

9. collect()

The collect() method collects elements from the stream and adds them to the result/collection specified by the argument.

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

List<String> result =
    names.stream()
         .collect(Collectors.toList());
Typical use: Convert a processed Stream back into a List, Set, String or another collector-defined result.

10. String.join() and Collectors.joining()

String.join()

String result =
    String.join(
        "-",
        "2015",
        "10",
        "31"
    );

List<String> list =
    Arrays.asList(
        "java",
        "python",
        "nodejs",
        "ruby"
    );

String result2 =
    String.join(", ", list);

Collectors.joining()

List<String> list =
    Arrays.asList(
        "java",
        "python",
        "nodejs",
        "ruby"
    );

String result =
    list.stream()
        .map(x -> x)
        .collect(
            Collectors.joining(" | ")
        );

11. Optional Class

Optional is a container object used to contain non-null objects. It can represent a value as available or not available instead of repeatedly checking for null values.

The source emphasizes Optional as a way to handle values as available or not available instead of direct null checks.

Example

public class OptionalDemo {

    public static void main(String[] args) {

        String[] words =
            new String[10];

        Optional<String> checkNull =
            Optional.ofNullable(words[5]);

        if (checkNull.isPresent()) {

            String word =
                words[5].toLowerCase();

            System.out.print(word);

        } else {

            System.out.println(
                "word is null"
            );
        }
    }
}

12. Optional Methods

Creation Methods

MethodDescription
empty()Returns an empty Optional instance.
of(T value)Returns an Optional containing the specified present non-null value.
ofNullable(T value)Returns an Optional describing the value if non-null; otherwise returns an empty Optional.

Core / Utility Methods

MethodDescription
equals(Object obj)Indicates whether another object is equal to this Optional.
filter(Predicate<? super T> predicate)If a value is present and matches the predicate, returns an Optional describing it; otherwise returns empty.
flatMap(Function<? super T, Optional<U>> mapper)Applies an Optional-bearing mapping function if a value is present.
get()Returns the contained value if present; otherwise throws NoSuchElementException.
hashCode()Returns the hash code of the value, or 0 if no value is present.
ifPresent(Consumer<? super T>)If a value is present, invokes the specified consumer.
isPresent()Returns true if a value is present.
map(Function<? super T, ? extends U> mapper)Applies a mapping function if a value is present and returns an Optional of the result.
orElse(T other)Returns the value if present; otherwise returns the supplied other value.
orElseGet(Supplier<? extends T> other)Returns the value if present; otherwise invokes the supplier.
orElseThrow(Supplier<? extends X> exceptionSupplier)Returns the value if present; otherwise throws an exception created by the supplier.
toString()Returns a non-empty string representation suitable for debugging.

13. Advantages of Java 8 Optional

Null Checks

Null checks are not required in the same repetitive form.

Runtime Safety

The source presents Optional as helping avoid NullPointerException scenarios.

Clean APIs

Can help develop clean and neat APIs.

Less Boilerplate

Reduces repetitive null-handling boilerplate.

Optional<String> gender =
    Optional.of("MALE");

String answer1 = "Yes";
String answer2 = null;

System.out.println(
    "Non-Empty Optional: " + gender
);

System.out.println(
    "Non-Empty Optional: Gender value = "
    + gender.get()
);

System.out.println(
    "Empty Optional: "
    + Optional.empty()
);

System.out.println(
    "ofNullable on Non-Empty Optional: "
    + Optional.ofNullable(answer1)
);

System.out.println(
    "ofNullable on Empty Optional: "
    + Optional.ofNullable(answer2)
);

14. StringJoiner

StringJoiner is a new class added in Java 8 under the java.util package. It is useful for joining Strings using a delimiter, prefix and suffix.

The source lists two constructors:

StringJoiner(CharSequence delimiter)

StringJoiner(
    CharSequence delimiter,
    CharSequence prefix,
    CharSequence suffix
)

Example of StringJoiner

private static String PREFIX = "{";
private static String SUFFIX = "}";

StringJoiner joiner =
    new StringJoiner(
        ", ",
        PREFIX,
        SUFFIX
    );

joiner
    .add("Core Java")
    .add("Spring Boot")
    .add("Angular");

System.out.println(
    joiner.toString()
);

15. join() Method in String Class

Java 8 introduced the join() method in the String class. It concatenates the given Strings using the specified delimiter and returns a new String.

public static String join(
    CharSequence delimiter,
    CharSequence... elements
)

Example

public class Example {

    public static void main(String args[]) {

        String str =
            String.join(
                "-",
                "You",
                "are",
                "learning",
                "join",
                "method",
                "in Java 8"
            );

        System.out.println(str);
    }
}

Output:

You-are-learning-join-method-in Java 8

Example of a List using join()

import java.util.List;
import java.util.Arrays;

public class Example {

    public static void main(String args[]) {

        // Converting an array of String to the list
        List<String> list =
            Arrays.asList(
                "Virat",
                "Ricky",
                "Peterson",
                "Watson"
            );

        String names =
            String.join(" | ", list);

        System.out.println(names);
    }
}

16. Java 8 HashMap / Map New Methods

MethodDescription / Example
forEach Iterates over each key-value pair.
map.forEach((key, value) -> System.out.println(key + " = " + value));
getOrDefault Returns the value for the key, or a default value if the key is not found.
int count = map.getOrDefault("orange", 0);
putIfAbsent Puts a value only if the key is not already associated with a value.
map.putIfAbsent("apple", 5);
computeIfAbsent Computes and puts a value if the key is absent.
map.computeIfAbsent("orange", k -> 10);
computeIfPresent Computes a new value only if the key is already present.
map.computeIfPresent("banana", (k, v) -> v + 2);
compute Updates the value using a remapping function.
map.compute("apple", (k, v) -> (v == null) ? 1 : v + 1);
merge Combines the existing value with a new value using a merge function.
map.merge("apple", 1, Integer::sum);
replace Replaces an entry if it exists and optionally matches the old value.
map.replace("apple", 2, 4);
remove Removes an entry only if the key is mapped to the specified value.
map.remove("banana", 3);

17. Intermediate Operations

The source provides examples for several intermediate Stream operations.

filter()

public class FilterDemo {

    public static void main(String[] args) {

        List<String> names =
            List.of(
                "Alice",
                "Bob",
                "Amanda",
                "David"
            );

        List<String> names =
            names.stream()
                 .filter(
                     name ->
                         name.startsWith("A")
                 )
                 .collect(
                     Collectors.toList()
                 );

        System.out.println(names);
    }
}

map()

public class MapDemo {

    public static void main(String[] args) {

        List<String> names =
            List.of("Alice", "Bob");

        List<Integer> nameLengths =
            names.stream()
                 .map(String::length)
                 .collect(
                     Collectors.toList()
                 );

        System.out.println(nameLengths);
    }
}

distinct()

public class FlatMapDemo {

    public static void main(String[] args) {

        List<Integer> nums =
            List.of(1, 2, 2, 3);

        List<Integer> unique =
            nums.stream()
                .distinct()
                .collect(
                    Collectors.toList()
                );

        System.out.println(unique);
    }
}

flatMap()

public class DistinctDemo {

    public static void main(String[] args) {

        List<List<String>> data =
            List.of(
                List.of("a", "b"),
                List.of("c", "d")
            );

        List<String> flat =
            data.stream()
                .flatMap(List::stream)
                .collect(
                    Collectors.toList()
                );

        System.out.println(flat);
    }
}

sorted()

public class SortedDemo {

    public static void main(String[] args) {

        List<String> names =
            List.of(
                "John",
                "Alice",
                "Bob"
            );

        List<String> sorted =
            names.stream()
                 .sorted()
                 .collect(
                     Collectors.toList()
                 );

        System.out.println(sorted);
    }
}

limit()

public class LimitDemo {

    public static void main(String[] args) {

        List<Integer> limited =
            Stream.of(1, 2, 3, 4, 5)
                  .limit(3)
                  .collect(
                      Collectors.toList()
                  );

        System.out.println(limited);
    }
}

skip()

public class SkipDemo {

    public static void main(String[] args) {

        List<Integer> skipped =
            Stream.of(1, 2, 3, 4, 5)
                  .skip(2)
                  .collect(
                      Collectors.toList()
                  );

        System.out.println(skipped);
    }
}

18. Terminal Operations

peek()

public class PeekDemo {

    public static void main(String[] args) {

        List<String> result =
            List.of(
                "apple",
                "banana",
                "cherry"
            )
            .stream()
            .peek(
                System.out::println
            )
            .map(
                String::toUpperCase
            )
            .collect(
                Collectors.toList()
            );

        System.out.println(result);
    }
}

mapToInt()

public class MapToIntDemo {

    public static void main(String[] args) {

        int sum =
            List.of(
                "a",
                "bb",
                "ccc"
            )
            .stream()
            .mapToInt(String::length)
            .sum();

        System.out.println(sum);
    }
}

forEach()

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

names.stream()
     .forEach(
         System.out::println
     );

toArray()

String[] nameArray =
    names.stream()
         .toArray(String[]::new);

reduce()

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

int sum =
    numbers.stream()
           .reduce(
               0,
               Integer::sum
           );

collect()

List<String> list =
    names.stream()
         .collect(
             Collectors.toList()
         );

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

String joined =
    names.stream()
         .collect(
             Collectors.joining(", ")
         );

min() and max()

Optional<Integer> min =
    numbers.stream()
           .min(
               Integer::compareTo
           );

Optional<Integer> max =
    numbers.stream()
           .max(
               Integer::compareTo
           );

count()

long count =
    names.stream().count();

anyMatch(), allMatch(), noneMatch()

boolean anyStartsWithA =
    names.stream()
         .anyMatch(
             s -> s.startsWith("A")
         );

boolean allHaveLength3 =
    names.stream()
         .allMatch(
             s -> s.length() == 3
         );

boolean noneStartsWithZ =
    names.stream()
         .noneMatch(
             s -> s.startsWith("Z")
         );

findFirst()

Optional<String> first =
    names.stream()
         .findFirst();

findAny()

Optional<String> any =
    names.parallelStream()
         .findAny();

forEachOrdered()

names.parallelStream()
     .forEachOrdered(
         System.out::println
     );

19. Custom Collector

The final example in the PDF demonstrates a custom Collector that uses StringJoiner to combine stream elements with a delimiter.

Collector<String, StringJoiner, String>
    customCollector =
        Collector.of(
            () ->
                new StringJoiner(" | "),
            StringJoiner::add,
            StringJoiner::merge,
            StringJoiner::toString
        );

String result =
    names.stream()
         .collect(customCollector);
Stream Elements
Custom Collector
StringJoiner
Joined String

Example output shown in the source:

Alice | Bob | Charlie

20. Quick Revision

TopicKey Point
CollectionRepresents a group of objects as a single entity.
StreamUsed to process objects from a collection.
stream()Creates a Stream from a Collection.
filter()Filters elements using a Predicate condition.
map()Transforms each element using a Function.
flatMap()Combines transformation and flattening.
distinct()Removes duplicate elements.
sorted()Sorts stream elements.
limit()Restricts the number of elements processed.
skip()Skips the specified number of elements.
peek()Performs an action on elements while processing.
collect()Collects stream results into a desired form.
reduce()Combines elements into a single result.
count()Counts stream elements.
min()/max()Finds minimum/maximum values.
anyMatch()Checks whether any element matches a condition.
allMatch()Checks whether all elements match a condition.
noneMatch()Checks whether no elements match a condition.
findFirst()Retrieves the first element as Optional.
findAny()Retrieves an element as Optional.
OptionalContainer for representing a value as present or absent.
StringJoinerJoins Strings using delimiter, prefix and suffix.
String.join()Joins Strings with a specified delimiter.
Map enhancementsforEach, getOrDefault, putIfAbsent, compute, merge, replace, remove, etc.

Stream Pipeline Memory Trick

Source
Intermediate Operations
Terminal Operation

Source

Collection, List, Set, Stream.of(), etc.

Intermediate

filter, map, flatMap, distinct, sorted, limit, skip, peek.

Terminal

collect, forEach, reduce, count, min, max, match operations, find operations.

One-line summary: Java 8 Streams provide a declarative way to configure and process collections, while Optional provides a wrapper for representing values that may or may not be present.