Collections Aggregate Operations

To better understand this concept in this guide, review the guides Lambda Expressions and Method References.
Java 8 Streams is a new addition to the Java Collections API, which brings a new way to process collections of objects. Thus, streams in the Java Collections API is a different concept than the input and output streams in the Java IO API, even if the idea is similar (a stream of objects from a collection, instead of a stream of bytes or characters).
Streams are designed to work with Java Lambda Expressions.

1. Aggregate Operations Simple Example

Streams provide forEach() method to perform an aggregate operation. Let's discuss the aggregate operations with examples

Create Person.java Class

import java.time.LocalDate;
import java.time.chrono.IsoChronology;
import java.util.ArrayList;
import java.util.List;
 
public class Person {
   
    public enum Sex {
        MALE, FEMALE
    }
   
    String name; 
    LocalDate birthday;
    Sex gender;
    String emailAddress;
   
    Person(String nameArg, LocalDate birthdayArg,
        Sex genderArg, String emailArg) {
        name = nameArg;
        birthday = birthdayArg;
        gender = genderArg;
        emailAddress = emailArg;
    }  
 
    public int getAge() {
        return birthday
            .until(IsoChronology.INSTANCE.dateNow())
            .getYears();
    }
 
    public void printPerson() {
      System.out.println(name + ", " + this.getAge());
    }
     
    public Sex getGender() {
        return gender;
    }
     
    public String getName() {
        return name;
    }
     
    public String getEmailAddress() {
        return emailAddress;
    }
     
