What is the difference between the map() and flatMap() functions of Java 8?

Asked

Viewed 4,166 times

11

What is the difference between the functions map() and flatMap() made available by the API stream java 8?

2 answers

11

Both take the elements of one stream data (usually a solution such as array or ArrayList) and each element will have an action to be defined below.

The difference that flatMap() can do this in streams which have dimensions (it flattens the data to be linear), so each element of that data collection will be used regardless of whether it is nested in that collection. When you have data that is linear you never need to use it.

Let’s say you have a list of lists, the function map() would take the internal lists, but what you want is the elements of those lists, so just flatMap() resolves.

Another example is to have a list of strings and you want the characters. While map() pick up the texts one by one, flatMap() would pick up the characters.

Example:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Main {
    public static void main(String[] args) {
        List<List<Integer>> listOfListofInts = Arrays.asList(Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6), Arrays.asList( 7, 8, 9));
        System.out.println(listOfListofInts.stream().flatMap(List::stream).collect(Collectors.toList()));
        System.out.println(listOfListofInts.stream().map(List::stream).collect(Collectors.toList()));

    }
}

Behold working in the ideone. And in the repl it.. Also put on the Github for future reference.

10


Let’s start with the meaning of words?

So the flatMap will map and flatten. What does that mean? Before you answer, allow me to take a walk to talk about manipulation of these data streams.

One of the characteristics that make the use of streams is to manipulate their individual elements without having to worry about how the data arrives. In the case of map, I take one element and turn it into another element. Normally you do it to change one type to another or do an operation on it. But sometimes a single original element can be mapped into several other elements, and you need to work on top of those derived elements.

For example: I have a class called CategoriaCliente, that within it a list of TipoCliente, and within TipoCliente I have a list of SegmentacaoCliente. I need to display all customer segmentations for a particular category. How would you do?

Imperative way, you could do the following:

// class CategoriaCliente

public Set<SegmentacaoCliente> getSegmentacoesNavegaveis() {
  Set<SegmentacaoCliente> segmentacoes = new HashSet<>();

  for (TipoCliente tipo: tiposCliente) {
    for (CategoriaCliente categoria: tipo.getCategoriasCliente()) {
      segmentacoes.add(categoria);
    }
  }

  return segmentacoes;
}

Functionally, the idea would be the following:

  1. caught a stream of my tiposCliente
  2. transform a TipoCliente in several CategoriaCliente
  3. collect this stream in a set

Step 2. implies that I am using an atomic element (TipoCliente) and transforming into several others. This is the implementation:

// class CategoriaCliente

public Set<SegmentacaoCliente> getSegmentacoesNavegaveis() {
  return tiposCliente.stream()
      .flatMap(tipo -> tipo.getCategoriasCliente().stream())
      .collect(Collectors.toSet());
}

I have another interesting case here with flatMap: I need to know, on one request, how much of each family is being sold. The structure of objects is not complicated: an order has items, each item has a product, each product has a family, a family can have a mother family or be the root family (there are no family cycles, only family trees).

In the family, there is already a method called getFamiliasAncestrais(), who takes herself and her entire lineage of predecessors to the root. The code of this rescue is tedious, so it’s not even worth putting its internal workings.

So we started with the request. Functionally, I would do the following:

  1. got the items
  2. map the item to a collection of pairs, the pairs composed by the family and its quantity of sale
  3. collected in a cluster where the key is the family that is in the pair and the value is the sum of the sales quantities

That’s what the code looks like:

Map<FamiliaProduto, BigDecimal> mapQtItensFamilias = pedido.getItensPedido().stream()
    .flatMap(
       ip -> ip.getProduto().getFamilia().getFamiliasAncestrais()
                 .stream()
                 .map(f -> Pair.getPair(f, ip.getQtItem()))
    )
    .collect(Collectors.groupingBy(Pair::left, HashMap::new, BigDecimalUtils.summing(Pair::right)));

The code I’m putting here is really simplified, because the target where my code is actually running (traditional Java on server, Android, GWT and Totalcross) has its specific limitations.

In the case of Totalcross, there is no support for lambda methods, but I outline this using Retrolambda. I have also not provided access to stream of Java 8, but that also doesn’t limit me because I made a library of "compatibility". For example, instead of using pedido.getItensPedido().stream() I need to use new Stream<>(pedido.getItensPedido()), but the general use after the creation of stream is the same

