Java 8 introduces new features such as Streams.

Streams

Streams main purpose is to perform a set of operations on collections such as List, LinkedList, Maps, Sets. It also can be used with arrays or any kind of I/O.

What is a Stream Stream is a sequence of elements which can be created out of a collections

List<String> names = List.of("amy","dan")
names.stream(); //creates a stream

Usage of using stream api operations

Helper class for stream example

@Getter
@AllArgsConstructor
public class Student{
  private String name;
  private Integer gradeLevel;
  private double gpa;
  private String gender;
  private Integer noteBooks;
  private List<String> activities;
}

public class StudentRepository{
    public static List<Student> getAllStudents() {
        return List.of(
                new Student("Adam", 2, 3.6, "male", 10, List.of("swimming", "basketball", "volleyball")),
                new Student("Jenny", 2, 3.8, "female", 11, List.of("swimming", "gymnastics", "soccer")),
                new Student("Emily", 3, 3.4, "female", 12, List.of("swimming", "gymnastics", "aerobics")),
                new Student("Dave", 3, 3.4, "male", 12, List.of("swimming", "gymnastics", "aerobics")),
                new Student("Sophia", 4, 4.3, "female", 10, List.of("swimming", "dancing", "football")),
                new Student("James", 4, 4.6, "male", 22, List.of("swimming", "basketball", "baseball", "football"))
        );
    }
}
public class StreamsExample {
    public static void main(String[] args) {
        Predicate<Student> filterByGradeLevel = student -> student.getGradeLevel() >=3;
        Predicate<Student> filterByGpa = student -> student.getGpa() >= 3.9;
        Map<String, List<String>> studentMap =
                StudentRepository.getAllStudents().stream()
                        .filter(filterByGradeLevel)
                        .filter(filterByGpa)
                        .collect(Collectors.toMap(Student::getName, Student::getActivities));
        System.out.println(studentMap);
    }
}

Output

{James=[swimming, basketball, baseball, football], Sophia=[swimming, dancing, football]}

Sample of stream flow image

Debugging with stream

  • A helpful tip for debugging streams is to use the peek(Consumer) operation. This enables us to stop at a certain stream operation to see what is the output
 StudentRepository.getAllStudents().stream()
                        .filter(filterByGradeLevel)
                        .filter(filterByGpa)
                        .peek(System.out::println) //prints stream of students filtered by grade and gpa before being terminated
                        .collect(Collectors.toMap(Student::getName, Student::getActivities));

Streams API

map Returns a stream consisting of the results of applying the given function to the elements of this stream.

public static List<String> namesList(){
        return StudentRepository.getAllStudents().stream()
                .map(Student::getName)
                .map(String::toUpperCase)
                .collect(Collectors.toList());
    }
    public static void main(String[] args) {
        System.out.println(namesList());
    }

Output

[ADAM, JENNY, EMILY, DAVE, SOPHIA, JAMES]

flatMap Converts one type to another similar to map() method except that it’s used in the context of Stream where each element in the stream represents multiple elements such Stream<List> or Stream<Arrays

public class StreamFlatMapExample {
    public static List<String> printStudentActivities(){
        return StudentDataAccess.getAllStudents()  //List<Student>
                .stream() //Stream<Student>
                .map(Student::getActivities) //Stream<List<String>>
                .flatMap(List::stream) //Stream<String>
                .collect(Collectors.toList()); //List<String>

    }
    public static void main(String[] args) {
        System.out.println(printStudentActivities());
    }
}

Output

[swimming, basketball, volleyball, swimming, gymnastics, soccer, swimming, gymnastics, aerobics, swimming, gymnastics, aerobics, swimming, dancing, football, swimming, basketball, baseball, football]

distinct Returns a stream with unique elements

public class StreamFlatMapExample {
    public static List<String> printStudentActivities(){
        return StudentDataAccess.getAllStudents()  //List<Student>
                .stream() //Stream<Student>
                .map(Student::getActivities) //Stream<List<String>>
                .flatMap(List::stream) //Stream<String>
                .distinct()
                .collect(Collectors.toList()); //List<String>

    }
    public static void main(String[] args) {
        System.out.println(printStudentActivities());
    }
}

