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

+3


source to share


3 answers


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.

+5


source


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 ...

+1


source


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()));
        });

      

+1


source







All Articles