Another problem I face is that GWT and Java running on the server use java.math.BigDecimal, which is not/was easily recognized by Totalcross, which uses totalcross.util.BigDecimal (including the methods BigDecimal.remainder(BigDecimal) has different returns), so I needed to create a class to envelope and isolate these environments, working on top of their correct base classes. That’s why I used my own collector BigDecimalUtils.<T>summing(Function<T,BigDecimal>), to thus work upon my own class.

Imperative way (using the facilities of Java 8), it would be something like this:

Map<FamiliaProduto, BigDecimal> mapQtItensFamilias = new HashMap<>();
for (ItemPedido ip: pedido.getItensPedido()) {
  BigDecimal qtItem = ip.getQtItem();
  for (FamiliaProduto fp: ip.getProduto().getFamilia().getFamiliasAncestrais()) {
    mapQtItensFamilias.merge(fp, qtItem, BigDecimal::add);
  }
}

Now, what if you didn’t have the facilities of Java 8?

Map<FamiliaProduto, BigDecimal> mapQtItensFamilias = new HashMap<>();
for (ItemPedido ip: pedido.getItensPedido()) {
  BigDecimal qtItem = ip.getQtItem();
  for (FamiliaProduto fp: ip.getProduto().getFamilia().getFamiliasAncestrais()) {
    BigDecimal old = mapQtItensFamilias.get(fp);
    if (old != null) {
      mapQtItensFamilias.put(fp, old.add(qtItem));
    } else {
      mapQtItensFamilias.put(fp, qtItem);
    }
  }
}

Another interesting case: when I needed to make a dependency injector in Totalcross (the project was going through changes faster than we would be able to continue "injecting in the hand" maintaining sanity).

I asked some questions 1 2 3 that collaborated with the implementation of this injector

Basically, the idea of this injector was the following: I register in it objects with several getters. Each of these getters (if you pass any arbitrary criteria that I can pass at the time of registration) behaves similar to a method with annotation @Bean of an object annotated with @Configuration of Spring, but I can’t name them, and I identify them solely by their interfaces. Then, having all these objects created, I check their setters to see which interfaces they depend on (I can leave the dependency open, as if it were a @Autowired(required = false)).

After creating all the objects and correctly mapping their setters, I make the settlement. The heart of this settlement is mapping the interfaces to the objects that implement them. After I complete the entire record of my "Beans" and before making the injection, I make this normalization to point interface -> [obj_impl1 obj_impl2..]:

// class InjetorDependencias
  ...

  private ArrayList<MetadataInjection> managedBeans = new ArrayList<>();
  private boolean normalized = false;
  private Map<Class<?>, List<Pair<Object, Class<?>>>> metadataMultimap;

  ...

  private void normalize() {
    if (!this.normalized) {
      metadataMultimap = managedBeans.stream().flatMap(this::explodeMetadata).collect(Collectors.groupingBy(p -> p.getValue()));
      this.normalized = true;
    }
  }

  private Stream<Pair<Object, Class<?>>> explodeMetadata(MetadataInjection metadata) {
    Object obj = metadata.getObj();
    
    return metadata.getIfaces().stream().map(iface -> Pair.getPair(obj, iface));
  }

  ...
}

Note here the use of flatMap to monitor objects and their various classes.

If it were done without streams, but with the right to Java 8:

// class InjetorDependencias
  ...

  private ArrayList<MetadataInjection> managedBeans = new ArrayList<>();
  private boolean normalized = false;
  private Map<Class<?>, List<Pair<Object, Class<?>>>> metadataMultimap;

  ...

  private void normalize() {
    if (!this.normalized) {
      metadataMultimap = new HashMap<>();
      for (MetadataInjection bean: managedBeans) {
        Object obj = bean.getObj();
        for (Class<?> iface: bean.getIfaces()) {
          metadataMultimap.compute(iface, (k, listaObjetos) -> {
            if (listaObjetos == null) {
              listaObjetos = new ArrayList<>();
            }
            listaObjetos.add(obj);
            return listaObjetos;
          });
        }
      }
      this.normalized = true;
    }
  }

  ...
}

