Stream groupingBy: reducing to first element of list [duplicate]
Asked Answered
Y

6

46

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.

Yunyunfei answered 28/12, 2015 at 15:0 Comment(0)
O
68

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.

Objurgate answered 28/12, 2015 at 15:11 Comment(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.Duhamel
Lets say I want Map<String, String> instead of Map<String, Valuta>, I need Valuta.getDescription() , basically, is that possible?Rounder
Be aware that Collectors.toMap will throw an exception if you somehow have duplicate keys! They are not completely equivalent.Conformance
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://mcmap.net/q/36543/-ignore-duplicates-when-producing-map-using-streamsConformance
F
46

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))));
Fruma answered 6/1, 2018 at 3:23 Comment(4)
This is what i am finding forChelicera
this is what's needed in case you actually do have duplicates, but don't want to use itStibnite
You might be able to use the mergeFunction of .toMap to simply drop any duplicates and avoid extra object allocations: https://mcmap.net/q/36543/-ignore-duplicates-when-producing-map-using-streamsConformance
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)))Paediatrics
H
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.

Hyaluronidase answered 28/12, 2015 at 15:11 Comment(0)
D
12

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));
Designation answered 14/5, 2020 at 22:25 Comment(2)
Does the merge function preserve ordering? i.e. is it guaranteed that v1 always appears before v2 in list?Bagel
@Bagel 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://mcmap.net/q/36928/-does-parallelstream-in-java-8-guarantee-the-orderDesignation
K
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;
}
Kuhlmann answered 6/3, 2020 at 9:32 Comment(0)
F
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

}
Forehand answered 13/9, 2019 at 11:8 Comment(1)
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). :)Murrelet

© 2022 - 2024 — McMap. All rights reserved.