2

This question is related to the Java 8 map and flatMap that is present both in Streams and Optionals. It is worth to note that C# has a similar construct named SelectMany.

I have learned about the two methods, in particular that in Streams you can use flatMap to get a Collection<T> from a Collection<Collection<T>> which is what I want.

In my example I have a nested class structure (from a DTD I have no control over) in which I want to compute a sum of values. I will not redact the class names for laziness.

class DatiTelematico {
    private Adempimento adempimento;
}

class Adempimento {
    private List<DatiNegozio> datiNegozio;
}

class DatiNegozio {
    private List<Negozio> negozio;
}

class Negozio {
    private List<Tassazione> tassazione;
}

class Tassazione {
    private BigDecimal importo;
}

Given an Optional instance of a DatiTelematico class I would like to sum (importo) from telematico join adempimento join datiNegozio join negozio join tassazione.

The best I could do was to use nested lambdas and plain map method

optionalTelematico.map(DatiTelematico::getAdempimento)
  .map(Adempimento::getDatiNegozio)
  .map(l -> l.stream().map(DatiNegozio::getNegozio)
             .map(n -> n.stream()
                        .map(Negozio::getTassazione)
                        .map(t -> t.stream()
                                   .map(Tassazione::getImporto)
                                   .reduce(BigDecimal.ZERO,
                                           BigDecimal::add))
                        .reduce(BigDecimal.ZERO, BigDecimal::add))
             .reduce(BigDecimal.ZERO, BigDecimal::add))
  .orElse(BigDecimal.ZERO));

I have tried to start writing something like

