0

I have a list of shop objects that are grouped by the item they have.

class Shop{
  String shopName;
  String item;
  int size;
...}

How can I get a list of the 3 biggest shops (or n biggest shops) for each item? ie. suppose I have

Shop("Walmart", "Hammer", 100);
Shop("Target", "Scissor", 30);
Shop("Walgreens", "Hammer", 300);
Shop("Glens", "Hammer", 500);
Shop("Walmart", "Scissor", 75);
Shop("Toms", "Hammer", 150);

I want to return a list of the top 3 shops grouped by item. I grouped the items but i am not sure how to iterate through the given Map or entryset...

public class Shop {
  int size;
  String item;
  String name;

  public Shop(int size, String item, String name){
    this.size = size;
    this.item = item;
    this.name = name;
  }



  //Return a list of the top 3 largest shops by item
  public static void main(){


    List<Shop> shops = new LinkedList<Shop>();


    Comparator<Shop> shopComparator = new Comparator<Shop>(){
      @Override
      public int compare(Shop f1, Shop f2) {
        return f1.getSize() < f2.getSize() ? 1 : -1;
      }
    };

    shops.stream().collect(groupingBy(Shop::getItem))
            .entrySet()
            .stream()
            .filter(entry -> entry.getValue().stream().map )
            .forEach(item -> item.getValue())//Stuck here
            ;
  }

}

Linkx_lair
  • 569
  • 1
  • 9
  • 21
  • what do you mean by top 3 ? and you wanna get for specific item ? or for all items ?, can you show expected output ? – Ryuzaki L Oct 21 '21 at 15:16
  • Is this a school assignment that requires use of a `Stream`? It can certainly be done, but for large datasets, using a heap is more efficient and probably more readable than the approach I have in mind for a stream. – erickson Oct 21 '21 at 15:18
  • @erickson Its not a school assignment - just a personal project and trying to better grasp how to effectively use streams. – Linkx_lair Oct 21 '21 at 15:19
  • I am kind of confused by the code, that .stream().map seems like incomplete code and was intended to be a call to map() perhaps? – Gimby Oct 21 '21 at 16:39
  • @Gimby - that is where i am stuck. I am not sure how to process the Map object from the groupingBy – Linkx_lair Oct 21 '21 at 16:40
  • Well... then you miscommunicated that pretty badly by putting the "stuck here" on the forEach. – Gimby Oct 22 '21 at 08:08

4 Answers4

2

The most important thing that you can learn about streams is that they aren't inherently "better" than equivalent approaches by any measure. Sometimes, they make code more readable, other times, less so. Use them to clarify your code, and avoid them when they obfuscate it.

This is a case where your code will be far more readable by using a collector for this purpose. Coding your own is fairly easy, and if you really want to understand streams better, I recommend it as a simple learning exercise.

Here, I'm using MoreCollectors.greatest() from the StreamEx library:

Comparator<Shop> bySize = Comparator.comparingInt(Shop::getSize);
Map<String, List<Shop>> biggestByItem
    = shops.stream().collect(groupingBy(Shop::getItem, greatest(bySize, 3)));

This isn't better because it's shorter, or because it is faster and uses constant memory; it's better because complexity is factored out of the code, and hidden behind meaningful names that explain the behavior. Instead of littering your application with complex pipelines that need to be read, tested, and maintained independently, you have written (or referenced) a reusable collector with a clear behavior.

As I mentioned, there is a bit of a learning curve in understanding how the pieces of a Collector work together, but it's worth studying. Here's a possible implementation for a similar collector:

public static <T> Collector<T, ?, List<T>> top(int limit, Comparator<? super T> order) {
    if (limit < 1) throw new IndexOutOfBoundsException(limit);
    Objects.requireNonNull(order);

    Supplier<Queue<T>> supplier = () -> new PriorityQueue<>(order);
    BiConsumer<Queue<T>, T> accumulator = (q, e) -> collect(order, limit, q, e);
    BinaryOperator<Queue<T>> combiner = (q1, q2) -> {
        q2.forEach(e -> collect(order, limit, q1, e));
        return q1;
    };
    Function<Queue<T>, List<T>> finisher = q -> {
        List<T> list = new ArrayList<>(q);
        Collections.reverse(list);
        return list;
    };
    return Collector.of(supplier, accumulator, combiner, finisher, Collector.Characteristics.UNORDERED);
}