    public LocalDate getBirthday() {
        return birthday;
    }
     
    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }
 
    public static List<Person> createRoster() {
         
        List<Person> roster = new ArrayList<>();
        roster.add(
            new Person(
            "Fred",
            IsoChronology.INSTANCE.date(1980, 6, 20),
            Person.Sex.MALE,
            "fred@example.com"));
        roster.add(
            new Person(
            "Jane",
            IsoChronology.INSTANCE.date(1990, 7, 15),
            Person.Sex.FEMALE, "jane@example.com"));
        roster.add(
            new Person(
            "George",
            IsoChronology.INSTANCE.date(1991, 8, 13),
            Person.Sex.MALE, "george@example.com"));
        roster.add(
            new Person(
            "Bob",
            IsoChronology.INSTANCE.date(2000, 9, 12),
            Person.Sex.MALE, "bob@example.com"));
         
        return roster;
    }
     
}
In the above Person class, we have mocked person data using createRoster() method.
Let's write code to demonstrate usage of Aggregate Operations.
Example 1: The following example prints the name of all members contained in the collection roster with a for-each loop:
for (Person p : roster) {
    System.out.println(p.getName());
}
The following example prints all members contained in the collection roster but with the aggregate operation forEach:
roster
    .stream()
    .forEach(e -> System.out.println(e.getName());
Example 2: Let's Print names of male members, for-each loop:
System.out.println("Male members of the collection (for-each loop):");
for (Person p : roster) {
 if (p.getGender() == Person.Sex.MALE) {
  System.out.println(p.getName());
 }
}
Print names of male members, forEach operation:
roster
 .stream()
 .filter(e -> e.getGender() == Person.Sex.MALE)
 .forEach(e -> System.out.println(e.getName()));
Let's discuss new topics Pipelines and Streams introduced in Java 8.

2. Pipelines and Streams

pipeline is a sequence of aggregate operations.
The following example prints the male members contained in the collection roster with a pipeline that consists of the aggregate operations filter and forEach:
roster
    .stream()
    .filter(e -> e.getGender() == Person.Sex.MALE)
    .forEach(e -> System.out.println(e.getName()));
Compare this example to the following that prints the male members contained in the collection roster with a for-each loop:
for (Person p : roster) {
    if (p.getGender() == Person.Sex.MALE) {
        System.out.println(p.getName());
    }
}
A pipeline contains the following components:
  • A source: This could be a collection, an array, a generator function, or an I/O channel. In this example, the source is the collection roster. Example:
List<Person> roster = new ArrayList<>();
        roster.add(
            new Person(
            "Fred",
            IsoChronology.INSTANCE.date(1980, 6, 20),
            Person.Sex.MALE,
            "fred@example.com"));
        roster.add(
            new Person(
            "Jane",
            IsoChronology.INSTANCE.date(1990, 7, 15),
            Person.Sex.FEMALE, "jane@example.com"));
        roster.add(
            new Person(
            "George",
            IsoChronology.INSTANCE.date(1991, 8, 13),
            Person.Sex.MALE, "george@example.com"));
        roster.add(
            new Person(
            "Bob",
            IsoChronology.INSTANCE.date(2000, 9, 12),
            Person.Sex.MALE, "bob@example.com"));
  • Zero or more intermediate operations - An intermediate operation, such as filter, produces a new stream.
The filter operation returns a new stream that contains elements that match its predicate (this operation's parameter).
In this example, the predicate is the lambda expression
 e -> e.getGender() == Person.Sex.MALE.
It returns the boolean value true if the gender field of object e has the value Person.Sex.MALE.
Example: the filter operation in this example returns a stream that contains all male members in the collection roster.
roster
            .stream()
            .filter(e -> e.getGender() == Person.Sex.MALE)
            .forEach(e -> System.out.println(e.getName()));
stream is a sequence of elements. Unlike a collection, it is not a data structure that stores elements. Instead, a stream carries values from a source through a pipeline.
This example creates a stream from the collection roster by invoking the method stream.
List<Person> roster = new ArrayList<>();
roster.add(
 new Person(
 "Fred",
 IsoChronology.INSTANCE.date(1980, 6, 20),
 Person.Sex.MALE,
 "fred@example.com"));
roster.add(
 new Person(
 "Jane",
 IsoChronology.INSTANCE.date(1990, 7, 15),
 Person.Sex.FEMALE, "jane@example.com"));
roster.add(
 new Person(
 "George",
 IsoChronology.INSTANCE.date(1991, 8, 13),
 Person.Sex.MALE, "george@example.com"));
roster.add(
 new Person(
 "Bob",
 IsoChronology.INSTANCE.date(2000, 9, 12),
 Person.Sex.MALE, "bob@example.com"));
 
  Stream<Person> stream =  roster.stream();
  • A terminal operation - A terminal operation, such as forEach, produces a non-stream result, such as a primitive value (like a double value), a collection, or in the case of forEach, no value at all.
In this example, the parameter of the forEach operation is the lambda expression e -> System.out.println(e.getName()), which invokes the method getName on the object e. (The Java runtime and compiler infer that the type of the object e is Person.)
The following example calculates the average age of all male members contained in the collection roster with a pipeline that consists of the aggregate operations filter, mapToInt, and average:
double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();
From above explained as below important points
  • The mapToInt operation returns a new stream of type IntStream (which is a stream that contains only integer values).
  • The function is Person::getAge, which is a method reference that returns the age of the member.
  • The average operation calculates the average value of the elements contained in a stream of type IntStream. It returns an object of type OptionalDouble. If the stream contains no elements, then the average operation returns an empty instance of OptionalDouble, and invoking the method getAsDouble throws a NoSuchElementException.

3. Differences Between Aggregate Operations and Iterators

Aggregate operations, like forEach, appear to be like iterators. However, they have several fundamental differences:
  • They use internal iteration: Aggregate operations do not contain a method like next to instruct them to process the next element of the collection. With internal delegation, your application determines what collection it iterates, but the JDK determines how to iterate the collection.
  • The process elements from a stream: Aggregate operations process elements from a stream, not directly from a collection. Consequently, they are also called stream operations.
  • They support behavior as parameters: You can specify lambda expressions as parameters for most aggregate operations. This enables you to customize the behavior of a particular aggregate operation.

Comments