Java 8 Streams & lambdas support strict FP
Java 8 lambdas are very useful in many situations to implement code in FP models in a compact way.
But there are situations where we may need to access / mutate an external state, which is not good practice according to FP practice.
(since Java 8 functional interfaces have strong input and output signatures, we cannot pass additional arguments)
For example:
class Country{
List<State> states;
}
class State{
BigInt population;
String capital;
}
class Main{
List<Country> countries;
//code to fill
}
Suppose the use case is to get a list of all capitals and the totality of all states in all countries
Normal implantation:
List<String> capitals = new ArrayList<>();
BigInt population = new BigInt(0);
for(Country country:countries){
for(State state:states){
capitals.add(state.capital);
population.add(state.population)
}
}
How can I implement the same with Java 8 streams in a more optimized way?
Stream<State> statesStream = countries.stream().flatMap(country->country.getStates());
capitals = statesStream.get().collect(toList());
population = statesStream.get().reduce((pop1,pop2) -> return pop1+pop2);
But the above implementation is not very efficient. Any other better way to manipulate multiple collections using Java 8 streams
source to share
If you want to collect multiple results in one pipeline, you must create a results container and a custom one Collector
.
class MyResult {
private BigInteger population = BigInteger.ZERO;
private List<String> capitals = new ArrayList<>();
public void accumulate(State state) {
population = population.add(state.population);
capitals.add(state.capital);
}
public MyResult merge(MyResult other) {
population = population.add(other.population);
capitals.addAll(other.capitals);
return this;
}
}
MyResult result = countries.stream()
.flatMap(c -> c.getStates().stream())
.collect(Collector.of(MyResult::new, MyResult::accumulate, MyResult::merge));
BigInteger population = result.population;
List<String> capitals = result.capitals;
Or stream twice as you do.
source to share
You can only use a stream once, so you need to create an aggregate that can be shrunk:
public class CapitalsAndPopulation {
private List<String> capitals;
private BigInt population;
// constructors and getters omitted for conciseness
public CapitalsAndPopulation merge(CapitalsAndPopulation other) {
return new CapitalsAndPopulation(
Lists.concat(this.capitals, other.capitals),
this.population + other.population);
}
}
Then you create a pipeline:
countries.stream()
.flatMap(country->
country.getStates()
.stream())
.map(state -> new CapitalsAndPopulation(Collections.singletonList(state.getCapital()), state.population))
.reduce(CapitalsAndPopulation::merge);
The reason it looks so ugly is because you don't have a strong syntax for structures like tuples or maps, so you need to create classes to make pipelines look pretty ...
source to share
Try it.
class Pair<T, U> {
T first;
U second;
Pair(T first, U second) {
this.first = first;
this.second = second;
}
}
Pair<List<String>, BigInteger> result = countries.stream()
.flatMap(country -> country.states.stream())
.collect(() -> new Pair<>(
new ArrayList<>(),
BigInteger.ZERO
),
(acc, state) -> {
acc.first.add(state.capital);
acc.second = acc.second.add(state.population);
},
(a, b) -> {
a.first.addAll(b.first);
a.second = a.second.add(b.second);
});
You can use AbstractMap.Entry<K, V>
instead Pair<T, U>
.
Entry<List<String>, BigInteger> result = countries.stream()
.flatMap(country -> country.states.stream())
.collect(() -> new AbstractMap.SimpleEntry<>(
new ArrayList<>(),
BigInteger.ZERO
),
(acc, state) -> {
acc.getKey().add(state.capital);
acc.setValue(acc.getValue().add(state.population));
},
(a, b) -> {
a.getKey().addAll(b.getKey());
a.setValue(a.getValue().add(b.getValue()));
});
source to share