Output

[swimming, basketball, volleyball, gymnastics, soccer, aerobics, dancing, football, baseball]

count Return a long with the total number of elements in the Stream

public static long getCountOfStudentActivities(){
    return StudentDataAccess.getAllStudents()
            .stream()
            .map(Student::getActivities)
            .flatMap(List::stream)
            .distinct()
            .count();
}
 public static void main(String[] args) {
    System.out.println("Count: " + getCountOfStudentActivities());
}

Output

Count: 9

sorted Sort the elements in the stream

There are 2 overloaded method for sorted sorted() and sorted(Comparator Function) We could also chain sorted with reversed()

public class StreamFlatMapExample {
    public static List<String> printStudentActivities(){
        return StudentDataAccess.getAllStudents()
                .stream()
                .map(Student::getActivities)
                .flatMap(List::stream)
                .distinct()
                .sorted()
                .collect(Collectors.toList());

    }

    public static void main(String[] args) {
        System.out.println(printStudentActivities());
    }

Output

[aerobics, baseball, basketball, dancing, football, gymnastics, soccer, swimming, volleyball]
public class StreamFlatMapExample {
    public static List<Student> sortStudentsByName(){
        return StudentDataAccess.getAllStudents()
                .stream()
                .sorted(Comparator.comparing(Student::getName))
                .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        sortStudentsByName().forEach(System.out::println);
    }

Output

Student{name='Adam', gradeLevel=2, gpa=3.6, gender='male', noteBooks=10}
Student{name='Dave', gradeLevel=3, gpa=3.4, gender='male', noteBooks=12}
Student{name='Emily', gradeLevel=3, gpa=3.4, gender='female', noteBooks=12}
Student{name='James', gradeLevel=4, gpa=4.6, gender='male', noteBooks=22}
Student{name='Jenny', gradeLevel=2, gpa=3.8, gender='female', noteBooks=11}
Student{name='Sophia', gradeLevel=4, gpa=4.3, gender='female', noteBooks=10}

filter Filter the elements in the stream

public class StreamFilterExample {
    public static List<Student> filterStudents(){
        return StudentDataAccess.getAllStudents().stream()
                .filter(student -> student.getGpa() >= 4.0)
                .collect(Collectors.toList());
    }
    public static void main(String[] args) {
        System.out.println(filterStudents());
    }
}

Output

[Student{name='Sophia', gradeLevel=4, gpa=4.3, gender='female', noteBooks=10}, Student{name='James', gradeLevel=4, gpa=4.6, gender='male', noteBooks=22}]

reduce This is a terminal operation. Used to reduce the contents of stream to a single value. reduce method without identity will return Optional as a return type

public class StreamReduceExample {

    public static int performMultiplication(List<Integer> integerList){
       return integerList.stream()
               //1,3,5,7
               //a=1, b=1(stream) -> 1 is returned
               //a=1, b=3(stream) -> 3 is returned
               //a=3, b=5(stream) -> 15 is returned
               //a=15, b=7(stream) -> 105 is returned
                .reduce(1, (a,b) -> a*b);
    }

    public static Optional<Integer> performMultiplicationWithoutIdentity(List<Integer> integerList){
       return integerList.stream()
               //1,3,5,7
               //a=1, b=1(stream) -> 1 is returned
               //a=1, b=3(stream) -> 3 is returned
               //a=3, b=5(stream) -> 15 is returned
               //a=15, b=7(stream) -> 105 is returned
                .reduce((a,b) -> a*b);
    }
    public static void main(String[] args) {
        System.out.println(performMultiplication(List.of(1,3,5,7)));
        Optional<Integer> result = performMultiplicationWithoutIdentity(List.of(1, 3, 5, 7));
        result.ifPresent(System.out::println);
    }

Output

105
105

limit & skip These two functions helps to create a sub-stream

  • limit(n) - limits the n numbers of elements to be processed in the stream
  • skip(n) - skips the n numbers of elements from the stream
public class StreamsLimitSkipExample {

