Hello Developers!
In this article I wanna show you a few information about accumulating calculations on each elements during stream process in Java. According to my latest research, the functional paradigm in Java very often uses a one popular process which is known as map-filter-reduce. I will basically focus on the third one part – reduction operation. If you haven’t used it yet, it’s worth checking how it works first, and then I’ll discuss the theory.
Let’s take the iterate method which returns an infinite sequential ordered DoubleStream
produced by iterative application of the provided DoubleUnaryOperator
. I set the value 1 as the initial element (seed).
double result = DoubleStream
.iterate(1, d -> d + 2)
.limit(10)
.reduce((x, y) -> x + y).orElse(0.0);
The DoubleBinaryOperator here is supplied by a lambda expression. As you can see, it takes two double values and returns their sum. The stream could be empty if we had added a filter so the result is an OptionalDouble . Chaining the orElse method to itindicates that if there are no elements in the stream, the return value should be zero.
The reduce method is added in many interfaces and is overloaded in different versions. Today I will present, for example, cases related to Double values. This often gives a little more possibilities and does not require explicit typing (mainly because it allows you to handle fractions). The DoubleStream interface has two overloaded reduce methods:
OptionalDouble reduce(DoubleBinaryOperator op);
double reduce(double identity, DoubleBinaryOperator op);
The first method performs reduction on the elements of this stream using associative accumulation function.
The second function performs reduction on the elements using the provided identity value and accumulation function.
In the lambda expression, you can think of the first argument of the binary operator as an accumulator, and the second argument as the value of each element in the stream.
To show the example given at the beginning more clearly, below I put the code in which the block was added inside the lambda. Here the iterated values are printed to the console:
AtomicInteger counter = new AtomicInteger(0);
DoubleStream
.iterate(1, d -> d + 2)
.limit(10)
.reduce((x, y) -> {
System.out.printf("Iteration %s - x=%s / y=%s\n", counter.incrementAndGet(), x, y);
return x+y;
}).orElse(0.0);
Iteration 1 - x=1.0 / y=3.0
Iteration 2 - x=4.0 / y=5.0
Iteration 3 - x=9.0 / y=7.0
Iteration 4 - x=16.0 / y=9.0
Iteration 5 - x=25.0 / y=11.0
Iteration 6 - x=36.0 / y=13.0
Iteration 7 - x=49.0 / y=15.0
Iteration 8 - x=64.0 / y=17.0
Iteration 9 - x=81.0 / y=19.0
As the output shows, the initial values of x and y are the first two values of the range. The value returned by the binary operator becomes the value of x (i.e., the accumulator) on the next iteration, while y takes on each value in the stream.
We can also provide the initial value for our accumulator (x variable). Below you can see the code fragment, in which I’ve used another overloaded version of reduce method. I omitted the value of orElse here as well, because adding this attribute ensures the compiler that the stream will contain at least one value.
double reduce(double identity, DoubleBinaryOperator op);
double reduce = DoubleStream
.iterate(1, d -> d + 2)
.limit(10)
.reduce(0, (x, y) -> x + y);
Demonstrated initial value for accumulator referred to the first argument. In the signature of that method, we can see that value is called identity. It means that you as developer should supply a value to the binary operator that, when combined with any other value, returns the other value.
String accumulator
When combining many strings, we can also use the discussed accumulator to create one string. We’ll use the Stream interface for this. It has an overloaded collect method.
The signature of this method contains the necessary arguments. It takes a Supplier for the collection, a BiConsumer for adding a single element to collection and another BiConsumer that combines two collections. In this example, the natural accumulator is popular StringBuilder.
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
This approach can be expressed with the method reference lambda, or it can be more simply expressed using method references as presented below:
Stream.of("Nevada", "Wyoming", "Virginia", "Wisconsin", "Missouri", "Maine", "Ohio")
.collect(() -> new StringBuilder(),
(sb, str) -> sb.append(str),
(sb1, sb2) -> sb1.append(sb2))
.toString();
Stream.of("Nevada", "Wyoming", "Virginia", "Wisconsin", "Missouri", "Maine", "Ohio")
.collect(StringBuilder::new,
StringBuilder::append,
StringBuilder::append)
.toString();
At the end of that article, I want to tell you about the general from of reduce process. Below I have added a more complicated form of the reduce method:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Looks pretty complicated, doesn’t it? If something is difficult to understand, it is always worth trying from a practical example. Our exercise is to use the list of all US states and add each of them to a separate, new Map. The key in our map is the identifiers, while the stored value is the state name. What will be done here can be done much easier, but I wanted to find an example that will demonstrate well how this version of the „reduce” method works.
@Getter
@Setter
@AllArgsConstructor
public class State {
private Long id;
private String name;
private String capital;
}
List<State> states = Arrays.asList(
new State(1L, "Alaska", "Juneau"),
new State(2L, "Arizona", "Phoenix"),
new State(3L, "Colorado", "Denver"),
new State(4L, "Hawaii", "Honolulu"),
new State(5L, "Nevada", "Carson City"),
new State(6L, "New York", "Albany")
);
HashMap<Long, State> stateMap = books.stream()
.reduce(new HashMap<Integer, State>(),
(map, state) -> {
map.put(state.getId(), state);
return map;
},
(map1, map2) -> {
map1.putAll(map2);
return map1;
});
stateMap.forEach((k,v) -> System.out.println(k + ": " + v));
The first argument to the reduce method is the identity value for the combiner function. The second argument is a function that adds a single state to a Map. The last argument is a combiner , which is required to be a BinaryOperator . In this case, the provided lambda expression takes two maps and copies all the keys from the second map into the first one and returns it.
Where else can we use this accumulator? Let’s stay with the reduce function.
Optional<T> reduce(BinaryOperator<T> accumulator)
If you don’t remember, let me remind you that BinaryOperator has the same return value as the input values. The first element in the BinaryOperator is normally an accumulator, while the second element takes each value of the stream.
BigDecimal total = Stream.iterate(BigDecimal.ONE, n -> n.add(BigDecimal.ONE))
.limit(10)
.reduce(BigDecimal.ZERO, (acc, val) -> acc.add(val));
As usual, whatever is returned by the lambda expression becomes the value of the acc variable on the next iteration. In this way, the calculation accumulates the values of the first 10 BigDecimal instances.
To conclusion, reduction operations with accumulator component are fundamental to the functional programming idiom. The best news is that once you understand how to use reduce in Java 8, you know how to use the same operation in other languages. They all work the same way