16

I have a class Model with the following signature:

class Model {
        private String stringA;
        private String stringB;

        public Model(String stringA, String stringB) {
            this.stringA = stringA;
            this.stringB = stringB;
        }

        public String getStringA() {
            return stringA;
        }

        public String getStringB() {
            return stringB;
        }
}

I would like to map a List<Model> to a List<String> containing both stringA and stringB in a single stream

List<String> strings = models.stream()
                .mapFirst(Model::getStringA)
                .thenMap(Model::getStringB)
                .collect(Collectors.toList());

or:

List<String> strings = models.stream()
                .map(Mapping.and(Model::getStringA,Model::getStringB))
                .collect(Collectors.toList());

Of course none of them compiles, but you get the idea.

Is it somehow possible?

Edit:

Example:

Model ab = new Model("A","B");
Model cd = new Model("C","D");
List<String> stringsFromModels = {"A","B","C","D"};
Ousmane D.
  • 54,915
  • 8
  • 91
  • 126
Sunflame
  • 2,993
  • 4
  • 24
  • 48
  • 2
    When you say "both StringA and StringB", do you mean a concatenation? Or should your `List` contain the `StringA` for your `Model` at index `0`, then your `StringB` for your `Model` at index `0`, then [...] at index `1` and so on? – Mena Mar 29 '18 at 10:34
  • 2
    A Stream generally does not like having to increase in size, that is not how it normally behaves, you *can* achieve what you want by using `flatMap` but that will be ugly. Is there any reason you don't want to use traditional `for` loops? – luk2302 Mar 29 '18 at 10:35
  • @Mena I mean the second way, so `List strings` should contain every string from Model, both stringA and stringB. I will provide an example. – Sunflame Mar 29 '18 at 10:38
  • @luk2302 that is the actual solution but I was wondering if I could solve it in a stream – Sunflame Mar 29 '18 at 10:38
  • Then the answer is "yes, but it will neither be pretty nor better performance-wise" – luk2302 Mar 29 '18 at 10:39

2 Answers2

27

You can have a list of all the values one after another like so:

List<String> resultSet =
               modelList.stream()
                        .flatMap(e -> Stream.of(e.getStringA(), e.getStringB()))
                        .collect(Collectors.toList());

The function passed to flatMap will be applied to each element yielding a Stream<String>. The use of flatMap is necessary here to collapse all the nested Stream<Stream<String>> to a Stream<String> and therefore enabling us to collect the elements into a List<String>.

Ousmane D.
  • 54,915
  • 8
  • 91
  • 126
  • @Sunflame You're welcome. I've also added some additional explanation if it helps clarify the answer :). – Ousmane D. Mar 29 '18 at 10:58
  • 1
    Thanks, now it is a lot more clear how to use `flatMap`, I thought it used to merge `Collection`s or `Iterable`s in a single stream. It really helped. – Sunflame Mar 29 '18 at 12:11
7

flatMap is your friend here:

models
    .stream()
    .flatMap(model -> Stream.of(model.getStringA(),model.getStringB()))
    .collect(Collectors.toList());

flatMap takes a type R and expects to give a Stream (from list,collections, Array) of new Type RR back. For each 1 model you get n new elements (in this case StringA and StringB):

{model_1[String_A1,String_B1] , model_2[String_A2,String_B2] , model_3[String_A3,String_B3]}

All your n elements { [String_A1,String_B1], [String_A2,String_B2],[String_A3,String_B3]}

are then flattened that are placed into a new Stream with structure

{String_A1,String_B1,String_A2,String_B2,String_A3,String_B3}

of Type String. That's how you have a new Stream.

You can use flatMap e.g when you have many collections/streams of elements that need to be merged into only one. For more simple explanations, check this answer

arthur
  • 3,245
  • 4
  • 25
  • 34