Now without the facilities of Java 8:

// class InjetorDependencias
  ...

  private ArrayList<MetadataInjection> managedBeans = new ArrayList<>();
  private boolean normalized = false;
  private Map<Class<?>, List<Pair<Object, Class<?>>>> metadataMultimap;

  ...

  private void normalize() {
    if (!this.normalized) {
      metadataMultimap = new HashMap<>();
      for (MetadataInjection bean: managedBeans) {
        Object obj = bean.getObj();
        for (Class<?> iface: bean.getIfaces()) {
          List<Object> listaObjetos = metadataMultimap.get(iface);
          if (listaObjetos == null) {
            listaObjetos = new ArrayList<>();
            metadataMultimap.put(iface, listaObjetos);
          }
          listaObjetos.add(obj);
        }
      }
      this.normalized = true;
    }
  }

  ...
}

Finally, there’s one last interesting case I use flatMap. I have a table name map for a triple (Map<String, Collection<ImmutableTriple<String, String, String>>>). This triple consists of: key, status (ok or fail) and message. I need to pass this information to the database, but my SQL Server JDBC driver has a set parameter limitation. If I’m not mistaken it was around 700 or 600 and little at the time I wrote, or else I mistook it with the documentation of another driver that had this size in the limitation. In addition to each triple value, I also need, I need to inform the bank the name of the table related to each triple.

To process this information there was already a Procedure in the bank that receives a tabular variable with this information. What was it that I did, to use as much as possible:

  • I created a CTE to insert the information of the triples through parameters PreparedStatement
  • of this CTE, I made an insertion in a tabular variable along with the mapping key
  • I passed that variable to Procedure

As I was already using the MyBatis at various points of the system and it did not show a hindrance, I used it to make the assembly of my PreparedStatement to make iterations.

That said, I did the following:

  • I took every 20 triples and saved it in a sublist
  • associated the name of each table (mapping key) with each sublist
  • performed my logging function (ackMapper.registraAck) passing to the table and to the sub-list

The code was this:

// import com.google.common.collect.Lists;

Map<String, Collection<ImmutableTriple<String, String, String>>> tabelasAcks = contentHandler.getResponseMap();

tabelasAcks.entrySet().stream()
    .flatMap(e -> Lists.partition(e.getValue().stream().collect(Collectors.toList()), 20).stream().map(l -> ImmutablePair.of(e.getKey(), l)))
    .forEach(sublistaPair -> ackMapper.registraAck(sublistaPair.getLeft(), sublistaPair.getRight()));

Yes, if the return of contentHandler.getResponseMap were Map<String, List<...>> I didn’t have to do it e.getValue().stream().collect(Collectors.toList()), would simply e.getValue(), but I don’t remember the reason for returning Collection<...> in place of List<...>

Doing this in an imperative way:

// import com.google.common.collect.Lists;

Map<String, Collection<ImmutableTriple<String, String, String>>> tabelasAcks = contentHandler.getResponseMap();

for (EntrySet<String, Collection<ImmutableTriple<String, String, String>>> e: tabelasAcks.entrySet()) {
  String tableName = e.getKey();
  for (List<ImmutableTriple<String, String, String>> sublista: Lists.partition(new ArrayList<>(e.getValue()), 20)) {
    ackMapper.registraAck(tableName, sublista);
  }
}

If I didn’t have the com.google.common.collect.Lists on my side:

Map<String, Collection<ImmutableTriple<String, String, String>>> tabelasAcks = contentHandler.getResponseMap();

for (EntrySet<String, Collection<ImmutableTriple<String, String, String>>> e: tabelasAcks.entrySet()) {
  List<ImmutableTriple<String, String, String>> lista = new ArrayList<>();
  String tableName = e.getKey();
  int i = 0;
  for (ImmutableTriple<String, String, String> tripla: e.getValue()) {
    i++;
    lista.add(tripla);
    if (i == 20) {
      i = 0;
      ackMapper.registraAck(tableName, lista);
      lista.clear();
    }
  }
  if (i > 0) {
    ackMapper.registraAck(tableName, lista);
  }
}

Browser other questions tagged

You are not signed in. Login or sign up in order to post.