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 😏
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());
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 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);
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.
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.
- Pure functions do not change anything.
- Pure functions do not depend on anything that may change.
- Get the list of names, in uppercase, of those who are older than 30
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;
}
);
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 areduce
operation. Collectors
is a utility class.
- 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);
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}]
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}]
}