Introduction
A stream represents a sequence of elements and supports different kinds of operations that lead to the desired result. The source of a stream is usually a Collection or an Array, from which data is streamed from.
Streams differ from collections in several ways; most notably in that the streams are not a data structure that stores elements. They're functional in nature, and it's worth noting that operations on a stream produce a result and typically return another stream, but do not modify its source.
To "solidify" the changes, you collect the elements of a stream back into a Collection
.
The mathematical operation of finding an arithmetic mean is one we use fairly frequently, and there are many ways we go about performing it.
We'll be doing exactly that in this guide - we'll take a look at how to get a mean/average value for different numerical types within Java through built-in methods within the Collectors
class.
Note: It's worth noting that you can average the elements themselves, if they're numerical, or reduce them to a numerical representation and then average the reductions, if they aren't.
Collectors and Stream.collect()
Collectors represent implementations of the Collector
interface, which implements various useful reduction operations, such as accumulating elements into collections, summarizing elements based on a specific parameter, etc.
All predefined implementations can be found within the
Collectors
class.
You can also very easily implement your own collector and use it instead of the predefined ones, though - you can get pretty far with the built-in collectors, as they cover the vast majority of cases in which you might want to use them.
To be able to use the class in our code we need to import it:
import static java.util.stream.Collectors.*;
Stream.collect()
performs a mutable reduction operation on the elements of the stream.
A mutable reduction operation collects input elements into a mutable container, such as a Collection
, as it processes the elements of the stream.
Definition of the averaging_() Collectors
The Collectors
class has a variety of useful functions, and it so happens that it contains a few that allow us to find a mean value of input elements.
There's a total of three of them:
Collectors.averagingInt()
,Collectors.averagingDouble()
andCollectors.averagingLong()
.
Let's start off by taking a look at the method signatures:
public static <T> Collector<T,?,Double> averagingInt(ToIntFunction<? super T> mapper)
public static <T> Collector<T,?,Double> averagingDouble(ToDoubleFunction<? super T> mapper)
public static <T> Collector<T,?,Double> averagingLong(ToLongFunction<? super T> mapper)
Note: The generic T
in the method signatures represents the type of the input elements we're working with.
The ToIntFunction
, ToDoubleFunction
and ToLongFunction
from java.util.function
enable us to perform conversions (reductions) from object types to their primitive int
, double
or long
fields. Let's define a Student
class that we can reduce to a numerical field:
public class Student {
private String name;
private Double grade;
private Integer age;
private Long examsTaken;
// Constructor, getters and setters
Let's also instantiate our students in a List
:
List<Student> students = Arrays.asList(
new Student("John", 7.56, 21, 17L),
new Student("Jane", 8.31, 19, 9L),
new Student("Michael", 9.17, 20, 14L),
new Student("Danielle", 9.17, 21, 23L),
new Student("Joe", 8.92, 22, 25L)
);
Besides custom objects - we'll also take a look at how we can use the averaging collectors on primitive data types - that is Lists
consisted of only numerical elements.
Collectors.averagingInt()
This method returns a Collector
that produces the arithmetic mean of an integer-valued function applied to the input elements. In the case of no elements being present, the result is 0
.
Since all of the averaging methods are pretty straightforward we'll go straight to an example.
The first of the two examples will be using a simple List
consisted of Integers
. Let's say we have the following:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Double average = numbers.stream().collect(Collectors.averagingInt(Integer::intValue));
System.out.println(average);
We apply the .stream()
method to create a stream of Integer
objects, after which we use the previously discussed .collect()
method to collect the stream with the averagingInt()
collector.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Since the values are already integers, get the intValue
through a method reference, effectively performing a 1-to-1 mapping as our ToIntFunction
, as there's no conversion required:
3.0
Next on, in our Student
class, the only integer-valued field is age
. In the following example we'll be calculating the average age of all of our students:
Double averageAge = students.stream().collect(Collectors.averagingInt(Student::getAge));
System.out.println("Average student age is: " + averageAge);
The inner workings when using a field from a user-defined class is the same, the only difference is that we can't average the Student
instances, so we reduce them to their age. The ToIntFunction
in this case is a method reference to the getAge()
method, and through it, we reduce the Student
to their age.
This process stays the same in both of our upcoming methods as well, the only thing that'll change is which method we're referencing for the conversion functions. The output of our code snippet is:
Average student age is: 20.6
Collectors.averagingDouble()
This method returns a Collector
that produces the arithmetic mean of an double-valued function applied to the input elements. In the case of no elements being present, the result is 0
.
The Collectors.averagingDouble()
differs a tiny bit from averagingInt()
given the fact that it works with doubles. The average that's being returned can vary depending on the order in which values are processed, due to the accumulated rounding errors. Values sorted by increasing order tend to produce more accurate results.
If any value is NaN
or the sum at any point is NaN
- the mean will also be NaN
. Additionally, the double format can be represented with all consecutive integers in the range from -253 to 253.
Let's calculate the average value of a list of doubles:
List<Double> numbers = Arrays.asList(3.0, 8.0, 4.0, 11.0);
Double average = numbers.stream().collect(Collectors.averagingDouble(Double::doubleValue));
System.out.println(average);
This results in:
6.5
Now, let's apply the method to our Student
class. The grade
field is a double, so we'll use a method reference to that field's getter as the ToDoubleFunction
in the averagingDouble()
call:
Double averageGrade = students.stream().collect(Collectors.averagingDouble(Student::getGrade));
System.out.println("Average student grade is: " + averageGrade);
Running this gives us the following output:
Average student grade is: 8.62
Collectors.averagingLong()
The last of the averaging methods is Collectors.averagingLong()
. This method as well as the previous two, returns a Collector
that produces the arithmetic mean of a long-valued function applied to the input elements. If there are no elements average, 0
is returned.
As with the previous two, we can easily average a list of long values:
List<Long> numbers = Arrays.asList(10L, 15L, 1L, 3L, 7L);
Double average = numbers.stream().collect(Collectors.averagingDouble(Long::longValue));
System.out.println(average);
This results in:
7.2
Finally, our Student
class has a Long-valued examsTaken
field, for which we can calculate the mean of. We'll use a method reference to the getExamsTaken()
method as the ToLongFunction
:
Double averageExamsTaken = students.stream().
collect(Collectors.averagingLong(Student::getExamsTaken));
System.out.println("Average exam taken per student is: " + averageExamsTaken);
Running this outputs:
Average exam taken per student is: 17.6
Conclusion
Many a times we have a need for computing averages of multiple values, and using the provided averaging_() methods of the Collectors
class is one of the easiest and most efficient ways to do this in Java.
In this guide, we've gone through all three of them available within the aforementioned class, explained how each of them works on an example of a user-defined class and showed how we can use them in code to achieve the results we were aiming for.