private static <T> void collect(Comparator<? super T> order, int limit, Queue<T> q, T e) {
    if (q.size() < limit) {
        q.add(e);
    } else if (order.compare(e, q.peek()) > 0) {
        q.remove();
        q.add(e);
    }
}

Given this factory, it's trivial to create others that give you bottom(3, bySize), etc.

You may be interested in this related question and its answers.

erickson
  • 265,237
  • 58
  • 395
  • 493
  • 1
    OMG! WOW! Mind is blown! thank you erickson for such insight. i never even knew that was possible - hats off to you – Linkx_lair Oct 22 '21 at 20:07
1

Well, you could take the following steps:

  • With groupingBy(Shop::getItem), you could create a map which sorts by the item, so your result would be a Map<String, List<Shop>>, where the list contains all shops with that item.

  • Now we need to sort the List<Shop> in reversed order, so the top items of the list are the shops with the largest size. In order to do this, we could use collectingAndThen as downstream collector to groupingBy.

    Collectors.collectingAndThen(Collectors.toList(), finisherFunction);
    

    Our finisher function should sort the list:

    list -> {
        Collections.sort(list, Comparator.comparing(Shop::size).reversed());
        return list;
    }
    

    This would result in a Map<String, List<Shop>>, where the list is sorted, highest size first.

  • Now the only thing we need to do, is limiting the list size to 3. We could use subList. I think subList throws an exception if the list contains less than 3 items, so we need to use Math.min(3, list.size()) to take this into account.

    list -> {
        Collections.sort(list, Comparator.comparing(Shop::size).reversed());
        return list.subList(0, Math.min(3, list.size()));
    }
    

The whole code then looks like this:

shops.stream()
    .collect(groupingBy(Shop::item, Collectors.collectingAndThen(Collectors.toList(), list -> {
        Collections.sort(list, Comparator.comparing(Shop::size).reversed());
        return list.subList(0, Math.min(3, list.size()));
    })));

Online demo


Instead of 'manually' sorting the list and limiting it to 3, you could create a small class which automatically does this — both limit and sort the list upon adding elements.

MC Emperor
  • 22,334
  • 15
  • 80
  • 130
  • yep i would migrate the code into a separate class or method at least - just playing around with streams atm. I did not know you could do that with "collectingAndThen" – Linkx_lair Oct 21 '21 at 15:31
  • In the return list line I get a "Bad return type in lambda expression: List cannot be converted to List". – Linkx_lair Oct 21 '21 at 15:36
  • Well, if you store the result in a `Map>` variable, then it *should* work. – MC Emperor Oct 21 '21 at 21:19
0

Not as fancy as MC Emperor but it seems to work. I started from the part you already did:

shops.stream().collect(Collectors.groupingBy(Shop::getItem))
        .entrySet().stream().map(entry -> {
            entry.setValue(entry.getValue().stream()
              .sorted(Comparator.comparingInt(s->-s.size))
              .limit(3) // only keep top 3
              .collect(Collectors.toList()));
            return entry;
    }).forEach(item -> {
        System.out.println(item.getKey()+":"+item.getValue());
    });
Conffusion
  • 4,335
  • 2
  • 16
  • 28
0

You can use groupingBy along with limit to get desired result:

import static java.util.stream.Collectors.*;

// Define the sort logic. reversed() applies asc order (Default is desc)
Comparator<Shop> sortBySize = Comparator.comparingInt(Shop::getSize).reversed();
int limit = 3; // top n items

var itemToTopNShopsMap = list.stream().collect(
        collectingAndThen(groupingBy(Shop::getItem),
                itemToShopsMap -> getTopNShops(sortBySize, itemToShopsMap, limit)));


static Map<String, List<Shop>> getTopNShops(Comparator<Shop> sortBy, Map<String, List<Shop>> inMap, int limit) {
    var returningMap = new HashMap<String, List<Shop>>();
    for (var i : inMap.entrySet()) {
        returningMap.put(i.getKey(),  i.getValue().stream().sorted(sortBy).limit(Long.valueOf(limit)).collect(toList()));
    }
    return returningMap;
}

We took following steps:

  1. Group the List by 'item'
  2. For each grouping, i.e., item to list of shops entry, we sort the list of shops by predefined sort logic and collect (limit) the top n results.

Note:
In static method getTopNShops, mutation of source map is avoided. We could have written this method as a stream, but the stream version may have been less readable than the foreach loop.

adarsh
  • 1,393
  • 1
  • 8
  • 16