【问题标题】:How to force max to return ALL maximum values in a Java Stream?如何强制 max 返回 Java Stream 中的所有最大值?
【发布时间】:2015-06-02 18:31:08
【问题描述】:

我在 Java 8 lambdas 和流上测试了 max 函数,似乎在执行 max 的情况下,即使多个对象与 0 比较,它也会返回并列候选人,无需进一步考虑。

对于这种最大预期行为是否有明显的技巧或功能,以便返回所有最大值?我在 API 中看不到任何内容,但我确信它一定存在比手动比较更好的东西。

例如:

// myComparator is an IntegerComparator
Stream.of(1, 3, 5, 3, 2, 3, 5)
    .max(myComparator)
    .forEach(System.out::println);
// Would print 5, 5 in any order.

【问题讨论】:

  • 您究竟希望这个max 方法返回什么? HashSet?
  • 哈希集或链接更多操作的方法,就好像它是一个过滤器一样,具有绑定的最大值
  • 您的说法不正确。如果流是有序的(例如从数组或列表中得到的流),它会返回在多个最大元素的情况下最大的 first 元素;只有当流是无序的时,它才允许选择任意元素。
  • 谢谢你的意思,我没想到

标签: java collections lambda java-8 java-stream


【解决方案1】:

我相信 OP 正在使用Comparator 将输入划分为等价类,并且期望的结果是根据Comparator 最大的等价类成员列表。

不幸的是,使用int 值作为示例问题是一个糟糕的例子。所有相等的int 值都是可替代的,因此没有保留等价值顺序的概念。也许一个更好的例子是使用字符串长度,其中期望的结果是从输入中返回一个字符串列表,这些字符串在该输入中都具有最长的长度。

如果不将至少部分结果存储在集合中,我不知道有任何方法可以做到这一点。

给定一个输入集合,比如说

List<String> list = ... ;

...这很简单,分两次执行,第一次获得最长的长度,第二次过滤具有该长度的字符串:

int longest = list.stream()
                  .mapToInt(String::length)
                  .max()
                  .orElse(-1);

List<String> result = list.stream()
                          .filter(s -> s.length() == longest)
                          .collect(toList());

如果输入是cannot be traversed more than once 的流,则可以使用收集器仅在一次传递中计算结果。编写这样的收集器并不难,但有点繁琐,因为要处理几种情况。给定Comparator,生成这样一个收集器的辅助函数如下:

static <T> Collector<T,?,List<T>> maxList(Comparator<? super T> comp) {
    return Collector.of(
        ArrayList::new,
        (list, t) -> {
            int c;
            if (list.isEmpty() || (c = comp.compare(t, list.get(0))) == 0) {
                list.add(t);
            } else if (c > 0) {
                list.clear();
                list.add(t);
            }
        },
        (list1, list2) -> {
            if (list1.isEmpty()) {
                return list2;
            } 
            if (list2.isEmpty()) {
                return list1;
            }
            int r = comp.compare(list1.get(0), list2.get(0));
            if (r < 0) {
                return list2;
            } else if (r > 0) {
                return list1;
            } else {
                list1.addAll(list2);
                return list1;
            }
        });
}

这会将中间结果存储在ArrayList 中。不变量是任何此类列表中的所有元素在Comparator 方面都是等效的。添加元素时,如果小于列表中的元素,则忽略;如果相等,则相加;如果它更大,则清空列表并添加新元素。合并也不是太难:返回具有较大元素的列表,但如果它们的元素相等,则追加列表。

给定一个输入流,这很容易使用:

Stream<String> input = ... ;

List<String> result = input.collect(maxList(comparing(String::length)));

【讨论】:

  • 很好的答案。一旦你了解了供应商/累加器/组合器/整理器的命名法,编写自己的收集器真的很强大,实际上也很简单!
【解决方案2】:

我会按值分组并将值存储到TreeMap 中以便对我的值进行排序,然后通过将最后一个条目作为下一个条目来获得最大值:

Stream.of(1, 3, 5, 3, 2, 3, 5)
    .collect(groupingBy(Function.identity(), TreeMap::new, toList()))
    .lastEntry()
    .getValue()
    .forEach(System.out::println);

输出:

5
5

【讨论】:

  • 如果 Stream.of() 为空,则属于 NPE。有人知道怎么解决吗?
  • @Svetopolk 您只需要使用简单的 if 或将 lastEntry() 之前的所有内容包装到 Optional.ofNullable(...) 中来检查最后一个条目是否不为空
  • mmm... 可以用流(方法链)的方式写吗?整个解决方案的外观如何(以及 NPE 保存)?
【解决方案3】:

我使用自定义下游收集器实现了更通用的收集器解决方案。可能有些读者会觉得它很有用:

public static <T, A, D> Collector<T, ?, D> maxAll(Comparator<? super T> comparator, 
                                                  Collector<? super T, A, D> downstream) {
    Supplier<A> downstreamSupplier = downstream.supplier();
    BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
    BinaryOperator<A> downstreamCombiner = downstream.combiner();
    class Container {
        A acc;
        T obj;
        boolean hasAny;
        
        Container(A acc) {
            this.acc = acc;
        }
    }
    Supplier<Container> supplier = () -> new Container(downstreamSupplier.get());
    BiConsumer<Container, T> accumulator = (acc, t) -> {
        if(!acc.hasAny) {
            downstreamAccumulator.accept(acc.acc, t);
            acc.obj = t;
            acc.hasAny = true;
        } else {
            int cmp = comparator.compare(t, acc.obj);
            if (cmp > 0) {
                acc.acc = downstreamSupplier.get();
                acc.obj = t;
            }
            if (cmp >= 0)
                downstreamAccumulator.accept(acc.acc, t);
        }
    };
    BinaryOperator<Container> combiner = (acc1, acc2) -> {
        if (!acc2.hasAny) {
            return acc1;
        }
        if (!acc1.hasAny) {
            return acc2;
        }
        int cmp = comparator.compare(acc1.obj, acc2.obj);
        if (cmp > 0) {
            return acc1;
        }
        if (cmp < 0) {
            return acc2;
        }
        acc1.acc = downstreamCombiner.apply(acc1.acc, acc2.acc);
        return acc1;
    };
    Function<Container, D> finisher = acc -> downstream.finisher().apply(acc.acc);
    return Collector.of(supplier, accumulator, combiner, finisher);
}

所以默认情况下可以使用以下方法将其收集到一个列表中:

public static <T> Collector<T, ?, List<T>> maxAll(Comparator<? super T> comparator) {
    return maxAll(comparator, Collectors.toList());
}

但您也可以使用其他下游收集器:

public static String joinLongestStrings(Collection<String> input) {
    return input.stream().collect(
            maxAll(Comparator.comparingInt(String::length), Collectors.joining(","))));
}