optionalTelematico.map(DatiTelematico::getAdempimento) .map(Adempimento::getDatiNegozio) .map(l->l.stream().flatMap(n->n.getNegozio().stream().flatMap(s->s.getTassazione().stream().flatMap(Tassazione::getImporto)))....TBD

But then I get a compiler error

Method binding must be directly contained in a class (OTJLD A.3.1).

How do I smartly switch from an Optional<T> (singleton) to a Collection<U> that is to be summed over? I am asking this to increase my knowledge of Java lambdas.

usr-local-ΕΨΗΕΛΩΝ
  • 26,101
  • 30
  • 154
  • 305

3 Answers3

3

Both Optional and Stream actually only represent a single piece of data - in case of optional, that piece of data might be either absent or present, in case of stream, there may be other pieces of data coming before or after, but in current moment we have this piece only.

Now,

map method is essentially a kind of type transformation for both Optional and Stream: mapping takes a function I -> R, applying which one can make transformation Optional<I> -> Optional<R> (or Stream<I> -> Stream<R>).

flatMap method is a kind of transformation that can:

  • Transform optional value into another optional (possibly empty). That means a function type I -> Optional<R>
  • Transform each item in stream into another stream (having 0..n number of elements in it). That means function type I -> Stream<R>. Note that for streams this operation can change the number of elements that are contained in stream (but it won't change the fact that there always effectively one stream element being processed at a time).

In your particular case, by making transformation to optional, you may obtain up to Optional<List<DatiNegozio>> directly:

Optional<List<DatiNegozio>> optDatiNegozio = optionalDatiTelematico
  .map(DatiTelematico::getAdempimento) // Optional<Adempimento>
  .map(Adempimento::getDatiNegozio);

Every List<DatiNegozio> you can easily convert to Optional<BigDecimal> summing and accessing elements via Stream:

static Optional<BigDecimal> sumImporto(List<DatiNegozio> datiNegozio) {

  return datiNegozio.stream() // Stream<DatiNegozio>
    .map(DatiNegozio::getNegozio)                 // Stream<List<Negozio>>

    // unroll stream of collections into a stream of collection elements
    .flatMap(List::stream)                        // Stream<Negozio>
    .map(Negozio::getTassazione)                  // Stream<List<Tassazione>>

    // again, unroll stream of collections into a stream of collection elements
    .flatMap(List::stream)
    .map(Tassazione::getImporto)                  // Stream<BigDecimal>

    // last thing we need to do is just reduce
    .reduce(BigDecimal::add);
  }

As you can see, second snippet allows you to convert List<DatiNegozio> into an Optional<BigDecimal>. After this, you have two options (stylistic choice):

  1. There is a variant of reduce that yields BigDecimal instead of Optional<BigDecimal>:

    .reduce(BigDecimal.ZERO, BigDecimal::add); // it yields concrete type instead of optional because even in case when there is no elements in stream, we can at least return value from which we started - ZERO

  2. You can use second code snippet to produce Function that is usable in flatMap-ing an optional:

    optionalDatiTelematico .map(DatiTelematico::getAdempimento) .map(Adempimento::getDatiNegozio) .flatMap(Example::sumImporto) // reference to method from 2nd code snippet .orElse(BigDecimal.ZERO); // what to do if we had Optional.empty at any point

M. Prokhorov
  • 3,894
  • 25
  • 39
  • I think the correct answer is a combination of both yours and @Vlad's answers. Thanks for the detailed explanation. I will expand later – usr-local-ΕΨΗΕΛΩΝ Feb 14 '17 at 12:35
  • @usr-local-ΕΨΗΕΛΩΝ, yep, his answer and mine are pretty much the same except for some stylistic choises: he chooses to materialize `Optional` as soon as possible, while I chose to `flatMap` over it. Result should be the same, which is a good example of how flexible these APIs are. – M. Prokhorov Feb 14 '17 at 12:44
  • In fact I would like to expand on what I got from the two answers, for better understanding by posterity. Vlad simply highlights that in order to use the correct `flatMap` that **flattens** the list, one has to explicitly unwrap the singleton-optional into a concrete stream (not an `Optional` as mistaken). You gave a correct explanation of the meaning of the method, the other author added a useful `orElseGet` that more than plain style gives understanding of the object involved in every step of the method chain (what you did exactly in commenting the inner mapping method sumImporto). – usr-local-ΕΨΗΕΛΩΝ Feb 14 '17 at 13:03
1

You can use Collection.stream() method to convert Collection to Stream and use it in flatMap. So combination of .map(d -> d.getList()).flatMap(Collection::stream) returns stream for all internal lists of Stream<D>.

In your case it can looks like:

    Optional.of(datiTelematico)
            .map(DatiTelematico::getAdempimento)
            .map(Adempimento::getDatiNegozio)
            .map(Collection::stream)
            .orElseGet(Stream::empty)
            .map(DatiNegozio::getNegozio)
            .flatMap(Collection::stream)
            .map(Negozio::getTassazione)
            .flatMap(Collection::stream)
            .map(Tassazione::getImporto)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
Vlad Bochenin
  • 3,007
  • 1
  • 20
  • 33
  • Small clarification and comparison with [my answer](http://stackoverflow.com/a/42226150/7470253): @Vlad's solution unwraps optional by replacing it with empty stream in case there is no value inside the optional (or any of the mappings yielded optional value). My answer keeps the "original" optional and flatMaps it only once. Perceived benefit of Vlad's solution: there is only one chain of methods. However, depending on implementation details of `Stream.empty`, Vlad's solution still does stuff with stream if original optional is empty, whereas my solution does not. – M. Prokhorov Feb 14 '17 at 15:01
0

Let say

Optional<DatiNegozio> abc = optionalTelematico.map(DatiTelematico::getAdempimento)
  .map(Adempimento::getDatiNegozio)

Now when you say abc.map(xyz). xyz must be function which takes instance of DatiNegozio as one and only argument. In your case xyz is a lambda which takes one parameter l whose type should be DatiNegozio. You are now doing l.stream() which throws compile error beacsue stream() does not exist in l(instance ofDatiNegozio).

Ashwani
  • 1,938
  • 11
  • 15