user3123610
user3123610

Reputation: 13

List to Map of Pairs in Java

Let's say my query result is like this

MyClass(name=AAA, action=action1, price=5),
MyClass(name=BBB, action=action13, price=7),
MyClass(name=AAA, action=action31, price=2)

and want to create a map grouped by name, where value will be a set of (k,v) on action and price, something like below

(AAA=((action1, 5),(action31, 2)), BBB=(action13, 7))

so been trying as below, but I'm getting error -> "non-static method cannot be referred from static content" when trying to use Map.Entry

Stream<Object> trying = results
    .stream()
    .flatMap(it -> {
          Map<String,String> innerMap = new HashMap<>();
          innerMap.put(it.getAction(),it.getPrice());
          Map<String,Map<String,String>> upperMap = new HashMap<>();
          upperMap.put(it.getName(), innerMap);
          return upperMap.entrySet().stream();
        }
    ) 
    .collect(Collectors.groupingBy(Map.Entry::getKey,
        Collectors.mapping(Map.Entry::getValue,
            Collectors.toList())));

Upvotes: 1

Views: 3185

Answers (4)

rzwitserloot
rzwitserloot

Reputation: 103913

Why? Having a Map<String, Map<String, String>> is a typing disaster. Java is nominal; the language works better if you use what it's good at, which is not that.

Your problem description is ambiguous. Imagine the following input:

MyClass(name=AAA, action=action1, price=5),
MyClass(name=BBB, action=action13, price=7),
MyClass(name=AAA, action=action1, price=2)

What should the output be?

  • An exception
  • (AAA=((action1, 2)), BBB=(action13, 7))
  • (AAA=((action1, 5)), BBB=(action13, 7))
  • (AAA=((action1, [2, 5])), BBB=(action13, [7]))

That last one is somewhat sensible, because the others, particularly #2 and #3, are utterly arbitrary. Even if you want one of those, hopefully it helps you to realize that it won't be an easy, understandable series of functional operations to get there, and most likely you don't want to use streams at all - because not using them would lead to more readable code.

The signatures, in any case, do not match up with what you wrote; I assume you messed up (you seem to have set the type of the price variant as a String which seems a little crazy. In this code I'll represent it as an Integer, representing atomic units (eurocents, satoshis, etc).

If you really want this (And I highly doubt you do!), then let's break down the steps:

  • You first want to group by, giving you the name (AAA) as group key, and then a list of MyClass instances.
  • You then want to group-by again, grouping all MyClass instances with the same action name together, in order to end up with that [2, 5] integer list.
  • You also want to map your MyClass instance to just its price somewhere along this process.

Perusing the API of the stream stuff, specifically, Collectors, gives us the call you want:

groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)

Returns a Collector implementing a cascaded "group by" operation on input elements of type T, grouping elements according to a classification function, and then performing a reduction operation on the values associated with a given key using the specified downstream Collector.

So let's get to it, assuming you want the 4th option:

@lombok.Value public class MyClass {
  String name, action;
  int price;
}

List<MyClass> results = List.of(
  new MyClass("AAA", "action1", 5),
  new MyClass("BBB", "action2", 4),
  new MyClass("AAA", "action1", 3),
  new MyClass("AAA", "action3", 2));

Map<String, Map<String, List<MyClass>>> answer =
  results.stream()
  .collect(Collectors.groupingBy(MyClass::getName,
    Collectors.groupingBy(MyClass::getAction)));

System.out.println(answer);

That's a good first step, but instead of a list of MyClass instances, you wanted just a List of integers. We can do that, too:

Map<String, Map<String, List<Integer>>> answer =
  results.stream()
  .collect(Collectors.groupingBy(MyClass::getName,
    Collectors.groupingBy(MyClass::getAction,
      Collectors.collectingAndThen(Collectors.toList(),
     listOfMc -> listOfMc.stream().map(MyClass::getPrice).collect(Collectors.toList())
  ))));

And this gets you:

{AAA={action1=[5, 3], action3=[2]}, BBB={action2=[4]}}

Exactly what you wanted, I think.

But, then look back at that code and realize that somewhere, somehow, you made some wrong choices :P - that's quite a far sight removed from easily readable code!

Upvotes: 0

Lovesh Dongre
Lovesh Dongre

Reputation: 1344

My suggestion will be to try using a map of type Map<String, List<MyClass>> this way it will be more manageable.

Here an idea how it can be coded.

import java.util.*;

public class MyClass {
    String name;
    String action;
    int price;

    public MyClass(String name, String action, int price) {
        this.name = name;
        this.action = action;
        this.price = price;
    }

    @Override
    public String toString() {
        return  "{" + name + ", " + action + ", " + price +"}";
    }

    public static void main(String[] args) {
        Map<String, List<MyClass>> map = new HashMap<>();
        
        MyClass[] objs = {  
                            new MyClass("AAA", "action1", 5),
                            new MyClass("BBB", "action2", 7),
                            new MyClass("AAA", "action3", 2)
                        };

        for(MyClass myClass: objs) {
            if(!map.containsKey(myClass.name)) {
                map.put(myClass.name, new ArrayList<MyClass>());
            }
            map.get(myClass.name).add(myClass);
        }
        
        System.out.println(map);
    }
}

Upvotes: 0

Gautham M
Gautham M

Reputation: 4935

You could provide a Collector along with groupingBy to produce the inner map: (Assume static import for Collectors.groupingBy and Collectors.toMap)

results.stream()
       .collect(groupingBy(MyClass::getName,
                           toMap(MyClass::getAction,MyClass::getPrice)));       

Upvotes: 1

Manish Kumar
Manish Kumar

Reputation: 634

You need to process them in a complete oops way. Here is one clean, and readable way:-



public class Builder {

    public static void main(String[] args) throws IOException {
        List<MyClass> list = Arrays.asList(new MyClass("AAA", "action1", 5)
                , new MyClass("BBB", "action13", 7), new MyClass("AAA", "action31", 2));

        Map<String, List<Pairs>> listList = new HashMap<String, List<Pairs>>();

        list.stream().map(l -> {
            List<Pairs> pairs = listList.getOrDefault(l.name, new ArrayList<>());

            pairs.add(new Pairs(l.action, l.price));
            listList.put(l.name, pairs);

            return pairs;
        }).collect(Collectors.toList());

        System.out.println(listList);
//        {AAA=[Pairs{action='action1', price=5}, Pairs{action='action31', price=2}], BBB=[Pairs{action='action13', price=7}]}

    }

}

class Pairs {
  String action;
  int price;

    public Pairs(String action, int price) {
        this.action = action;
        this.price = price;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Pairs{");
        sb.append("action='").append(action).append('\'');
        sb.append(", price=").append(price);
        sb.append('}');
        return sb.toString();
    }
}

class MyClass {
    String name;
    String action;
    int price = 5;

    public MyClass(String name, String action, int price) {
        this.name = name;
        this.action = action;
        this.price = price;
    }
}


Upvotes: 0

Related Questions