44

I have a List<Valuta> which can be represented (simplified) JSON-style:

[ { codice=EUR, description=Euro, ratio=1 }, { codice=USD, description=Dollars, ratio=1.1 } ]

I want to transform that in a Map<String, Valuta> like this:

{ EUR={ codice=EUR, description=Euro, ratio=1 }, USD={ codice=USD, description=Dollars, ratio=1.1 }}

I wrote this one-liner:

getValute().stream().collect(Collectors.groupingBy(Valuta::getCodice));

but this returns a Map<String, List<Valuta>> instead of what I need.

I suppose mapping() function would work for me, but don't know how.

Tunaki
  • 132,869
  • 46
  • 340
  • 423
Fabio B.
  • 9,138
  • 25
  • 105
  • 177

6 Answers6

67

Actually, you need to use Collectors.toMap here instead of Collectors.groupingBy:

Map<String, Valuta> map = 
    getValute().stream()
               .collect(Collectors.toMap(Valuta::getCodice, Function.identity()));

groupingBy is used to group elements of a Stream based on a grouping function. 2 Stream elements that will have the same result with the grouping function will be collected into a List by default.

toMap will collect the elements into a Map where the key is the result of applying a given key mapper and the value is the result of applying a value mapper. Note that toMap, by default, will throw an exception if a duplicate is encountered.

Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • 4
    How to do the same with `groupingBy`? I cannot use `toMap` because I must expect duplicate keys from the key mapper. I see `groupingBy` has signature for downstream collector as 2nd argument but couldn't make it work yet. – user909481 Aug 14 '17 at 09:38
  • Lets say I want Map instead of Map, I need Valuta.getDescription() , basically, is that possible? – user1735921 Nov 25 '19 at 07:57
  • Be aware that `Collectors.toMap` will throw an exception if you somehow have duplicate keys! They are not completely equivalent. – jocull Jan 17 '20 at 14:04
  • 1
    Discovered this - you might be able to use the `mergeFunction` of `.toMap` to simply drop or replace any duplicates and avoid extra object allocations: https://stackoverflow.com/a/32313069/97964 – jocull Jan 27 '20 at 14:28
43

It's a bit late in the game, but try this:

Map<String, Valuta> map = 
    getValute().stream()
               .collect(Collectors.groupingBy(Valuta::getCodice,
                            Collectors.collectingAndThen(
                                Collectors.toList(), 
                                values -> values.get(0))));
Philippe
  • 6,703
  • 3
  • 30
  • 50
  • This is what i am finding for – BaiJiFeiLong Jun 06 '18 at 08:09
  • 4
    this is what's needed in case you actually do have duplicates, but don't want to use it – fricke Oct 08 '18 at 11:45
  • 3
    You might be able to use the `mergeFunction` of `.toMap` to simply drop any duplicates and avoid extra object allocations: https://stackoverflow.com/a/32313069/97964 – jocull Jan 27 '20 at 14:28
  • 8
    Why create a list in the stream? Here your solution adapted without creating the list: `getValute().stream().collect(groupingBy(Valuta::getCodice, reducing(null, identity(), (first, last) -> last)))` – toongeorges Jun 07 '20 at 20:59
13

You could use Collectors.toMap(keyMappingFunction, valueMappingFunction)

Map<String, Valuta> map = list
        .stream()
        .collect(Collectors.toMap(Valuta::getCodice, v -> v));

You can replace v->v with Function.identity() if you find it more readable. Note that toMap, by default, will throw an exception if a duplicate is encountered.

rogerdpack
  • 62,887
  • 36
  • 269
  • 388
Pshemo
  • 122,468
  • 25
  • 185
  • 269
11

The toMap version which opts to choose the 1st value on collisions instead of throwing an exception is:

Collectors.toMap(keyMapper, valueMapper, mergeFunction) ex:

Map<String, Valuta> map = list
        .stream()
        .collect(Collectors.toMap(Valuta::getCodice, v -> v, (v1, v2) -> v1));
rogerdpack
  • 62,887
  • 36
  • 269
  • 388
3ygun
  • 1,192
  • 12
  • 14
  • Does the merge function preserve ordering? i.e. is it guaranteed that v1 always appears before v2 in list? – andresp Feb 28 '22 at 14:45
  • 1
    @andresp If you're working with `stream()` called on an `ArrayList` (which uses it's ordered iterator) than yes. Otherwise, it's dependent on your collection see something like https://stackoverflow.com/a/40583642/6480404 – 3ygun Feb 28 '22 at 19:37
1

Not completely related to this question but quiet similar case .If you want to group list by multiple params and after that need return object of different type as value then you can try this solution . Hope helps to someone!