    public static Optional<Integer> limitList(List<Integer> integers){
        return integers.stream()
                .limit(2)
                .reduce(Integer::sum);
    }

    public static Optional<Integer> skipList (List<Integer> integers){
        return integers.stream()
                .skip(2)
                .reduce(Integer::sum);
    }

    public static void main(String[] args) {
        List<Integer> integerList = List.of(6,7,8,9,10);

        Optional<Integer> limitedList  = limitList(integerList);
        Consumer<Integer> displayStatement = (value) -> System.out.println("The limit result is : " + value);
        limitedList.ifPresent(displayStatement);

        Optional<Integer> skipList = skipList(integerList);
        Consumer<Integer> skipStatement = (value) -> System.out.println("The skip result is : " + value);
        skipList.ifPresent(skipStatement);
    }
}

Output

The limit result is : 13
The skip result is : 27

allMatch & anyMatch & noneMatch These functions takes in a predicatee as in input and returns a boolean as an output

  • anyMatch() - Returns true if any one of the element matches the predicate, otherwise false
  • allMatch() - Returns true if all the element in the stream matches the predicate, otherwise false
  • noneMatch() - Opposite to allMatch(). Returns true if none of the element in the stream amtches the predicate, otherwise false
public class StreamsMatchExample {

    private static boolean allMatch() {
        return StudentDataAccess.getAllStudents().stream()
                .allMatch(student -> student.getGpa() >= 3.4);
    }

    private static boolean anyMatch() {
        return StudentDataAccess.getAllStudents().stream()
                .anyMatch(student -> student.getGpa() >= 4.0);
    }

    private static boolean noneMatch() {
        return StudentDataAccess.getAllStudents().stream()
                .noneMatch(student -> student.getGpa() >= 4.9);
    }

    public static void main(String[] args) {
        System.out.println("Result of allMatch: " + allMatch());
        System.out.println("Result of anyMatch: " + anyMatch());
        System.out.println("Result of noneMatch: " + noneMatch());
    }
}

Output

Result of allMatch: true
Result of anyMatch: true
Result of noneMatch: true

findFirst & findAny Functions used to find an element in the stream and reutrns the result type of Optional.

  • findFirst() - Returns the first element in the stream
  • findAny() - Returns the first encountered element in the stream
public class StreamsFindAnyFirstExample {

    private static Optional<Student> findAny() {
        return StudentDataAccess.getAllStudents().stream()
                .filter(student -> student.getGpa() > 3.4)
                .findAny();
    }
    private static Optional<Student> findFirst() {
        return StudentDataAccess.getAllStudents().stream()
                .filter(student -> student.getGpa() > 3.4)
                .findFirst();
    }


    public static void main(String[] args) {
        Optional<Student> anyStudent = findAny();
        anyStudent.ifPresent((student) -> System.out.println("Any student: " + student));

        Optional<Student> firstStudent = findFirst();
        firstStudent.ifPresent((student) -> System.out.println("First student: " + student));
    }
}

Output

Any student: Student{name='Adam', gradeLevel=2, gpa=3.6, gender='male', noteBooks=10}
First student: Student{name='Adam', gradeLevel=2, gpa=3.6, gender='male', noteBooks=10}

Terminal Operations

Terminal operations collects the data and starts the whole stream pipeline

  • forEach
  • reduce
  • collect
  • etc..

joining()

  • Collector performs the String concatenation on the elements in the stream
  • We can use it with delimiter, prefix and suffix
public class StreamsJoiningExample {

    public static String joiningDefault(){
        return StudentDataAccess.getAllStudents().stream()
                .map(Student::getName)
                .collect(joining());
    }
    public static String joiningWithDelimiter(){
        return StudentDataAccess.getAllStudents().stream()
                .map(Student::getName)
                .collect(joining("|"));
    }

    public static String joiningWithPrefixAndSuffix(){
        return StudentDataAccess.getAllStudents().stream()
                .map(Student::getName)
                .collect(joining("-", "(", ")"));
    }

    public static void main(String[] args) {
        System.out.println(joiningDefault());
        System.out.println(joiningWithDelimiter());
        System.out.println(joiningWithPrefixAndSuffix());
    }
}

groupingBy()