【讨论】:

    【解决方案4】:

    如果我理解得很好,你想要 max 值在 Stream 中的频率。

    实现此目的的一种方法是,当您从 Stream 中收集元素时,将结果存储在 TreeMap&lt;Integer, List&lt;Integer&gt; 中。然后你抓住最后一个键(或第一个取决于你给出的比较器)来获取包含最大值列表的值。

    List<Integer> maxValues = st.collect(toMap(i -> i,
                         Arrays::asList,
                         (l1, l2) -> Stream.concat(l1.stream(), l2.stream()).collect(toList()),
                         TreeMap::new))
                 .lastEntry()
                 .getValue();
    

    Stream(4, 5, -2, 5, 5) 收集它会给你一个List [5, 5, 5]

    本着同样精神的另一种方法是使用 group by 操作结合 counting() 收集器:

    Entry<Integer, Long> maxValues = st.collect(groupingBy(i -> i,
                    TreeMap::new,
                    counting())).lastEntry(); //5=3 -> 5 appears 3 times
    

    基本上你首先会得到一个Map&lt;Integer, List&lt;Integer&gt;&gt;。然后下游 counting() 收集器将返回每个列表中由其键映射的元素数,从而生成一个 Map。从那里您可以获取最大条目。

    第一种方法需要存储流中的所有元素。第二个更好(参见 Holger 的评论),因为没有构建中间 List。在这两种方法中,结果都是一次计算的。

    如果您从集合中获取源,您可能希望使用Collections.max 一次来查找最大值,然后使用Collections.frequency 来查找该值出现的次数。

    它需要两次传递,但使用的内存更少,因为您不必构建数据结构。

    等效的流是coll.stream().max(...).get(...),后跟coll.stream().filter(...).count()

    【讨论】:

    • “这两种方法都需要存储流中的所有元素”,考虑到第二种方法只存储 counts 而不是元素列表,这是一种误导性的说法。这就是提供另一个Collector 而不是首先创建Map&lt;…, List…&gt; 的全部意义所在。它必须处理每个项目,但不会存储项目。
    • @Holger 谢谢,我不知道我为什么这么认为。
    【解决方案5】:

    我不确定你是否正在尝试

    • (a) 找出最大项的出现次数,或
    • (b) 找出与equals 不一致的Comparator 的所有最大值。

    (a)的一个例子是[1, 5, 4, 5, 1, 1] -&gt; [5, 5]

    (b) 的一个例子是:

    Stream.of("Bar", "FOO", "foo", "BAR", "Foo")
          .max((s, t) -> s.toLowerCase().compareTo(t.toLowerCase()));
    

    你想给[Foo, foo, Foo],而不仅仅是FOOOptional[FOO]

    在这两种情况下,都有一些巧妙的方法可以一次性完成。但是这些方法的价值值得怀疑,因为您需要在此过程中跟踪不必要的信息。例如,如果您以[2, 0, 2, 2, 1, 6, 2] 开头,那么只有当您到达6 时,您才会意识到没有必要跟踪所有2s。

    我认为最好的方法是显而易见的;使用max,然后再次迭代项目,将所有关系放入您选择的集合中。这适用于 (a) 和 (b)。

    【讨论】:

    • 是的,给定一个比较器(没有任何平局系统),它正在获得最大出现次数(不是索引)。最好以流方式(我的意思是输出是那些最大元素的流,就像它是过滤器,而不是收集器或结束操作)
    • @user1352530 我认为你问错了问题。人们过去常常问“我怎么做X?”。现在他们问“我怎样才能使用流来做 X?”。这是个错误的问题。目标是做 X,而不是使用流。
    • 我只是在学习方法,并不是真正的需要。我可以解决它,但我确信必须有一个有效的或捷径。另外,看看帖子更新
    • @user1352530:你不能像filter 操作那样单通流,因为你必须处理每个元素,包括最后一个元素,然后才能确定最大元素是什么。只需考虑最后一个元素是唯一最大值的可能性。即使您找到一种方法将这一事实隐藏在看起来像单个流操作的事物中(例如 Stream.sorted),它也意味着在下游操作发生之前处理/收集所有元素。
    【解决方案6】:

    如果您宁愿依赖库而不是此处的其他答案,请StreamEx has a collector 执行此操作。

    Stream.of(1, 3, 5, 3, 2, 3, 5)
        .collect(MoreCollectors.maxAll())
        .forEach(System.out::println);
    

    a version which takes a Comparator 也用于不具有自然顺序的项目流(即不实现 Comparable)。

    【讨论】:

      【解决方案7】:

      我一直在寻找关于这个问题的好答案,但有点复杂,直到我自己弄清楚之前找不到任何东西,这就是为什么如果这对任何人有帮助,我会发布。

      我有一份小猫名单。 小猫是一个有名字、年龄和性别的物体。我必须返回一份所有最小小猫的名单。

      例如: 所以小猫列表将包含小猫对象(k1,k2,k3,k4),它们的年龄相应地是(1、2、3、1)。我们想要返回 [k1, k4],因为它们都是最年轻的。如果只存在一个最年轻的,则该函数应返回 [k1(youngest)]。

      1. 查找列表的最小值(如果存在):

         Optional<Kitten> minKitten = kittens.stream().min(Comparator.comparingInt(Kitten::getAge));
        
      2. 按最小值过滤列表

         return minKitten.map(value -> kittens.stream().filter(kitten -> kitten.getAge() == value.getAge())
               .collect(Collectors.toList())).orElse(Collections.emptyList());
        

      【讨论】:

      • 您没有找到好的答案令人惊讶,因为this answer 正好提供您想要的;你只需要适应你的情况,最小年龄的小猫而不是最大长度的字符串,即int youngest = kittens.stream() .mapToInt(Kitten::getAge) .min() .orElse(-1); List&lt;Kitten&gt; result = kittens.stream() .filter(kitten -&gt; kitten.getAge() == youngest) .collect(toList());你现在做的基本相同,只是Optional
      【解决方案8】:

      以下两行将在不实现单独的比较器的情况下完成:

        List<Integer> list = List.of(1, 3, 5, 3, 2, 3, 5);
        list.stream().filter(i -> i == (list.stream().max(Comparator.comparingInt(i2 -> i2))).get()).forEach(System.out::println);
      

      【讨论】:

      • 这会导致较大列表的性能很糟糕,因为您正在重复搜索每个元素的最大元素。也称为二次时间复杂度。
      • 这个答案真的是?。抛开性能不谈,因为这在许多情况下都被高估了,除非真正在企业规模上工作。如果这是一个问题,那么下面的其他答案??显示相同的两步方法。
      【解决方案9】:
      System.out.println(
        Stream.of(1, 3, 5, 3, 2, 3, 5)
          .map(a->new Integer[]{a})
          .reduce((a,b)-> 
              a[0]==b[0]?
                  Stream.concat(Stream.of(a),Stream.of(b)).toArray() :
                  a[0]>b[0]? a:b
          ).get()
      )
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-22
        • 1970-01-01
        • 1970-01-01
        • 2019-10-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多