public Map<Integer, Map<String, ObjectTypeB>> setObjectTypeB(List<ObjectTypeA> typeAList) {

   Map<Integer, Map<String, ObjectTypeB>> map = typeAList.stream()
        .collect(groupingBy(ObjectTypeA::getId,
         groupingBy(ObjectTypeA::getDate, 
         collectingAndThen(mapping(this::getObjectTypeB,toList()),values -> values.get(0)))));    
    return map;
}

    public ObjectTypeB getObjectTypeB(ObjectTypeA typeA) {
       ObjectTypeB typeB = new ObjectTypeB();
       typeB.setUnits(typeA.getUnits());
       return typeB;
}
0

Here are 3 methods.

public class Test1 {
  static class Foo {
    public int id, targetCost, actualCost;
    public String ref;

    public Foo(int id, String ref, int actualCost, int targetCost) {

      this.id = id;
      this.targetCost = targetCost;
      this.actualCost = actualCost;
      this.ref = ref;
    }

    public int getId() {
      return id;
    }

    public void setId(int id) {
      this.id = id;
    }

    public int getTargetCost() {
      return targetCost;
    }

    public void setTargetCost(int targetCost) {
      this.targetCost = targetCost;
    }

    public int getActualCost() {
      return actualCost;
    }

    public void setActualCost(int actualCost) {
      this.actualCost = actualCost;
    }

    public String getRef() {
      return ref;
    }

    public void setRef(String ref) {
      this.ref = ref;
    }

    @Override
    public String toString() {
      return " [id=" + id + ", targetCost="
    + targetCost + ", " + "actualCost=" 
          + actualCost + ", ref=" + ref
          + "]";
    }

  }// foo

  public static void main(String[] args) {

    List<Foo> list = Arrays.asList(

        new Foo(1, "P1", 300, 400), new Foo(2, "P2", 600, 400), new Foo(3, "P3", 30, 20),
        new Foo(3, "P3", 70, 20), new Foo(1, "P1", 360, 40), new Foo(4, "P4", 320, 200),
        new Foo(4, "P4", 500, 900)

    );
    // Method 1  : 
    Map<Integer, List<Foo>> collect = list.stream()
        .collect(
            Collectors.groupingBy(
                Foo::getId, 
                Collectors.collectingAndThen(

            Collectors.toList(),

            Function.identity()

        )// andthen

        )// gr

        );
    System.out.println(collect);
    /*
    {
        1=[ [id=1, targetCost=400, actualCost=300, ref=P1],
        id=1, targetCost=40, actualCost=360, ref=P1]],

         2=[ [id=2, targetCost=400, actualCost=600, ref=P2]],

          3=[ [id=3, targetCost=20, actualCost=30, ref=P3], 
           [id=3, targetCost=20, actualCost=70, ref=P3]], 

           4=[ [id=4, targetCost=200, actualCost=320, ref=P4], 
            [id=4, targetCost=900, actualCost=500, ref=P4]]

          }
  */

    // Method 2

    Map<Integer, List<Foo>> collect2 = list.stream().collect(
        Collectors.groupingBy(
            Foo::getId, 
            Collectors.mapping(
                Function.identity(), 
                Collectors.toList())));

    System.out.println(collect2);
    /*
     {
  1=[ [id=1, targetCost=400, actualCost=300, ref=P1], 
   [id=1, targetCost=40, actualCost=360, ref=P1]],

    2=[ [id=2, targetCost=400, actualCost=600, ref=P2]],

     3=[ [id=3, targetCost=20, actualCost=30, ref=P3],
       [id=3, targetCost=20, actualCost=70, ref=P3]],

        4=[ [id=4, targetCost=200, actualCost=320, ref=P4], 
         [id=4, targetCost=900, actualCost=500, ref=P4]]

       }

*/

    // Method 3 

    // If you need to compare something the you can use Compare.comparing

     Map<Integer, List<Foo>> collect3 = list
     .stream()
     .sorted( Comparator.comparing(Foo::getId)
         .thenComparing(Foo::getActualCost)
         .thenComparing(Foo::getTargetCost)    )

     .collect(                 

    Collectors.groupingBy(ch -> ch.id)     



         );

     System.out.println(collect3);


/*
{
  1=[ [id=1, targetCost=400, actualCost=300, ref=P1],  
  [id=1, targetCost=40, actualCost=360, ref=P1]],

   2=[ [id=2, targetCost=400, actualCost=600, ref=P2]],

    3=[ [id=3, targetCost=20, actualCost=30, ref=P3], 
     [id=3, targetCost=20, actualCost=70, ref=P3]],

      4=[ [id=4, targetCost=200, actualCost=320, ref=P4], 
       [id=4, targetCost=900, actualCost=500, ref=P4]]

     }
*/


  }// main

}
Soudipta Dutta
  • 1,353
  • 1
  • 12
  • 7
  • These appear to be three equivalent ways to perform the default `groupingBy` behavior. Which is interesting but I don't think what the OP wanted (which was a single k -> v). :) – rogerdpack May 07 '21 at 19:45