Skip to content

Artemas-Muzanenhamo/java-functional-collectors

Repository files navigation

Java Functional Collectors

Java CI with Gradle

Common Operators

Given some people:

public static List<Person> createPeople() {
    return List.of(
            new Person("Sara", 20),
            new Person("Nancy", 22),
            new Person("Bob", 20),
            new Person("Paula", 32),
            new Person("Paul", 32),
            new Person("Bill", 3),
            new Person("Jack", 72),
            new Person("Jill", 11)
        );
    );
}

The code above will be used as the default input for every example below 😏

filter()

Allows us to pick some values and not pick other values within a stream pipeline. It takes a Predicate which returns a boolean of either true or false value.

  • Get people over the age of 30 only:
List<Person> over30s = createPeople()
        .stream()
        .filter(person -> person.getAge() > 30)
        .collect(toList());

map()

This will transform your output in your stream pipeline to be whatever type you map your element to in the .map() operation.

  • Get all people's first names:
List<String> namesOfOver30s = createPeople()
        .stream()
        .map(Person::getName)
        .collect(toList());

reduce()

Reduce take the collection and reduces it to a single value. Reduce converts a Stream to something more concrete. Java has reduce in two forms:

  • .reduce()
  • .collect()
  • Get the total age of every person:
Integer totalAgeOfEveryone = createPeople()
        .stream()
        .map(Person::getAge)
        .reduce(0, Integer::sum);

Honouring Immutability

There may be some cases when you need to honour immutability.

  • Get all people's ages
List<Integer> listOfAges = createPeople().stream()
        .map(Person::getAge)
        .collect(toList());

listOfAges.add(99); // valid as list returned is still mutable

assertThat(listOfAges)
        .isNotEmpty()
        .contains(99);

In the code above, you can see that we are still able to mutate/change the list and add an age to the existing one. In some cases you might want to honour immutability when you have finished processing your collection in a stream pipeline. To do so, you can use the toUnmodifiableList() collector operation to achieve this. That way, your list will not be modifiable after your terminal operation.

// Honour Immutability
List<Integer> unmodifiableListOfAllAges = createPeople().stream()
        .map(Person::getAge)
        .collect(toUnmodifiableList());

assertThatThrownBy(() -> unmodifiableListOfAllAges.add(99))
        .isExactlyInstanceOf(UnsupportedOperationException.class);

In fact, should one try to add or modify the unModifiableList, a UnsupportedOperationException will be thrown.

Functional Programming

Object-Oriented Programming: Polymorphism Functional Programming: Functional Composition + Lazy Evaluation

  • Lazy evaluation requires purity of functions(a function without side-effects). Pure function
  • Return the same result any number of times we call it with the same input(idempotency).
  • Pure functions do not have side effects.
  1. Pure functions do not change anything.
  2. Pure functions do not depend on anything that may change.

Collectors

  • Get the list of names, in uppercase, of those who are older than 30

The HARD way:

List<String> over30sNamesUpperCased = createPeople()
        .stream()
        .filter(person -> person.getAge() > 30)
        .map(Person::getName)
        .map(String::toUpperCase)
        .reduce(
                new ArrayList<>(),
                (names, name) -> {
                    names.add(name);
                    return names;
                },
                (names1, names2) -> {
                    names1.addAll(names2);
                    return names1;
                }
        );

The EASY way:

List<String> over30sNamesUpperCased = createPeople()
        .stream()
        .filter(person -> person.getAge() > 30)
        .map(Person::getName)
        .map(String::toUpperCase)
        .collect(toList());
  • It is our responsibility to keep the functions pure otherwise we will not be able to achieve lazy-evaluation.
  • Collectors are a group of utility functions written to make our life solely easy.
  • The .collect() is a reduce operation.
  • Collectors is a utility class.

toMap()

  • Get names as key and age as value
Map<String, Integer> expectedOutput = Map.of("Bob", 20, "Bill", 3, "Nancy", 22, "Sara", 20, "Paula", 32, "Jack", 72, "Jill", 11, "Paul", 32);

Map<String, Integer> nameAndAge = createPeople().stream()
        .collect(toMap(Person::getName, Person::getAge));

assertThat(nameAndAge).
        isNotEmpty()
        .containsExactlyInAnyOrderEntriesOf(expectedOutput);

partitioningBy()

Returns a Collector which partitions the input elements according to a Predicate, and organizes them into a Map<Boolean, List>

  • Map people over the age of 21 by age
List<Person> expectedFalsePartition = List.of(
        new Person("Sara", 20),
        new Person("Bob", 20),
        new Person("Bill", 3),
        new Person("Jill", 11)
);

List<Person> expectedTruePartition = List.of(
        new Person("Nancy", 22),
        new Person("Paula", 32),
        new Person("Paul", 32),
        new Person("Jack", 72)
);

Map<Boolean, List<Person>> peopleByAge = createPeople().stream()
                .collect(partitioningBy(person -> person.getAge() > 21));

        assertThat(peopleByAge)
                .isNotEmpty()
                .extractingByKey(false)
                .isEqualTo(expectedFalsePartition);
        
        assertThat(peopleByAge)
                .isNotEmpty()
                .extractingByKey(true)
                .isEqualTo(expectedTruePartition);

So there will be two partitions created by the partitionBy() operation:

false=[Person{name='Sara', age=20}, Person{name='Bob', age=20}, Person{name='Bill', age=3}, Person{name='Jill', age=11}]

and

true=[Person{name='Nancy', age=22}, Person{name='Paula', age=32}, Person{name='Paul', age=32}, Person{name='Jack', age=72}]

groupingBy()

Returns a Collector implementing a "group by" operation on input elements of the supplied type, grouping elements according to a classification function, and returning the results in a Map.

  • Group people by age
Map<Integer, List<Person>> expectedOutcome = Map.of(
        20, List.of(new Person("Sara", 20), new Person("Bob", 20)),
        22, List.of(new Person("Nancy", 22)),
        32, List.of(new Person("Paula", 32), new Person("Paul", 32)),
        3, List.of(new Person("Bill", 3)),
        72, List.of(new Person("Jack", 72)),
        11, List.of(new Person("Jill", 11))
);

Map<Integer, List<Person>> peopleGroupedByAge = createPeople().stream()
                .collect(groupingBy(Person::getAge));

assertThat(peopleGroupedByAge)
                .isNotEmpty()
                .hasSize(6)
                .isEqualTo(expectedOutcome);

The output when we perform the groupingBy() operation will produce:

{
  32=[Person{name='Paula', age=32}, Person{name='Paul', age=32}],
  3=[Person{name='Bill', age=3}],
  20=[Person{name='Sara', age=20}, Person{name='Bob', age=20}],
  22=[Person{name='Nancy', age=22}],
  72=[Person{name='Jack', age=72}],
  11=[Person{name='Jill', age=11}]
}