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循环)。
这是一个使用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);
Stream stringStream = stringList.stream(); String coolest = stringStream.reduce((a,b)-> coolnessIndex(a) > coolnessIndex(b) ? a:b; ).get()
谢谢大家的建议。 最后,我找到了比较适用的方式效率最高的解决方案 – 来自bayou.io的答案:
有一个通用的cache
方法:
public static Function cache(Function f, Map cache) { return k -> cache.computeIfAbsent(k, f); } public static Function cache(Function f) { return cache(f, new IdentityHashMap<>()); }
然后可以使用如下:
String coolestString = stringList .stream() .max(Comparator.comparing(cache(CoolUtil::coolnessIndex))) .orElse(null);
如何使用两个流,一个用于创建具有预先计算值的地图,另一个用于使用地图的条目集来查找最大值:
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();
我会创建一个本地类(一个在方法中定义的类 – 罕见,但完全合法),并将您的对象映射到该类,因此昂贵的属性只为每个计算一次:
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);
您可以利用从流中收集结果的想法。 昂贵的酷度计算函数的约束使您考虑为流的每个元素调用该函数一次。
Java 8提供了Stream
上的collect
方法以及可以使用收集器的各种方法。 看来,如果您使用TreeMap
收集结果,您可以保持表现力,同时保持对效率的考虑:
public class Expensive { static final Random r = new Random(); public static void main(String[] args) { Map.Entry 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
,并且每个stooge
都会调用coolness
方法一次。 可以进一步改进用作mergeFunction
( (a, b) ->a
)的mergeFunction
。
这是一个减少问题。 将列表减少到特定值。 通常,reduce会按照部分解决方案和列表中的项目操作列表。 在这种情况下,这意味着将先前的“获胜”值与列表中的新值进行比较,该值将在每次比较时计算两次昂贵的操作。
根据https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html ,另一种方法是使用collect而不是reduce。
自定义使用者类将允许在减少列表时跟踪昂贵的操作。 消费者可以通过使用可变状态来绕过对昂贵计算的多次调用。
class Cooler implements Consumer{ 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());
首先创建您的(对象,度量)对:
public static Optional maximizeOver(List ts, Function 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的)