使用 java 流找到最大的 3 家商店

find the largest 3 shops using java stream

我有一个商店对象列表,这些对象按所拥有的商品分组。

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

如何获得每件商品最大的 3 家商店(或 n 家最大的商店)的列表? IE。假设我有

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

我想要 return 按项目分组的前 3 家商店的列表。 我对项目进行了分组,但我不确定如何遍历给定的地图或条目集...

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
            ;
  }

}

那么,您可以采取以下步骤:

  • 使用 groupingBy(Shop::getItem),您可以创建一个按项目排序的地图,因此您的结果将是 Map<String, List<Shop>>,其中列表包含具有该项目的所有商店。

  • 现在我们需要对 List<Shop> 进行倒序排序,因此列表的顶部项目是规模最大的商店。为了做到这一点,我们可以使用 collectingAndThen 作为 下游收集器 groupingBy.

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

    我们的整理函数应该对列表进行排序:

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

    这将导致 Map<String, List<Shop>>,其中列表排序,最大的排在前面。

  • 现在我们唯一需要做的就是将列表大小限制为 3。我们可以使用 subList。我认为如果列表包含的项目少于 3 个,subList 会抛出异常,因此我们需要使用 Math.min(3, list.size()) 来考虑这一点。

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

整个代码如下所示:

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


您可以创建一个小的 class 来自动执行此操作,而不是 'manually' 对列表进行排序并将其限制为 3 个 - 在添加元素时对列表进行限制和排序。

不像MC Emperor那么花哨,但它似乎有效。 我从你已经做过的部分开始:

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());
    });

您可以将 groupingBylimit 一起使用以获得所需的结果:

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;
}

我们采取了以下步骤:

  1. 按 'item'
  2. 对列表进行分组
  3. 对于每个分组,即从项目到商店条目列表,我们按照预定义的排序逻辑对商店列表进行排序,并收集 (limit) 前 n 个结果。

注:
在静态方法 getTopNShops 中,避免了源映射的突变。我们可以将此方法编写为流,但流版本的可读性可能不如 foreach 循环。

关于流,您可以了解的最重要的一点是,无论以何种标准衡量,它们在本质上并不比同等方法“更好”。有时,它们使代码更具可读性,有时则不然。使用它们来阐明您的代码,并在它们混淆代码时避免使用它们。

在这种情况下,通过为此目的使用收集器,您的代码将更具可读性。编写自己的代码相当容易,如果您真的想更好地理解流,我建议您将其作为简单的学习练习。

在这里,我使用 StreamEx 库中的 MoreCollectors.greatest()

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

这不是更好,因为它更短,或者因为它更快并且使用常量内存;它更好,因为复杂性被排除在代码之外,并隐藏在解释行为的有意义的名称后面。您已经编写(或引用)了一个具有清晰行为的可重用收集器,而不是在您的应用程序中散布需要独立读取、测试和维护的复杂管道。

正如我提到的,理解 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);
    }
}

给定这个工厂,创建其他给你bottom(3, bySize)等的工厂是微不足道的

您可能对此 及其答案感兴趣。