Java Stream:查找具有属性的最小/最大值的元素

时间:2016-04-21 16:27:46

标签: java collections java-stream

我有一个对象流,我想找到一个具有某个属性的最大值的对象,计算起来很昂贵。

作为一个特定的简单示例,假设我们有一个字符串列表,我们希望找到最酷的字符串,给定coolnessIndex函数。

以下内容应该有效:

String coolestString = stringList
        .stream()
        .max((s1, s2) -> Integer.compare(coolnessIndex(s1), coolnessIndex(s2)))
        .orElse(null);

现在,这有两个问题。首先,假设coolnessIndex的计算成本很高,这可能不会非常有效。我想max方法需要重复使用比较器,反过来会反复调用coolnessIndex,最后每个字符串会多次调用它。

其次,必须提供比较器会导致代码中出现一些冗余。我更喜欢这样的语法:

String coolestString = stringList
        .stream()
        .maxByAttribute(s -> coolnessIndex(s))
        .orElse(null);

但是,我无法在Stream API中找到匹配的方法。这让我感到惊讶,因为通过属性查找min / max似乎是一种常见的模式。我想知道是否有比使用比较器更好的方法(除了for循环)。

9 个答案:

答案 0 :(得分:8)

这是使用Object[]作为元组的变体,而不是最漂亮的代码,但简洁

String coolestString = stringList
        .stream()
        .map(s -> new Object[] {s, coolnessIndex(s)})
        .max(Comparator.comparingInt(a -> (int)a[1]))
        .map(a -> (String)a[0])
        .orElse(null);

答案 1 :(得分:8)

Stream<String> stringStream = stringList.stream();
String coolest = stringStream.reduce((a,b)-> 
    coolnessIndex(a) > coolnessIndex(b) ? a:b;
).get()

答案 2 :(得分:2)

感谢大家的建议。最后我在Efficiency of the way comparator works找到了我最喜欢的解决方案 - 来自bayou.io的答案:

拥有通用cache方法:

public static <K,V> Function<K,V> cache(Function<K,V> f, Map<K,V> cache)
{
    return k -> cache.computeIfAbsent(k, f);
}

public static <K,V> Function<K,V> cache(Function<K,V> f)
{
    return cache(f, new IdentityHashMap<>());
}

然后可以按如下方式使用:

String coolestString = stringList
        .stream()
        .max(Comparator.comparing(cache(CoolUtil::coolnessIndex)))
        .orElse(null);

答案 3 :(得分:1)

如何使用两个流,一个用于创建具有预先计算值的地图,另一个用于使用地图的条目集来查找最大值:

        String coolestString = stringList
            .stream()
            .collect(Collectors.toMap(Function.identity(), Test::coolnessIndex))
            .entrySet()
            .stream()
            .max((s1, s2) -> Integer.compare(s1.getValue(), s2.getValue()))
            .orElse(null)
            .getKey();

答案 4 :(得分:0)

这是一个减少问题。将列表减少到特定值。通常,reduce会对列表中的部分解决方案和项目进行操作。在这种情况下,这意味着要比较以前的胜利&#39;值列表中的新值将在每次比较时计算两次昂贵的操作。

根据https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html,另一种方法是使用collect而不是reduce。

自定义consumer类将允许跟踪昂贵的操作,因为它会减少列表。通过使用可变状态,消费者可以绕过对昂贵计算的多次调用。

    class Cooler implements Consumer<String>{

    String coolestString = "";
    int coolestValue = 0;

    public String coolest(){
        return coolestString;
    }
    @Override
    public void accept(String arg0) {
        combine(arg0, expensive(arg0));
    }

    private void combine (String other, int exp){
        if (coolestValue < exp){
            coolestString = other;
            coolestValue = exp;
        }
    }
    public void combine(Cooler other){
        combine(other.coolestString, other.coolestValue);
    }
}

此类接受一个字符串,如果它比前一个获胜者更冷,它会替换它并保存昂贵的计算值。

Cooler cooler =  Stream.of("java", "php", "clojure", "c", "lisp")
                 .collect(Cooler::new, Cooler::accept, Cooler::combine);
System.out.println(cooler.coolest());

答案 5 :(得分:0)

我会创建一个本地类(一个在方法中定义的类 - 很少见,但完全合法),并将对象映射到该类,因此昂贵的属性只计算一次:

class IndexedString {
    final String string;
    final int index;

    IndexedString(String s) {
        this.string = Objects.requireNonNull(s);
        this.index = coolnessIndex(s);
    }

    String getString() {
        return string;
    }

    int getIndex() {
        return index;
    }
}

String coolestString = stringList
    .stream()
    .map(IndexedString::new)
    .max(Comparator.comparingInt(IndexedString::getIndex))
    .map(IndexedString::getString)
    .orElse(null);

答案 6 :(得分:0)

您可以适当地利用从流中收集结果的想法。昂贵的酷度计算函数的约束使您考虑为流的每个元素调用该函数一次。

Java 8在collect上提供Stream方法,并提供了各种使用收集器的方法。看来,如果您使用TreeMap来收集结果,您可以保留表现力,同时保持对效率的考虑:

public class Expensive {
    static final Random r = new Random();
    public static void main(String[] args) {
        Map.Entry<Integer, String> e =
        Stream.of("larry", "moe", "curly", "iggy")
                .collect(Collectors.toMap(Expensive::coolness,
                                          Function.identity(),
                                          (a, b) -> a,
                                          () -> new TreeMap<>
                                          ((x, y) -> Integer.compare(y, x))
                        ))
                .firstEntry();
        System.out.println("coolest stooge name: " + e.getKey() + ", coolness: " + e.getValue());
    }

    public static int coolness(String s) {
        // simulation of a call that takes time.
        int x = r.nextInt(100);
        System.out.println(x);
        return x;
    }
}

此代码打印stooge具有最大凉爽度,coolness方法仅针对每个stooge调用一次。用作BinaryOperatormergeFunction)的(a, b) ->a可以进一步改进。

答案 7 :(得分:0)

首先创建您的(对象,指标)对:

public static <T> Optional<T> maximizeOver(List<T> ts, Function<T,Integer> f) {
    return ts.stream().map(t -> Pair.pair(t, f.apply(t)))
        .max((p1,p2) -> Integer.compare(p1.second(), p2.second()))
        .map(Pair::first);
}

(那些是com.googlecode.totallylazy.Pair的)

答案 8 :(得分:0)

如果速度/开销很重要,则您不想使用Stream.max(Comparator)来为获胜对象多次重新计算分数;上面的缓存解决方案可以工作,但是具有大量O(N)开销。装饰器模式需要更多的内存分配/ GC开销。

以下是适用于您的实用程序库的美观,可重用的解决方案和Java 15:

/** return argmin item, else null if none.  NAN scores are skipped */
public static <T> T argmin(Stream<T> stream, ToDoubleFunction<T> scorer) {
    Double min = null;
    T argmin = null;
    for (T p: (Iterable<T>) stream::iterator) {
        double score = scorer.applyAsDouble(p);
        if (min==null || min > score) {
            min = score;
            argmin = p;
        }
    }
    return argmin;
}