  • use to group elements based on a property
  • output of groupingBy is going to be Map<K,V>
  • 3 overloaded methods
    • groupingBy(classifier)
    • groupingBy(classifier,downstream reduction function)
    • groupingBy(classifier,supplier,downstream reduction function)
public class StreamsGroupingByExample {

    public static void categoriseByGender() {
        Map<String, List<Student>> collect = StudentDataAccess.getAllStudents()
                .stream()
                .collect(groupingBy(Student::getGender));

        System.out.println("Categorise by gender: " + collect);
    }

    public static void categoriseByGenderWithGpa() {
        Map<String, List<Student>> collect = StudentDataAccess.getAllStudents()
                .stream()
                .collect(groupingBy(student -> student.getGpa() >= 3.8 ? "OUTSTANDING" : "AVERAGE"));

        System.out.println("Categorise by genderWithGPA: " + collect);
    }

    public static void twoLevelGrouping() {
        Map<Integer, Map<String, List<Student>>> collect = StudentDataAccess.getAllStudents()
                .stream()
                .collect(groupingBy(Student::getGradeLevel,
                        groupingBy(student -> student.getGpa() >= 3.8 ? "OUTSTANDING" : "AVERAGE")));

        System.out.println("Two level grouping: " + collect);
    }

    public static void mapStudentToNotebooks() {
        Map<String, Integer> collect = StudentDataAccess.getAllStudents()
                .stream()
                .collect(groupingBy(Student::getName,
                        summingInt(Student::getNoteBooks)));
        System.out.println("mapStudentToNotebooks: "+ collect);
    }

    //With classifier, supplier, and collector downstream function
    public static void threeArgumentGroupBy() {
        LinkedHashMap<String, Set<Student>> collect = StudentDataAccess.getAllStudents()
                .stream()
                .collect(groupingBy(Student::getName, LinkedHashMap::new,
                        toSet()));

        System.out.println("threeArgumentGroupBy: " + collect);
    }

    public static void calculateTopGpa(){
        Map<Integer, Student> collect = StudentDataAccess.getAllStudents()
                .stream()
                .collect(groupingBy(Student::getGradeLevel,
                        collectingAndThen(maxBy(Comparator.comparing(Student::getGpa)),
                                Optional::get)));

        System.out.println("Top GPA: " + collect);
    }
    public static void calculateLeastGpa(){
        Map<Integer, Student> collect = StudentDataAccess.getAllStudents()
                .stream()
                .collect(groupingBy(Student::getGradeLevel,
                        collectingAndThen(minBy(Comparator.comparing(Student::getGpa)),
                                Optional::get)));
        System.out.println("Least GPA: " + collect);
    }


    public static void main(String[] args) {
        categoriseByGender();
        categoriseByGenderWithGpa();
        twoLevelGrouping();
        mapStudentToNotebooks();
        threeArgumentGroupBy();
        calculateTopGpa();
        calculateLeastGpa();
    }
}

partitioningBy

  • accepts a predicate as an input
  • Retrun a Map<K,V. Key of return type is going to be a boolean
  • 2 overloaded methods
    • partitioningBy(predicate)
    • partitioningBy(predicate, downstream) downstream can be any kind of collector
public class StreamsPartitioningByExample {
    public static void partitioningBy1(){
        Predicate<Student> gpaPredicate = student -> student.getGpa() >= 3.8;
        Map<Boolean, List<Student>> collect = StudentDataAccess.getAllStudents().stream()
                .collect(Collectors.partitioningBy(gpaPredicate));

        System.out.println(collect);
    }

    public static void partitioningBy2(){
        Predicate<Student> gpaPredicate = student -> student.getGpa() >= 3.8;
        Map<Boolean, Set<Student>> collect = StudentDataAccess.getAllStudents().stream()
                .collect(Collectors.partitioningBy(gpaPredicate,
                        Collectors.toSet()));

        System.out.println(collect);
    }
    public static void main(String[] args) {
        partitioningBy1();
        partitioningBy2();

    }
}